์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ”— ๋ผ๋ผ๋ฒจ๋กœ ๋งŒ๋“œ๋Š” ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ถ€ ํˆฌ๋ช…์„ฑ ํ”Œ๋žซํผ

๐Ÿ”— ๋ผ๋ผ๋ฒจ๋กœ ๋งŒ๋“œ๋Š” ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ถ€ ํˆฌ๋ช…์„ฑ ํ”Œ๋žซํผ

ํˆฌ๋ช…ํ•œ ๊ธฐ๋ถ€ ์ƒํƒœ๊ณ„๋ฅผ ์œ„ํ•œ ์‹ค์ „ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ ๐Ÿ’Ž
๋ธ”๋ก 1 ๊ธฐ๋ถ€ ๋ฐ์ดํ„ฐ ๋ธ”๋ก 2 ์ง‘ํ–‰ ๋‚ด์—ญ ๋ธ”๋ก 3 ํˆฌ๋ช…์„ฑ ๊ฒ€์ฆ ๋ธ”๋ก N ์ง€์†์  ๊ธฐ๋ก ๐Ÿ’ฐ ๊ธฐ๋ถ€์ž ๐Ÿ” ๋ผ๋ผ๋ฒจ ํ”Œ๋žซํผ โค๏ธ ์ˆ˜ํ˜œ์ž ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ฐ˜ ํˆฌ๋ช…์„ฑ ์‹œ์Šคํ…œ ๋ชจ๋“  ๊ฑฐ๋ž˜๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ๊ธฐ๋ก๋˜๋Š” ์‹ ๋ขฐ์˜ ํ”Œ๋žซํผ

๐ŸŽฏ ์™œ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์ด ํ•„์š”ํ• ๊นŒ?

์•ผ, ์†”์งํžˆ ๋งํ•ด์„œ ๊ธฐ๋ถ€ํ•  ๋•Œ ์ œ์ผ ๊ฑฑ์ •๋˜๋Š” ๊ฒŒ ๋ญ์•ผ? ๋ฐ”๋กœ "๋‚ด๊ฐ€ ๋‚ธ ๋ˆ์ด ์ง„์งœ ์ œ๋Œ€๋กœ ์“ฐ์ด๋Š” ๊ฑฐ ๋งž์•„?" ์ด๊ฑฐ์ž–์•„. ๐Ÿ˜…

ํ†ต๊ณ„๋ฅผ ๋ณด๋ฉด ์ถฉ๊ฒฉ์ ์ด์•ผ. ๊ตญ๋‚ด ๊ธฐ๋ถ€์ž์˜ ์•ฝ 67%๊ฐ€ ๊ธฐ๋ถ€๊ธˆ ์‚ฌ์šฉ์ฒ˜์— ๋Œ€ํ•œ ๋ถˆ์‹ ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Œ€. ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ ์ผ๋ถ€ ๋‹จ์ฒด๋“ค์—์„œ ๋ถˆํˆฌ๋ช…ํ•œ ํšŒ๊ณ„ ์ฒ˜๋ฆฌ๋กœ ๋…ผ๋ž€์ด ๋˜๋Š” ๊ฒฝ์šฐ๋„ ์ข…์ข… ์žˆ์—ˆ์ง€.

์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ์žฅํ•œ ๊ฒŒ ๋ฐ”๋กœ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ฐ˜ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์ด์•ผ. ๋ธ”๋ก์ฒด์ธ์˜ ํ•ต์‹ฌ ํŠน์ง•์ธ '๋ถˆ๋ณ€์„ฑ'๊ณผ 'ํˆฌ๋ช…์„ฑ'์„ ํ™œ์šฉํ•˜๋ฉด, ๋ชจ๋“  ๊ธฐ๋ถ€ ๋‚ด์—ญ๊ณผ ์ง‘ํ–‰ ๊ณผ์ •์„ ๋ˆ„๊ตฌ๋‚˜ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฑฐ๋“ .

๊ทธ๋ฆฌ๊ณ  ์ด๊ฑธ ๋ผ๋ผ๋ฒจ(Laravel)๋กœ ๊ตฌํ˜„ํ•˜๋ฉด? ๊ฐœ๋ฐœ ์†๋„๋„ ๋น ๋ฅด๊ณ , ์œ ์ง€๋ณด์ˆ˜๋„ ์‰ฝ๊ณ , ํ™•์žฅ์„ฑ๋„ ์ข‹์•„. ์™„์ „ ์ผ์„์‚ผ์กฐ์ง€! ๐ŸŽ‰

๐Ÿ’ก ์ด ๊ธ€์—์„œ ๋‹ค๋ฃฐ ํ•ต์‹ฌ ๋‚ด์šฉ

โœ… ๋ธ”๋ก์ฒด์ธ๊ณผ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์˜ ๋งŒ๋‚จ
โœ… ๋ผ๋ผ๋ฒจ ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„
โœ… ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์—ฐ๋™ ๋ฐฉ๋ฒ•
โœ… ์‹ค์‹œ๊ฐ„ ํˆฌ๋ช…์„ฑ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„
โœ… ๋ณด์•ˆ๊ณผ ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ์ „๋žต
โœ… ์‹ค์ „ ์ฝ”๋“œ์™€ ๋ฐฐํฌ ๊ฐ€์ด๋“œ

์ด ๊ธ€์„ ๋‹ค ์ฝ๊ณ  ๋‚˜๋ฉด, ๋„ˆ๋„ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์„ ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿ’ช

๐Ÿ—๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ํ•˜๊ธฐ

์ž, ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๋จผ์ € ์ „์ฒด ์‹œ์Šคํ…œ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•ด.

๐Ÿ“ 3๊ณ„์ธต ์•„ํ‚คํ…์ฒ˜

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ํ”Œ๋žซํผ์€ ํฌ๊ฒŒ 3๊ฐœ ๊ณ„์ธต์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์–ด:

1. ํ”„๋ก ํŠธ์—”๋“œ ๊ณ„์ธต (Presentation Layer)
- Vue.js ๋˜๋Š” React๋กœ ๊ตฌํ˜„ํ•˜๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค
- ๊ธฐ๋ถ€์ž์™€ ์ˆ˜ํ˜œ ๋‹จ์ฒด๊ฐ€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๊ณต๊ฐ„
- ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ์™€ ํˆฌ๋ช…์„ฑ ๋ฆฌํฌํŠธ ์ œ๊ณต

2. ๋ฐฑ์—”๋“œ ๊ณ„์ธต (Application Layer)
- ๋ผ๋ผ๋ฒจ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
- API ์—”๋“œํฌ์ธํŠธ ์ œ๊ณต
- ์‚ฌ์šฉ์ž ์ธ์ฆ/์ธ๊ฐ€ ์ฒ˜๋ฆฌ
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ

3. ๋ธ”๋ก์ฒด์ธ ๊ณ„์ธต (Blockchain Layer)
- ์ด๋”๋ฆฌ์›€ ๋˜๋Š” ํ•˜์ดํผ๋ ˆ์ € ํŒจ๋ธŒ๋ฆญ ๊ฐ™์€ ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ
- ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์‹คํ–‰
- ๋ถˆ๋ณ€ ๊ฑฐ๋ž˜ ๊ธฐ๋ก ์ €์žฅ

์ด ์„ธ ๊ณ„์ธต์ด ์œ ๊ธฐ์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์–ด์•ผ ์ง„์งœ ํˆฌ๋ช…ํ•œ ํ”Œ๋žซํผ์ด ์™„์„ฑ๋˜๋Š” ๊ฑฐ์ง€! ๐ŸŽจ

๐Ÿ”ง ๊ธฐ์ˆ  ์Šคํƒ ์„ ์ •

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•  ๊ธฐ์ˆ  ์Šคํƒ์„ ์ •๋ฆฌํ•ด๋ดค์–ด:

์˜์—ญ ๊ธฐ์ˆ  ์„ ํƒ ์ด์œ 
๋ฐฑ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ Laravel 10.x ๊ฐ•๋ ฅํ•œ ORM, ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„, ๋น ๋ฅธ ๊ฐœ๋ฐœ ์†๋„
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค PostgreSQL ํŠธ๋žœ์žญ์…˜ ์•ˆ์ •์„ฑ, JSON ์ง€์›, ํ™•์žฅ์„ฑ
๋ธ”๋ก์ฒด์ธ Ethereum (Sepolia ํ…Œ์ŠคํŠธ๋„ท) ์„ฑ์ˆ™ํ•œ ์ƒํƒœ๊ณ„, ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ ๋„๊ตฌ
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ Solidity ์ด๋”๋ฆฌ์›€ ํ‘œ์ค€ ์–ธ์–ด, ๊ฒ€์ฆ๋œ ๋ณด์•ˆ ํŒจํ„ด
Web3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ web3.php PHP์—์„œ ๋ธ”๋ก์ฒด์ธ ์ƒํ˜ธ์ž‘์šฉ
ํ”„๋ก ํŠธ์—”๋“œ Vue.js 3 + Inertia.js ๋ผ๋ผ๋ฒจ๊ณผ์˜ ์™„๋ฒฝํ•œ ํ†ตํ•ฉ
ํ ์‹œ์Šคํ…œ Redis + Laravel Queue ๋น„๋™๊ธฐ ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ

๐Ÿ’พ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„

๋ธ”๋ก์ฒด์ธ๊ณผ ์ „ํ†ต์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ทจํ•  ๊ฑฐ์•ผ. ์™œ๋ƒํ•˜๋ฉด:

- ๋ธ”๋ก์ฒด์ธ: ์ค‘์š”ํ•œ ๊ฑฐ๋ž˜ ๊ธฐ๋ก, ํ•ด์‹œ๊ฐ’, ๋ถˆ๋ณ€ ๋ฐ์ดํ„ฐ
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: ์‚ฌ์šฉ์ž ์ •๋ณด, ๊ฒ€์ƒ‰ ์ธ๋ฑ์Šค, ์บ์‹œ ๋ฐ์ดํ„ฐ

์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•˜๋ฉด ์„ฑ๋Šฅ๋„ ์ข‹๊ณ  ๋น„์šฉ๋„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ฐ

๐Ÿ“Š ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ

users ํ…Œ์ด๋ธ”
๊ธฐ๋ถ€์ž์™€ ์ˆ˜ํ˜œ ๋‹จ์ฒด ์ •๋ณด๋ฅผ ์ €์žฅํ•ด:
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->string('wallet_address')->nullable()->unique();
    $table->enum('user_type', ['donor', 'beneficiary', 'admin']);
    $table->boolean('is_verified')->default(false);
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
    $table->softDeletes();
});
campaigns ํ…Œ์ด๋ธ”
๊ธฐ๋ถ€ ์บ ํŽ˜์ธ ์ •๋ณด:
Schema::create('campaigns', function (Blueprint $table) {
    $table->id();
    $table->foreignId('beneficiary_id')->constrained('users');
    $table->string('title');
    $table->text('description');
    $table->decimal('goal_amount', 20, 2);
    $table->decimal('current_amount', 20, 2)->default(0);
    $table->string('smart_contract_address')->nullable();
    $table->timestamp('start_date');
    $table->timestamp('end_date');
    $table->enum('status', ['draft', 'active', 'completed', 'cancelled']);
    $table->json('metadata')->nullable();
    $table->timestamps();
    $table->softDeletes();
    
    $table->index(['status', 'start_date', 'end_date']);
});
donations ํ…Œ์ด๋ธ”
๊ฐœ๋ณ„ ๊ธฐ๋ถ€ ๋‚ด์—ญ:
Schema::create('donations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('campaign_id')->constrained();
    $table->foreignId('donor_id')->constrained('users');
    $table->decimal('amount', 20, 2);
    $table->string('currency', 10)->default('KRW');
    $table->string('transaction_hash')->unique();
    $table->string('block_number')->nullable();
    $table->timestamp('blockchain_confirmed_at')->nullable();
    $table->enum('status', ['pending', 'confirmed', 'failed']);
    $table->text('message')->nullable();
    $table->boolean('is_anonymous')->default(false);
    $table->timestamps();
    
    $table->index(['campaign_id', 'status']);
    $table->index('transaction_hash');
});
expenditures ํ…Œ์ด๋ธ”
๊ธฐ๋ถ€๊ธˆ ์ง‘ํ–‰ ๋‚ด์—ญ (ํˆฌ๋ช…์„ฑ์˜ ํ•ต์‹ฌ!):
Schema::create('expenditures', function (Blueprint $table) {
    $table->id();
    $table->foreignId('campaign_id')->constrained();
    $table->decimal('amount', 20, 2);
    $table->string('category'); // ์˜ˆ: ์˜๋ฃŒ๋น„, ๊ต์œก๋น„, ์ƒํ™œ๋น„
    $table->text('description');
    $table->string('receipt_url')->nullable();
    $table->string('transaction_hash')->unique();
    $table->string('block_number')->nullable();
    $table->timestamp('executed_at');
    $table->timestamp('blockchain_confirmed_at')->nullable();
    $table->enum('status', ['pending', 'confirmed', 'rejected']);
    $table->foreignId('approved_by')->nullable()->constrained('users');
    $table->timestamps();
    
    $table->index(['campaign_id', 'status']);
});
blockchain_logs ํ…Œ์ด๋ธ”
๋ชจ๋“  ๋ธ”๋ก์ฒด์ธ ์ƒํ˜ธ์ž‘์šฉ ๋กœ๊ทธ:
Schema::create('blockchain_logs', function (Blueprint $table) {
    $table->id();
    $table->string('transaction_hash')->unique();
    $table->string('block_number')->nullable();
    $table->string('from_address');
    $table->string('to_address');
    $table->string('contract_address')->nullable();
    $table->string('event_type'); // donation, expenditure, contract_deploy
    $table->json('event_data');
    $table->decimal('gas_used', 20, 0)->nullable();
    $table->decimal('gas_price', 30, 0)->nullable();
    $table->timestamp('blockchain_timestamp');
    $table->timestamps();
    
    $table->index('transaction_hash');
    $table->index(['event_type', 'created_at']);
});
๐Ÿ’ก Pro Tip: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋Š” ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ์˜ '์บ์‹œ'๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฑฐ์•ผ. ์‹ค์ œ ์ง„์‹ค์˜ ์›์ฒœ(Source of Truth)์€ ๋ธ”๋ก์ฒด์ธ์— ์žˆ์ง€๋งŒ, ๋น ๋ฅธ ์กฐํšŒ๋ฅผ ์œ„ํ•ด DB์—๋„ ๋ณต์‚ฌ๋ณธ์„ ๋‘ฌ. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋ณผ ๋•Œ๋งˆ๋‹ค ๋ธ”๋ก์ฒด์ธ์„ ์ง์ ‘ ์กฐํšŒํ•  ํ•„์š”๊ฐ€ ์—†์–ด์„œ ํ›จ์”ฌ ๋น ๋ฅด๊ฑฐ๋“ ! โšก

๐Ÿ” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐœ๋ฐœ

์ž, ์ด์ œ ๋ธ”๋ก์ฒด์ธ์˜ ์‹ฌ์žฅ๋ถ€์ธ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ์†”๋ฆฌ๋””ํ‹ฐ(Solidity)๋กœ ์ž‘์„ฑํ•  ๊ฑด๋ฐ, ๊ฑฑ์ • ๋งˆ. ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ•ด์ค„๊ฒŒ. ๐Ÿ˜Š

๐Ÿ“ ๊ธฐ๋ถ€ ์บ ํŽ˜์ธ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์ปจํŠธ๋ž™ํŠธ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์€ ์ด๊ฑฐ์•ผ:

1. ๊ธฐ๋ถ€๊ธˆ ๋ฐ›๊ธฐ (Donation)
2. ๊ธฐ๋ถ€๊ธˆ ์ง‘ํ–‰ํ•˜๊ธฐ (Expenditure)
3. ํˆฌ๋ช…์„ฑ ๋ณด์žฅ (Transparency)
4. ํ™˜๋ถˆ ์ฒ˜๋ฆฌ (Refund - ์บ ํŽ˜์ธ ์‹คํŒจ ์‹œ)

์ „์ฒด ์ฝ”๋“œ๋ฅผ ๋ณด์ž:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract DonationCampaign {
    address public beneficiary;
    address public platformAdmin;
    uint256 public goalAmount;
    uint256 public currentAmount;
    uint256 public deadline;
    bool public isActive;
    bool public goalReached;
    
    uint256 public constant PLATFORM_FEE_PERCENT = 2; // 2% ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ
    
    struct Donation {
        address donor;
        uint256 amount;
        uint256 timestamp;
        string message;
    }
    
    struct Expenditure {
        uint256 amount;
        string category;
        string description;
        uint256 timestamp;
        bool executed;
    }
    
    Donation[] public donations;
    Expenditure[] public expenditures;
    
    mapping(address => uint256) public donorContributions;
    
    event DonationReceived(
        address indexed donor,
        uint256 amount,
        uint256 timestamp,
        string message
    );
    
    event ExpenditureExecuted(
        uint256 indexed expenditureId,
        uint256 amount,
        string category,
        uint256 timestamp
    );
    
    event GoalReached(uint256 totalAmount, uint256 timestamp);
    
    event RefundIssued(address indexed donor, uint256 amount);
    
    modifier onlyBeneficiary() {
        require(msg.sender == beneficiary, "Only beneficiary can call this");
        _;
    }
    
    modifier onlyAdmin() {
        require(msg.sender == platformAdmin, "Only admin can call this");
        _;
    }
    
    modifier campaignActive() {
        require(isActive, "Campaign is not active");
        require(block.timestamp < deadline, "Campaign has ended");
        _;
    }
    
    constructor(
        address _beneficiary,
        address _platformAdmin,
        uint256 _goalAmount,
        uint256 _durationInDays
    ) {
        require(_beneficiary != address(0), "Invalid beneficiary address");
        require(_goalAmount > 0, "Goal amount must be positive");
        
        beneficiary = _beneficiary;
        platformAdmin = _platformAdmin;
        goalAmount = _goalAmount;
        deadline = block.timestamp + (_durationInDays * 1 days);
        isActive = true;
        goalReached = false;
    }
    
    function donate(string memory _message) public payable campaignActive {
        require(msg.value > 0, "Donation amount must be positive");
        
        donations.push(Donation({
            donor: msg.sender,
            amount: msg.value,
            timestamp: block.timestamp,
            message: _message
        }));
        
        donorContributions[msg.sender] += msg.value;
        currentAmount += msg.value;
        
        emit DonationReceived(msg.sender, msg.value, block.timestamp, _message);
        
        if (currentAmount >= goalAmount && !goalReached) {
            goalReached = true;
            emit GoalReached(currentAmount, block.timestamp);
        }
    }
    
    function executeExpenditure(
        uint256 _amount,
        string memory _category,
        string memory _description
    ) public onlyBeneficiary {
        require(_amount > 0, "Amount must be positive");
        require(_amount <= currentAmount, "Insufficient funds");
        require(goalReached, "Goal not reached yet");
        
        uint256 platformFee = (_amount * PLATFORM_FEE_PERCENT) / 100;
        uint256 netAmount = _amount - platformFee;
        
        expenditures.push(Expenditure({
            amount: _amount,
            category: _category,
            description: _description,
            timestamp: block.timestamp,
            executed: true
        }));
        
        currentAmount -= _amount;
        
        // ์ˆ˜ํ˜œ์ž์—๊ฒŒ ์†ก๊ธˆ
        payable(beneficiary).transfer(netAmount);
        
        // ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ ์†ก๊ธˆ
        payable(platformAdmin).transfer(platformFee);
        
        emit ExpenditureExecuted(
            expenditures.length - 1,
            _amount,
            _category,
            block.timestamp
        );
    }
    
    function refund() public {
        require(!goalReached, "Goal was reached, no refunds");
        require(block.timestamp >= deadline, "Campaign still active");
        
        uint256 contribution = donorContributions[msg.sender];
        require(contribution > 0, "No contribution found");
        
        donorContributions[msg.sender] = 0;
        currentAmount -= contribution;
        
        payable(msg.sender).transfer(contribution);
        
        emit RefundIssued(msg.sender, contribution);
    }
    
    function getDonationsCount() public view returns (uint256) {
        return donations.length;
    }
    
    function getExpendituresCount() public view returns (uint256) {
        return expenditures.length;
    }
    
    function getDonation(uint256 _index) public view returns (
        address donor,
        uint256 amount,
        uint256 timestamp,
        string memory message
    ) {
        require(_index < donations.length, "Invalid index");
        Donation memory d = donations[_index];
        return (d.donor, d.amount, d.timestamp, d.message);
    }
    
    function getExpenditure(uint256 _index) public view returns (
        uint256 amount,
        string memory category,
        string memory description,
        uint256 timestamp,
        bool executed
    ) {
        require(_index < expenditures.length, "Invalid index");
        Expenditure memory e = expenditures[_index];
        return (e.amount, e.category, e.description, e.timestamp, e.executed);
    }
    
    function closeCampaign() public onlyAdmin {
        isActive = false;
    }
}

๐ŸŽ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ•ต์‹ฌ ๊ฐœ๋… ์„ค๋ช…

1. ์ƒํƒœ ๋ณ€์ˆ˜ (State Variables)
- ๋ธ”๋ก์ฒด์ธ์— ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅ๋˜๋Š” ๋ฐ์ดํ„ฐ
- `beneficiary`, `goalAmount`, `currentAmount` ๋“ฑ
- ๊ฐ€์Šค๋น„๊ฐ€ ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ด

2. ์ด๋ฒคํŠธ (Events)
- ๋ธ”๋ก์ฒด์ธ์— ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋Š” ๋ฐฉ๋ฒ•
- ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ง€ ๊ฐ€๋Šฅ
- `DonationReceived`, `ExpenditureExecuted` ๋“ฑ

3. ์ˆ˜์ •์ž (Modifiers)
- ํ•จ์ˆ˜ ์‹คํ–‰ ์ „ ์กฐ๊ฑด์„ ์ฒดํฌ
- `onlyBeneficiary`, `campaignActive` ๋“ฑ
- ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ๋ณด์•ˆ์„ ๋†’์—ฌ์ค˜

4. ๊ฐ€์‹œ์„ฑ (Visibility)
- `public`: ๋ˆ„๊ตฌ๋‚˜ ํ˜ธ์ถœ ๊ฐ€๋Šฅ
- `private`: ์ปจํŠธ๋ž™ํŠธ ๋‚ด๋ถ€์—์„œ๋งŒ
- `view`: ์ƒํƒœ๋ฅผ ์ฝ๊ธฐ๋งŒ ํ•จ (๊ฐ€์Šค๋น„ ๋ฌด๋ฃŒ)
- `pure`: ์ƒํƒœ๋ฅผ ์ฝ์ง€๋„ ์“ฐ์ง€๋„ ์•Š์Œ

๐Ÿ”— ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ”๋ก์ฒด์ธ ์—ฐ๋™ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ผ๋ผ๋ฒจ์—์„œ ์ด ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ์—ฌ๊ธฐ์„œ `web3.php` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ.

๐Ÿ“ฆ ํŒจํ‚ค์ง€ ์„ค์น˜

๋จผ์ € ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์„ค์น˜ํ•˜์ž:
composer require sc0vu/web3.php
composer require kornrunner/keccak

โš™๏ธ ํ™˜๊ฒฝ ์„ค์ •

`.env` ํŒŒ์ผ์— ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด:
# Blockchain Configuration
BLOCKCHAIN_NETWORK=sepolia
BLOCKCHAIN_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
BLOCKCHAIN_CHAIN_ID=11155111

# Platform Wallet
PLATFORM_WALLET_ADDRESS=0xYourPlatformWalletAddress
PLATFORM_WALLET_PRIVATE_KEY=YourPrivateKey

# Smart Contract
DONATION_CONTRACT_ADDRESS=0xYourDeployedContractAddress

# Gas Settings
GAS_LIMIT=300000
GAS_PRICE=20000000000
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ: ์ ˆ๋Œ€๋กœ ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๊ฑฐ๋‚˜ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ๋งˆ! ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ํ”„๋กœ๋•์…˜์—์„œ๋Š” AWS Secrets Manager๋‚˜ HashiCorp Vault ๊ฐ™์€ ๋ณด์•ˆ ์†”๋ฃจ์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ๐Ÿ”’

๐Ÿ—๏ธ ๋ธ”๋ก์ฒด์ธ ์„œ๋น„์Šค ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

`app/Services/BlockchainService.php` ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž:
<?php

namespace App\Services;

use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Illuminate\Support\Facades\Log;

class BlockchainService
{
    protected $web3;
    protected $contract;
    protected $contractAddress;
    protected $platformWalletAddress;
    protected $platformPrivateKey;
    
    public function __construct()
    {
        $this->web3 = new Web3(config('blockchain.rpc_url'));
        $this->contractAddress = config('blockchain.contract_address');
        $this->platformWalletAddress = config('blockchain.platform_wallet');
        $this->platformPrivateKey = config('blockchain.platform_private_key');
        
        // ABI ๋กœ๋“œ (Application Binary Interface)
        $abi = json_decode(file_get_contents(storage_path('contracts/DonationCampaign.json')), true);
        
        $this->contract = new Contract($this->web3->provider, $abi);
    }
    
    /**
     * ์ƒˆ๋กœ์šด ๊ธฐ๋ถ€ ์บ ํŽ˜์ธ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ
     */
    public function deployCampaign(
        string $beneficiaryAddress,
        float $goalAmount,
        int $durationInDays
    ): array {
        try {
            $goalAmountWei = Utils::toWei((string)$goalAmount, 'ether');
            
            // ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ
            $bytecode = file_get_contents(storage_path('contracts/DonationCampaign.bin'));
            
            $transaction = [
                'from' => $this->platformWalletAddress,
                'data' => $bytecode . $this->encodeConstructorParams(
                    $beneficiaryAddress,
                    $this->platformWalletAddress,
                    $goalAmountWei,
                    $durationInDays
                ),
                'gas' => Utils::toHex(config('blockchain.gas_limit'), true),
                'gasPrice' => Utils::toHex(config('blockchain.gas_price'), true),
            ];
            
            // ํŠธ๋žœ์žญ์…˜ ์„œ๋ช… ๋ฐ ์ „์†ก
            $signedTx = $this->signTransaction($transaction);
            $txHash = $this->sendRawTransaction($signedTx);
            
            // ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ๋Œ€๊ธฐ
            $receipt = $this->waitForTransactionReceipt($txHash);
            
            return [
                'success' => true,
                'contract_address' => $receipt['contractAddress'],
                'transaction_hash' => $txHash,
                'block_number' => hexdec($receipt['blockNumber']),
            ];
            
        } catch (\Exception $e) {
            Log::error('Contract deployment failed: ' . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    /**
     * ๊ธฐ๋ถ€ ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ
     */
    public function processDonation(
        string $contractAddress,
        string $donorAddress,
        float $amount,
        string $message
    ): array {
        try {
            $amountWei = Utils::toWei((string)$amount, 'ether');
            
            // donate ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋ฐ์ดํ„ฐ ์ธ์ฝ”๋”ฉ
            $functionData = $this->contract->at($contractAddress)
                ->getData('donate', $message);
            
            $transaction = [
                'from' => $donorAddress,
                'to' => $contractAddress,
                'value' => Utils::toHex($amountWei, true),
                'data' => $functionData,
                'gas' => Utils::toHex(config('blockchain.gas_limit'), true),
                'gasPrice' => Utils::toHex(config('blockchain.gas_price'), true),
            ];
            
            return [
                'success' => true,
                'transaction' => $transaction,
            ];
            
        } catch (\Exception $e) {
            Log::error('Donation processing failed: ' . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    /**
     * ๊ธฐ๋ถ€๊ธˆ ์ง‘ํ–‰
     */
    public function executeExpenditure(
        string $contractAddress,
        float $amount,
        string $category,
        string $description
    ): array {
        try {
            $amountWei = Utils::toWei((string)$amount, 'ether');
            
            $functionData = $this->contract->at($contractAddress)
                ->getData('executeExpenditure', $amountWei, $category, $description);
            
            $transaction = [
                'from' => $this->platformWalletAddress,
                'to' => $contractAddress,
                'data' => $functionData,
                'gas' => Utils::toHex(config('blockchain.gas_limit'), true),
                'gasPrice' => Utils::toHex(config('blockchain.gas_price'), true),
            ];
            
            $signedTx = $this->signTransaction($transaction);
            $txHash = $this->sendRawTransaction($signedTx);
            
            $receipt = $this->waitForTransactionReceipt($txHash);
            
            return [
                'success' => true,
                'transaction_hash' => $txHash,
                'block_number' => hexdec($receipt['blockNumber']),
                'gas_used' => hexdec($receipt['gasUsed']),
            ];
            
        } catch (\Exception $e) {
            Log::error('Expenditure execution failed: ' . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    /**
     * ์บ ํŽ˜์ธ ์ •๋ณด ์กฐํšŒ
     */
    public function getCampaignInfo(string $contractAddress): array
    {
        try {
            $contract = $this->contract->at($contractAddress);
            
            $info = [];
            
            // ๊ฐ ์ƒํƒœ ๋ณ€์ˆ˜ ์ฝ๊ธฐ
            $contract->call('beneficiary', function ($err, $result) use (&$info) {
                if (!$err) $info['beneficiary'] = $result[0];
            });
            
            $contract->call('goalAmount', function ($err, $result) use (&$info) {
                if (!$err) $info['goal_amount'] = Utils::fromWei($result[0], 'ether');
            });
            
            $contract->call('currentAmount', function ($err, $result) use (&$info) {
                if (!$err) $info['current_amount'] = Utils::fromWei($result[0], 'ether');
            });
            
            $contract->call('goalReached', function ($err, $result) use (&$info) {
                if (!$err) $info['goal_reached'] = $result[0];
            });
            
            $contract->call('getDonationsCount', function ($err, $result) use (&$info) {
                if (!$err) $info['donations_count'] = hexdec($result[0]);
            });
            
            return [
                'success' => true,
                'data' => $info,
            ];
            
        } catch (\Exception $e) {
            Log::error('Failed to get campaign info: ' . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    /**
     * ์ด๋ฒคํŠธ ๋กœ๊ทธ ์กฐํšŒ
     */
    public function getEventLogs(
        string $contractAddress,
        string $eventName,
        int $fromBlock = 0,
        int $toBlock = null
    ): array {
        try {
            $toBlock = $toBlock ?? 'latest';
            
            $filter = [
                'fromBlock' => Utils::toHex($fromBlock, true),
                'toBlock' => $toBlock === 'latest' ? 'latest' : Utils::toHex($toBlock, true),
                'address' => $contractAddress,
            ];
            
            $logs = [];
            
            $this->web3->eth->getLogs($filter, function ($err, $result) use (&$logs) {
                if (!$err) {
                    $logs = $result;
                }
            });
            
            return [
                'success' => true,
                'logs' => $logs,
            ];
            
        } catch (\Exception $e) {
            Log::error('Failed to get event logs: ' . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    /**
     * ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…
     */
    protected function signTransaction(array $transaction): string
    {
        // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” eth-php ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
        // ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋žตํ™”
        return '0xSignedTransactionData';
    }
    
    /**
     * Raw ํŠธ๋žœ์žญ์…˜ ์ „์†ก
     */
    protected function sendRawTransaction(string $signedTx): string
    {
        $txHash = '';
        
        $this->web3->eth->sendRawTransaction($signedTx, function ($err, $result) use (&$txHash) {
            if (!$err) {
                $txHash = $result;
            }
        });
        
        return $txHash;
    }
    
    /**
     * ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ๋Œ€๊ธฐ
     */
    protected function waitForTransactionReceipt(string $txHash, int $maxAttempts = 30): array
    {
        $attempts = 0;
        $receipt = null;
        
        while ($attempts < $maxAttempts) {
            $this->web3->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
                if (!$err && $result) {
                    $receipt = $result;
                }
            });
            
            if ($receipt) {
                return $receipt;
            }
            
            sleep(2);
            $attempts++;
        }
        
        throw new \Exception('Transaction receipt not found after ' . $maxAttempts . ' attempts');
    }
    
    /**
     * ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฏธํ„ฐ ์ธ์ฝ”๋”ฉ
     */
    protected function encodeConstructorParams(...$params): string
    {
        // ABI ์ธ์ฝ”๋”ฉ ๋กœ์ง
        // ์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๊ตฌํ˜„ ํ•„์š”
        return '';
    }
}
๐Ÿ’ก ์‹ค์ „ ํŒ: ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ ค. ํŠนํžˆ ์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์—์„œ๋Š” ๋ช‡ ๋ถ„์”ฉ ๊ฑธ๋ฆด ์ˆ˜๋„ ์žˆ์–ด. ๊ทธ๋ž˜์„œ ๋ฐ˜๋“œ์‹œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์•ผ ํ•ด! ๋ผ๋ผ๋ฒจ์˜ Queue ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜๋ฉด ์™„๋ฒฝํ•˜์ง€. ๐Ÿ‘Œ

๐ŸŽจ ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„

์ด์ œ ์‹ค์ œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. RESTful API ์„ค๊ณ„ ์›์น™์„ ๋”ฐ๋ฅผ ๊ฑฐ์•ผ.

๐Ÿ“‹ CampaignController

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\User;
use App\Services\BlockchainService;
use App\Jobs\DeployCampaignContract;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;

class CampaignController extends Controller
{
    protected $blockchainService;
    
    public function __construct(BlockchainService $blockchainService)
    {
        $this->blockchainService = $blockchainService;
    }
    
    /**
     * ์บ ํŽ˜์ธ ๋ชฉ๋ก ์กฐํšŒ
     */
    public function index(Request $request)
    {
        $query = Campaign::with(['beneficiary', 'donations'])
            ->where('status', 'active');
        
        // ํ•„ํ„ฐ๋ง
        if ($request->has('category')) {
            $query->where('category', $request->category);
        }
        
        if ($request->has('search')) {
            $query->where(function($q) use ($request) {
                $q->where('title', 'like', '%' . $request->search . '%')
                  ->orWhere('description', 'like', '%' . $request->search . '%');
            });
        }
        
        // ์ •๋ ฌ
        $sortBy = $request->get('sort_by', 'created_at');
        $sortOrder = $request->get('sort_order', 'desc');
        $query->orderBy($sortBy, $sortOrder);
        
        // ํŽ˜์ด์ง€๋„ค์ด์…˜
        $campaigns = $query->paginate(12);
        
        // ๊ฐ ์บ ํŽ˜์ธ์˜ ๋ธ”๋ก์ฒด์ธ ์ •๋ณด ์ถ”๊ฐ€
        $campaigns->getCollection()->transform(function ($campaign) {
            if ($campaign->smart_contract_address) {
                $blockchainInfo = $this->blockchainService
                    ->getCampaignInfo($campaign->smart_contract_address);
                
                if ($blockchainInfo['success']) {
                    $campaign->blockchain_data = $blockchainInfo['data'];
                }
            }
            return $campaign;
        });
        
        return response()->json([
            'success' => true,
            'data' => $campaigns,
        ]);
    }
    
    /**
     * ์บ ํŽ˜์ธ ์ƒ์„ธ ์กฐํšŒ
     */
    public function show($id)
    {
        $campaign = Campaign::with([
            'beneficiary',
            'donations.donor',
            'expenditures'
        ])->findOrFail($id);
        
        // ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ
        if ($campaign->smart_contract_address) {
            $blockchainInfo = $this->blockchainService
                ->getCampaignInfo($campaign->smart_contract_address);
            
            if ($blockchainInfo['success']) {
                $campaign->blockchain_data = $blockchainInfo['data'];
            }
            
            // ์ตœ๊ทผ ์ด๋ฒคํŠธ ๋กœ๊ทธ ์กฐํšŒ
            $logs = $this->blockchainService->getEventLogs(
                $campaign->smart_contract_address,
                'DonationReceived',
                $campaign->blockchain_start_block ?? 0
            );
            
            if ($logs['success']) {
                $campaign->recent_blockchain_events = $logs['logs'];
            }
        }
        
        return response()->json([
            'success' => true,
            'data' => $campaign,
        ]);
    }
    
    /**
     * ์ƒˆ ์บ ํŽ˜์ธ ์ƒ์„ฑ
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => 'required|string|max:255',
            'description' => 'required|string',
            'goal_amount' => 'required|numeric|min:0',
            'duration_days' => 'required|integer|min:1|max:365',
            'category' => 'required|string',
            'beneficiary_wallet' => 'required|string|regex:/^0x[a-fA-F0-9]{40}$/',
        ]);
        
        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors(),
            ], 422);
        }
        
        DB::beginTransaction();
        
        try {
            // ์ˆ˜ํ˜œ์ž ํ™•์ธ ๋˜๋Š” ์ƒ์„ฑ
            $beneficiary = User::where('wallet_address', $request->beneficiary_wallet)
                ->first();
            
            if (!$beneficiary) {
                return response()->json([
                    'success' => false,
                    'message' => 'Beneficiary wallet not registered',
                ], 404);
            }
            
            // ์บ ํŽ˜์ธ ์ƒ์„ฑ
            $campaign = Campaign::create([
                'beneficiary_id' => $beneficiary->id,
                'title' => $request->title,
                'description' => $request->description,
                'goal_amount' => $request->goal_amount,
                'start_date' => now(),
                'end_date' => now()->addDays($request->duration_days),
                'status' => 'draft',
                'category' => $request->category,
                'metadata' => $request->metadata ?? [],
            ]);
            
            // ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ ์ž‘์—… ํ์— ์ถ”๊ฐ€
            DeployCampaignContract::dispatch($campaign);
            
            DB::commit();
            
            return response()->json([
                'success' => true,
                'message' => 'Campaign created successfully. Smart contract deployment in progress.',
                'data' => $campaign,
            ], 201);
            
        } catch (\Exception $e) {
            DB::rollBack();
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to create campaign',
                'error' => $e->getMessage(),
            ], 500);
        }
    }
    
    /**
     * ์บ ํŽ˜์ธ ์ˆ˜์ •
     */
    public function update(Request $request, $id)
    {
        $campaign = Campaign::findOrFail($id);
        
        // ๊ถŒํ•œ ์ฒดํฌ
        if ($campaign->beneficiary_id !== auth()->id() && !auth()->user()->isAdmin()) {
            return response()->json([
                'success' => false,
                'message' => 'Unauthorized',
            ], 403);
        }
        
        // ํ™œ์„ฑํ™”๋œ ์บ ํŽ˜์ธ์€ ์ผ๋ถ€ ํ•„๋“œ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ
        if ($campaign->status === 'active') {
            $allowedFields = ['description', 'metadata'];
            $updateData = $request->only($allowedFields);
        } else {
            $updateData = $request->all();
        }
        
        $campaign->update($updateData);
        
        return response()->json([
            'success' => true,
            'message' => 'Campaign updated successfully',
            'data' => $campaign,
        ]);
    }
    
    /**
     * ์บ ํŽ˜์ธ ํ™œ์„ฑํ™”
     */
    public function activate($id)
    {
        $campaign = Campaign::findOrFail($id);
        
        if (!$campaign->smart_contract_address) {
            return response()->json([
                'success' => false,
                'message' => 'Smart contract not deployed yet',
            ], 400);
        }
        
        $campaign->update(['status' => 'active']);
        
        return response()->json([
            'success' => true,
            'message' => 'Campaign activated successfully',
            'data' => $campaign,
        ]);
    }
    
    /**
     * ์บ ํŽ˜์ธ ํ†ต๊ณ„
     */
    public function statistics($id)
    {
        $campaign = Campaign::with(['donations', 'expenditures'])->findOrFail($id);
        
        $stats = [
            'total_donations' => $campaign->donations->count(),
            'total_amount' => $campaign->donations->sum('amount'),
            'unique_donors' => $campaign->donations->unique('donor_id')->count(),
            'total_expenditures' => $campaign->expenditures->count(),
            'total_spent' => $campaign->expenditures->sum('amount'),
            'remaining_amount' => $campaign->current_amount,
            'goal_percentage' => ($campaign->current_amount / $campaign->goal_amount) * 100,
            'days_remaining' => now()->diffInDays($campaign->end_date, false),
            'average_donation' => $campaign->donations->avg('amount'),
        ];
        
        // ์ผ๋ณ„ ๊ธฐ๋ถ€ ์ถ”์ด
        $dailyDonations = $campaign->donations()
            ->selectRaw('DATE(created_at) as date, COUNT(*) as count, SUM(amount) as total')
            ->groupBy('date')
            ->orderBy('date')
            ->get();
        
        $stats['daily_trend'] = $dailyDonations;
        
        // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง‘ํ–‰ ๋‚ด์—ญ
        $expendituresByCategory = $campaign->expenditures()
            ->selectRaw('category, COUNT(*) as count, SUM(amount) as total')
            ->groupBy('category')
            ->get();
        
        $stats['expenditures_by_category'] = $expendituresByCategory;
        
        return response()->json([
            'success' => true,
            'data' => $stats,
        ]);
    }
}

๐Ÿ’ธ DonationController

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\Donation;
use App\Services\BlockchainService;
use App\Jobs\ProcessDonationTransaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;

class DonationController extends Controller
{
    protected $blockchainService;
    
    public function __construct(BlockchainService $blockchainService)
    {
        $this->blockchainService = $blockchainService;
    }
    
    /**
     * ๊ธฐ๋ถ€ํ•˜๊ธฐ
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'campaign_id' => 'required|exists:campaigns,id',
            'amount' => 'required|numeric|min:0.001',
            'message' => 'nullable|string|max:500',
            'is_anonymous' => 'boolean',
            'donor_wallet' => 'required|string|regex:/^0x[a-fA-F0-9]{40}$/',
        ]);
        
        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors(),
            ], 422);
        }
        
        $campaign = Campaign::findOrFail($request->campaign_id);
        
        // ์บ ํŽ˜์ธ ์ƒํƒœ ํ™•์ธ
        if ($campaign->status !== 'active') {
            return response()->json([
                'success' => false,
                'message' => 'Campaign is not active',
            ], 400);
        }
        
        if (now()->gt($campaign->end_date)) {
            return response()->json([
                'success' => false,
                'message' => 'Campaign has ended',
            ], 400);
        }
        
        DB::beginTransaction();
        
        try {
            // ๊ธฐ๋ถ€์ž ํ™•์ธ
            $donor = auth()->user();
            
            // ๊ธฐ๋ถ€ ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ
            $donation = Donation::create([
                'campaign_id' => $campaign->id,
                'donor_id' => $donor->id,
                'amount' => $request->amount,
                'currency' => 'ETH',
                'message' => $request->message,
                'is_anonymous' => $request->is_anonymous ?? false,
                'status' => 'pending',
            ]);
            
            // ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ์ค€๋น„
            $txData = $this->blockchainService->processDonation(
                $campaign->smart_contract_address,
                $request->donor_wallet,
                $request->amount,
                $request->message ?? ''
            );
            
            if (!$txData['success']) {
                throw new \Exception('Failed to prepare blockchain transaction');
            }
            
            // ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ž‘์—… ํ์— ์ถ”๊ฐ€
            ProcessDonationTransaction::dispatch($donation, $txData['transaction']);
            
            DB::commit();
            
            return response()->json([
                'success' => true,
                'message' => 'Donation initiated. Please confirm the transaction in your wallet.',
                'data' => [
                    'donation' => $donation,
                    'transaction' => $txData['transaction'],
                ],
            ], 201);
            
        } catch (\Exception $e) {
            DB::rollBack();
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to process donation',
                'error' => $e->getMessage(),
            ], 500);
        }
    }
    
    /**
     * ๊ธฐ๋ถ€ ๋‚ด์—ญ ์กฐํšŒ
     */
    public function index(Request $request)
    {
        $query = Donation::with(['campaign', 'donor']);
        
        // ํ•„ํ„ฐ๋ง
        if ($request->has('campaign_id')) {
            $query->where('campaign_id', $request->campaign_id);
        }
        
        if ($request->has('donor_id')) {
            $query->where('donor_id', $request->donor_id);
        }
        
        if ($request->has('status')) {
            $query->where('status', $request->status);
        }
        
        // ์ต๋ช… ๊ธฐ๋ถ€๋Š” ๊ธฐ๋ถ€์ž ์ •๋ณด ์ˆจ๊น€
        $donations = $query->latest()->paginate(20);
        
        $donations->getCollection()->transform(function ($donation) {
            if ($donation->is_anonymous && auth()->id() !== $donation->donor_id) {
                $donation->donor = null;
                $donation->donor_id = null;
            }
            return $donation;
        });
        
        return response()->json([
            'success' => true,
            'data' => $donations,
        ]);
    }
    
    /**
     * ๊ธฐ๋ถ€ ์ƒ์„ธ ์กฐํšŒ
     */
    public function show($id)
    {
        $donation = Donation::with(['campaign', 'donor'])->findOrFail($id);
        
        // ์ต๋ช… ๊ธฐ๋ถ€ ์ฒ˜๋ฆฌ
        if ($donation->is_anonymous && auth()->id() !== $donation->donor_id) {
            $donation->donor = null;
        }
        
        // ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ
        if ($donation->transaction_hash) {
            // ์‹ค์ œ๋กœ๋Š” ๋ธ”๋ก์ฒด์ธ์—์„œ ํŠธ๋žœ์žญ์…˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•จ
            $donation->blockchain_confirmation = [
                'confirmed' => $donation->status === 'confirmed',
                'block_number' => $donation->block_number,
                'confirmations' => 12, // ์˜ˆ์‹œ
            ];
        }
        
        return response()->json([
            'success' => true,
            'data' => $donation,
        ]);
    }
    
    /**
     * ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (์›นํ›…์šฉ)
     */
    public function updateTransactionStatus(Request $request, $id)
    {
        $donation = Donation::findOrFail($id);
        
        $validator = Validator::make($request->all(), [
            'transaction_hash' => 'required|string',
            'block_number' => 'nullable|string',
            'status' => 'required|in:confirmed,failed',
        ]);
        
        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors(),
            ], 422);
        }
        
        DB::beginTransaction();
        
        try {
            $donation->update([
                'transaction_hash' => $request->transaction_hash,
                'block_number' => $request->block_number,
                'status' => $request->status,
                'blockchain_confirmed_at' => $request->status === 'confirmed' ? now() : null,
            ]);
            
            // ์บ ํŽ˜์ธ ํ˜„์žฌ ๊ธˆ์•ก ์—…๋ฐ์ดํŠธ
            if ($request->status === 'confirmed') {
                $campaign = $donation->campaign;
                $campaign->increment('current_amount', $donation->amount);
            }
            
            DB::commit();
            
            return response()->json([
                'success' => true,
                'message' => 'Transaction status updated',
                'data' => $donation,
            ]);
            
        } catch (\Exception $e) {
            DB::rollBack();
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to update transaction status',
                'error' => $e->getMessage(),
            ], 500);
        }
    }
}

โšก ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ (Queue Jobs)

๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋‹ˆ๊นŒ, ๋ฐ˜๋“œ์‹œ ํ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด. ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋˜๊ฑฐ๋“ ! ๐Ÿ˜ค

๐Ÿš€ DeployCampaignContract Job

<?php

namespace App\Jobs;

use App\Models\Campaign;
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;

class DeployCampaignContract implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public $campaign;
    public $tries = 3;
    public $timeout = 300;
    
    public function __construct(Campaign $campaign)
    {
        $this->campaign = $campaign;
    }
    
    public function handle(BlockchainService $blockchainService)
    {
        Log::info('Deploying smart contract for campaign: ' . $this->campaign->id);
        
        try {
            $beneficiary = $this->campaign->beneficiary;
            
            $result = $blockchainService->deployCampaign(
                $beneficiary->wallet_address,
                $this->campaign->goal_amount,
                now()->diffInDays($this->campaign->end_date)
            );
            
            if ($result['success']) {
                $this->campaign->update([
                    'smart_contract_address' => $result['contract_address'],
                    'deployment_transaction_hash' => $result['transaction_hash'],
                    'blockchain_start_block' => $result['block_number'],
                    'status' => 'active',
                ]);
                
                Log::info('Smart contract deployed successfully', [
                    'campaign_id' => $this->campaign->id,
                    'contract_address' => $result['contract_address'],
                ]);
                
                // ๋ฐฐํฌ ์„ฑ๊ณต ์•Œ๋ฆผ ๋ฐœ์†ก
                // event(new CampaignContractDeployed($this->campaign));
                
            } else {
                throw new \Exception($result['error']);
            }
            
        } catch (\Exception $e) {
            Log::error('Failed to deploy smart contract', [
                'campaign_id' => $this->campaign->id,
                'error' => $e->getMessage(),
            ]);
            
            $this->campaign->update([
                'status' => 'failed',
                'metadata' => array_merge($this->campaign->metadata ?? [], [
                    'deployment_error' => $e->getMessage(),
                ]),
            ]);
            
            throw $e;
        }
    }
    
    public function failed(\Throwable $exception)
    {
        Log::error('Campaign contract deployment job failed permanently', [
            'campaign_id' => $this->campaign->id,
            'error' => $exception->getMessage(),
        ]);
        
        // ๊ด€๋ฆฌ์ž์—๊ฒŒ ์•Œ๋ฆผ
        // event(new CampaignDeploymentFailed($this->campaign, $exception));
    }
}

๐Ÿ’ณ ProcessDonationTransaction Job

<?php

namespace App\Jobs;

use App\Models\Donation;
use App\Models\BlockchainLog;
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;

class ProcessDonationTransaction implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public $donation;
    public $transactionData;
    public $tries = 3;
    public $timeout = 300;
    
    public function __construct(Donation $donation, array $transactionData)
    {
        $this->donation = $donation;
        $this->transactionData = $transactionData;
    }
    
    public function handle(BlockchainService $blockchainService)
    {
        Log::info('Processing donation transaction', [
            'donation_id' => $this->donation->id,
        ]);
        
        try {
            // ์‹ค์ œ๋กœ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋“ฑ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์Šน์ธํ•ด์•ผ ํ•จ
            // ์—ฌ๊ธฐ์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๋ฅผ ๋ฐ›์•˜๋‹ค๊ณ  ๊ฐ€์ •
            
            // ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘
            $this->monitorTransaction($blockchainService);
            
        } catch (\Exception $e) {
            Log::error('Failed to process donation transaction', [
                'donation_id' => $this->donation->id,
                'error' => $e->getMessage(),
            ]);
            
            $this->donation->update(['status' => 'failed']);
            
            throw $e;
        }
    }
    
    protected function monitorTransaction(BlockchainService $blockchainService)
    {
        // ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๋กœ ์ƒํƒœ ํ™•์ธ
        $maxAttempts = 30;
        $attempts = 0;
        
        while ($attempts < $maxAttempts) {
            $receipt = $blockchainService->getTransactionReceipt(
                $this->donation->transaction_hash
            );
            
            if ($receipt) {
                $this->handleTransactionConfirmed($receipt);
                return;
            }
            
            sleep(5);
            $attempts++;
        }
        
        throw new \Exception('Transaction not confirmed within timeout period');
    }
    
    protected function handleTransactionConfirmed(array $receipt)
    {
        $this->donation->update([
            'block_number' => hexdec($receipt['blockNumber']),
            'status' => 'confirmed',
            'blockchain_confirmed_at' => now(),
        ]);
        
        // ๋ธ”๋ก์ฒด์ธ ๋กœ๊ทธ ์ €์žฅ
        BlockchainLog::create([
            'transaction_hash' => $this->donation->transaction_hash,
            'block_number' => hexdec($receipt['blockNumber']),
            'from_address' => $receipt['from'],
            'to_address' => $receipt['to'],
            'contract_address' => $receipt['contractAddress'] ?? null,
            'event_type' => 'donation',
            'event_data' => [
                'donation_id' => $this->donation->id,
                'amount' => $this->donation->amount,
                'campaign_id' => $this->donation->campaign_id,
            ],
            'gas_used' => hexdec($receipt['gasUsed']),
            'blockchain_timestamp' => now(),
        ]);
        
        // ์บ ํŽ˜์ธ ๊ธˆ์•ก ์—…๋ฐ์ดํŠธ
        $this->donation->campaign->increment('current_amount', $this->donation->amount);
        
        Log::info('Donation transaction confirmed', [
            'donation_id' => $this->donation->id,
            'transaction_hash' => $this->donation->transaction_hash,
        ]);
        
        // ๊ธฐ๋ถ€ ์™„๋ฃŒ ์•Œ๋ฆผ
        // event(new DonationConfirmed($this->donation));
    }
}

๐Ÿ“Š ํˆฌ๋ช…์„ฑ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„! ๋ฐ”๋กœ ํˆฌ๋ช…์„ฑ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋Œ€์‹œ๋ณด๋“œ์•ผ. ์ด๊ฒŒ ์—†์œผ๋ฉด ๋ธ”๋ก์ฒด์ธ์„ ์“ฐ๋Š” ์˜๋ฏธ๊ฐ€ ์—†์ž–์•„? ๐Ÿ˜…

๐ŸŽจ Vue.js ์ปดํฌ๋„ŒํŠธ - TransparencyDashboard

<template>
  <div class="transparency-dashboard">
    <div class="dashboard-header">
      <h2>๐Ÿ’Ž ํˆฌ๋ช…์„ฑ ๋Œ€์‹œ๋ณด๋“œ</h2>
      <p>๋ชจ๋“  ๊ฑฐ๋ž˜ ๋‚ด์—ญ์ด ๋ธ”๋ก์ฒด์ธ์— ์˜๊ตฌ์ ์œผ๋กœ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค</p>
    </div>
    
    <!-- ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ -->
    <div class="stats-grid">
      <div class="stat-card">
        <div class="stat-icon">๐Ÿ’ฐ</div>
        <div class="stat-value">{{ formatCurrency(campaign.current_amount) }}</div>
        <div class="stat-label">์ด ๊ธฐ๋ถ€๊ธˆ</div>
        <div class="stat-blockchain">
          <span class="blockchain-badge">๋ธ”๋ก์ฒด์ธ ๊ฒ€์ฆ๋จ</span>
        </div>
      </div>
      
      <div class="stat-card">
        <div class="stat-icon">๐Ÿ‘ฅ</div>
        <div class="stat-value">{{ campaign.donors_count }}</div>
        <div class="stat-label">๊ธฐ๋ถ€์ž ์ˆ˜</div>
      </div>
      
      <div class="stat-card">
        <div class="stat-icon">๐Ÿ“ค</div>
        <div class="stat-value">{{ formatCurrency(campaign.total_spent) }}</div>
        <div class="stat-label">์ง‘ํ–‰ ๊ธˆ์•ก</div>
        <div class="stat-blockchain">
          <span class="blockchain-badge">๋ธ”๋ก์ฒด์ธ ๊ฒ€์ฆ๋จ</span>
        </div>
      </div>
      
      <div class="stat-card">
        <div class="stat-icon">๐Ÿ’ต</div>
        <div class="stat-value">{{ formatCurrency(campaign.remaining_amount) }}</div>
        <div class="stat-label">์ž”์—ฌ ๊ธˆ์•ก</div>
      </div>
    </div>
    
    <!-- ์ง„ํ–‰๋ฅ  -->
    <div class="progress-section">
      <div class="progress-header">
        <span>๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ </span>
        <span class="progress-percentage">{{ goalPercentage }}%</span>
      </div>
      <div class="progress-bar">
        <div class="progress-fill" :style="{ width: goalPercentage + '%' }"></div>
      </div>
      <div class="progress-info">
        <span>ํ˜„์žฌ: {{ formatCurrency(campaign.current_amount) }}</span>
        <span>๋ชฉํ‘œ: {{ formatCurrency(campaign.goal_amount) }}</span>
      </div>
    </div>
    
    <!-- ํƒญ ๋ฉ”๋‰ด -->
    <div class="tabs">
      <button 
        v-for="tab in tabs" 
        :key="tab.id"
        :class="['tab-button', { active: activeTab === tab.id }]"
        @click="activeTab = tab.id"
      >
        {{ tab.icon }} {{ tab.label }}
      </button>
    </div>
    
    <!-- ๊ธฐ๋ถ€ ๋‚ด์—ญ -->
    <div v-show="activeTab === 'donations'" class="tab-content">
      <h3>๐Ÿ’ ๊ธฐ๋ถ€ ๋‚ด์—ญ</h3>
      <div class="transaction-list">
        <div 
          v-for="donation in donations" 
          :key="donation.id"
          class="transaction-item"
        >
          <div class="transaction-icon">๐Ÿ’ฐ</div>
          <div class="transaction-details">
            <div class="transaction-header">
              <span class="donor-name">
                {{ donation.is_anonymous ? '์ต๋ช…์˜ ๊ธฐ๋ถ€์ž' : donation.donor.name }}
              </span>
              <span class="transaction-amount">
                {{ formatCurrency(donation.amount) }} ETH
              </span>
            </div>
            <div class="transaction-meta">
              <span class="transaction-date">
                {{ formatDate(donation.created_at) }}
              </span>
              <a 
                :href="getEtherscanUrl(donation.transaction_hash)" 
                target="_blank"
                class="blockchain-link"
              >
                ๐Ÿ”— ๋ธ”๋ก์ฒด์ธ์—์„œ ํ™•์ธ
              </a>
            </div>
            <div v-if="donation.message" class="transaction-message">
              "{{ donation.message }}"
            </div>
          </div>
          <div class="transaction-status">
            <span :class="['status-badge', donation.status]">
              {{ getStatusText(donation.status) }}
            </span>
          </div>
        </div>
      </div>
      
      <button v-if="hasMoreDonations" @click="loadMoreDonations" class="load-more">
        ๋” ๋ณด๊ธฐ
      </button>
    </div>
    
    <!-- ์ง‘ํ–‰ ๋‚ด์—ญ -->
    <div v-show="activeTab === 'expenditures'" class="tab-content">
      <h3>๐Ÿ“ค ์ง‘ํ–‰ ๋‚ด์—ญ</h3>
      <div class="transaction-list">
        <div 
          v-for="expenditure in expenditures" 
          :key="expenditure.id"
          class="transaction-item expenditure"
        >
          <div class="transaction-icon">๐Ÿ“ค</div>
          <div class="transaction-details">
            <div class="transaction-header">
              <span class="category-badge">{{ expenditure.category }}</span>
              <span class="transaction-amount">
                -{{ formatCurrency(expenditure.amount) }} ETH
              </span>
            </div>
            <div class="transaction-description">
              {{ expenditure.description }}
            </div>
            <div class="transaction-meta">
              <span class="transaction-date">
                {{ formatDate(expenditure.executed_at) }}
              </span>
              <a 
                :href="getEtherscanUrl(expenditure.transaction_hash)" 
                target="_blank"
                class="blockchain-link"
              >
                ๐Ÿ”— ๋ธ”๋ก์ฒด์ธ์—์„œ ํ™•์ธ
              </a>
              <a 
                v-if="expenditure.receipt_url"
                :href="expenditure.receipt_url" 
                target="_blank"
                class="receipt-link"
              >
                ๐Ÿ“„ ์˜์ˆ˜์ฆ ๋ณด๊ธฐ
              </a>
            </div>
          </div>
          <div class="transaction-status">
            <span :class="['status-badge', expenditure.status]">
              {{ getStatusText(expenditure.status) }}
            </span>
          </div>
        </div>
      </div>
    </div>
    
    <!-- ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ -->
    <div v-show="activeTab === 'blockchain'" class="tab-content">
      <h3>โ›“๏ธ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ</h3>
      <div class="blockchain-events">
        <div 
          v-for="event in blockchainEvents" 
          :key="event.id"
          class="event-item"
        >
          <div class="event-type">{{ event.event_type }}</div>
          <div class="event-details">
            <div class="event-data">{{ formatEventData(event.event_data) }}</div>
            <div class="event-meta">
              <span>๋ธ”๋ก: {{ event.block_number }}</span>
              <span>๊ฐ€์Šค: {{ event.gas_used }}</span>
              <span>{{ formatDate(event.blockchain_timestamp) }}</span>
            </div>
          </div>
          <a 
            :href="getEtherscanUrl(event.transaction_hash)" 
            target="_blank"
            class="event-link"
          >
            ์ƒ์„ธ๋ณด๊ธฐ โ†’
          </a>
        </div>
      </div>
    </div>
    
    <!-- ํ†ต๊ณ„ ์ฐจํŠธ -->
    <div v-show="activeTab === 'statistics'" class="tab-content">
      <h3>๐Ÿ“ˆ ํ†ต๊ณ„ ๋ถ„์„</h3>
      
      <div class="chart-container">
        <h4>์ผ๋ณ„ ๊ธฐ๋ถ€ ์ถ”์ด</h4>
        <canvas ref="donationTrendChart"></canvas>
      </div>
      
      <div class="chart-container">
        <h4>์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง‘ํ–‰ ๋‚ด์—ญ</h4>
        <canvas ref="expenditureCategoryChart"></canvas>
      </div>
      
      <div class="stats-table">
        <h4>์ƒ์„ธ ํ†ต๊ณ„</h4>
        <table>
          <tr>
            <td>ํ‰๊ท  ๊ธฐ๋ถ€ ๊ธˆ์•ก</td>
            <td>{{ formatCurrency(statistics.average_donation) }} ETH</td>
          </tr>
          <tr>
            <td>์ตœ๋Œ€ ๊ธฐ๋ถ€ ๊ธˆ์•ก</td>
            <td>{{ formatCurrency(statistics.max_donation) }} ETH</td>
          </tr>
          <tr>
            <td>์ตœ์†Œ ๊ธฐ๋ถ€ ๊ธˆ์•ก</td>
            <td>{{ formatCurrency(statistics.min_donation) }} ETH</td>
          </tr>
          <tr>
            <td>์žฌ๊ธฐ๋ถ€ ๋น„์œจ</td>
            <td>{{ statistics.repeat_donor_rate }}%</td>
          </tr>
        </table>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue';
import { Chart } from 'chart.js/auto';
import axios from 'axios';

export default {
  name: 'TransparencyDashboard',
  props: {
    campaignId: {
      type: Number,
      required: true
    }
  },
  setup(props) {
    const campaign = ref({});
    const donations = ref([]);
    const expenditures = ref([]);
    const blockchainEvents = ref([]);
    const statistics = ref({});
    const activeTab = ref('donations');
    const hasMoreDonations = ref(false);
    
    const tabs = [
      { id: 'donations', label: '๊ธฐ๋ถ€ ๋‚ด์—ญ', icon: '๐Ÿ’' },
      { id: 'expenditures', label: '์ง‘ํ–‰ ๋‚ด์—ญ', icon: '๐Ÿ“ค' },
      { id: 'blockchain', label: '๋ธ”๋ก์ฒด์ธ', icon: 'โ›“๏ธ' },
      { id: 'statistics', label: 'ํ†ต๊ณ„', icon: '๐Ÿ“ˆ' }
    ];
    
    const goalPercentage = computed(() => {
      if (!campaign.value.goal_amount) return 0;
      return Math.min(
        (campaign.value.current_amount / campaign.value.goal_amount) * 100,
        100
      ).toFixed(1);
    });
    
    const loadCampaignData = async () => {
      try {
        const response = await axios.get(`/api/campaigns/${props.campaignId}`);
        campaign.value = response.data.data;
      } catch (error) {
        console.error('Failed to load campaign data:', error);
      }
    };
    
    const loadDonations = async () => {
      try {
        const response = await axios.get(`/api/donations`, {
          params: { campaign_id: props.campaignId }
        });
        donations.value = response.data.data.data;
        hasMoreDonations.value = response.data.data.next_page_url !== null;
      } catch (error) {
        console.error('Failed to load donations:', error);
      }
    };
    
    const loadExpenditures = async () => {
      try {
        const response = await axios.get(`/api/expenditures`, {
          params: { campaign_id: props.campaignId }
        });
        expenditures.value = response.data.data;
      } catch (error) {
        console.error('Failed to load expenditures:', error);
      }
    };
    
    const loadBlockchainEvents = async () => {
      try {
        const response = await axios.get(`/api/blockchain-logs`, {
          params: { campaign_id: props.campaignId }
        });
        blockchainEvents.value = response.data.data;
      } catch (error) {
        console.error('Failed to load blockchain events:', error);
      }
    };
    
    const loadStatistics = async () => {
      try {
        const response = await axios.get(`/api/campaigns/${props.campaignId}/statistics`);
        statistics.value = response.data.data;
        
        // ์ฐจํŠธ ๊ทธ๋ฆฌ๊ธฐ
        drawCharts();
      } catch (error) {
        console.error('Failed to load statistics:', error);
      }
    };
    
    const drawCharts = () => {
      // ์ผ๋ณ„ ๊ธฐ๋ถ€ ์ถ”์ด ์ฐจํŠธ
      // Chart.js ๊ตฌํ˜„...
    };
    
    const formatCurrency = (amount) => {
      return new Intl.NumberFormat('ko-KR', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 4
      }).format(amount);
    };
    
    const formatDate = (date) => {
      return new Date(date).toLocaleString('ko-KR');
    };
    
    const getEtherscanUrl = (txHash) => {
      const network = process.env.VUE_APP_BLOCKCHAIN_NETWORK;
      const baseUrl = network === 'mainnet' 
        ? 'https://etherscan.io' 
        : `https://${network}.etherscan.io`;
      return `${baseUrl}/tx/${txHash}`;
    };
    
    const getStatusText = (status) => {
      const statusMap = {
        pending: '๋Œ€๊ธฐ์ค‘',
        confirmed: 'ํ™•์ธ๋จ',
        failed: '์‹คํŒจ'
      };
      return statusMap[status] || status;
    };
    
    const formatEventData = (data) => {
      return JSON.stringify(data, null, 2);
    };
    
    onMounted(() => {
      loadCampaignData();
      loadDonations();
      loadExpenditures();
      loadBlockchainEvents();
      loadStatistics();
      
      // ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (์›น์†Œ์ผ“ ๋˜๋Š” ํด๋ง)
      setInterval(loadCampaignData, 30000);
    });
    
    return {
      campaign,
      donations,
      expenditures,
      blockchainEvents,
      statistics,
      activeTab,
      tabs,
      goalPercentage,
      hasMoreDonations,
      formatCurrency,
      formatDate,
      getEtherscanUrl,
      getStatusText,
      formatEventData
    };
  }
};
</script>

<style>.tx-header{background:linear-gradient(135deg,#f5f7fa 0%,#e8ecf1 100%);
border-radius:16px;
padding:40px;
text-align:center;
margin-bottom:40px;
box-shadow:0 4px 15px rgba(0,0,0,0.08);}
.tx-section{margin:50px 0;
padding:30px;
background:#fafbfc;
border-radius:12px;
border-left:4px solid #7c9cbf;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7d3 100%);
padding:25px;
border-radius:10px;
margin:25px 0;
border:2px solid #e8d5b7;}
.tx-tip{background:#e8f4f8;
padding:20px;
border-radius:10px;
margin:25px 0;
border-left:5px solid #4a9ebb;}
.tx-warning{background:#fff4e6;
padding:20px;
border-radius:10px;
margin:25px 0;
border-left:5px solid #f59e0b;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;}
.tx-table th{background:#7c9cbf;
color:#fff;
padding:15px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px 15px;
border-bottom:1px solid #e5e7eb;}
.tx-step{background:#f0f4f8;
padding:20px;
margin:20px 0;
border-radius:10px;
border-left:5px solid #5a7a9b;}
.tx-card{background:#fff;
padding:25px;
border-radius:12px;
margin:20px 0;
box-shadow:0 2px 10px rgba(0,0,0,0.06);}
.tx-emphasis{color:#5a7a9b;
font-weight:600;}
.tx-subtitle{color:#6b7280;
font-size:1.1em;
margin:15px 0;}</style>

๐ŸŽฏ ๋Œ€์‹œ๋ณด๋“œ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ

1. ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”
- ๋ธ”๋ก์ฒด์ธ๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋™๊ธฐํ™”
- WebSocket์ด๋‚˜ ํด๋ง์„ ํ†ตํ•œ ์ž๋™ ์—…๋ฐ์ดํŠธ

2. ๋ธ”๋ก์ฒด์ธ ๊ฒ€์ฆ ๋ฐฐ์ง€
- ๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋ก๋œ ๋ฐ์ดํ„ฐ์ž„์„ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ
- Etherscan ๋งํฌ๋กœ ์ง์ ‘ ํ™•์ธ ๊ฐ€๋Šฅ

3. ๋‹ค์–‘ํ•œ ์‹œ๊ฐํ™”
- Chart.js๋ฅผ ํ™œ์šฉํ•œ ํ†ต๊ณ„ ์ฐจํŠธ
- ์ง„ํ–‰๋ฅ  ๋ฐ”, ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„์„ ๋“ฑ

4. ํˆฌ๋ช…ํ•œ ์ง‘ํ–‰ ๋‚ด์—ญ
- ๋ชจ๋“  ์ง€์ถœ์— ๋Œ€ํ•œ ์ƒ์„ธ ์„ค๋ช…
- ์˜์ˆ˜์ฆ ์ฒจ๋ถ€ ๊ธฐ๋Šฅ
- ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ์ œ๊ณต

๐Ÿ”’ ๋ณด์•ˆ๊ณผ ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ

๋ธ”๋ก์ฒด์ธ์€ ํˆฌ๋ช…ํ•˜์ง€๋งŒ, ๊ทธ๋ ‡๋‹ค๊ณ  ๋ชจ๋“  ์ •๋ณด๋ฅผ ๋‹ค ๊ณต๊ฐœํ•˜๋ฉด ์•ˆ ๋ผ. ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ๋„ ์ค‘์š”ํ•˜๊ฑฐ๋“ ! ๐Ÿ›ก๏ธ

๐ŸŽญ ์ต๋ช… ๊ธฐ๋ถ€ ์ฒ˜๋ฆฌ

๊ธฐ๋ถ€์ž๊ฐ€ ์ต๋ช…์„ ์›ํ•  ๊ฒฝ์šฐ, ๋ธ”๋ก์ฒด์ธ์—๋Š” ์ง€๊ฐ‘ ์ฃผ์†Œ๋งŒ ๊ธฐ๋กํ•˜๊ณ  ์‹ค์ œ ์‹ ์› ์ •๋ณด๋Š” ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•ด์•ผ ํ•ด:
<?php

namespace App\Services;

use Illuminate\Support\Facades\Crypt;

class PrivacyService
{
    /**
     * ๋ฏผ๊ฐํ•œ ์ •๋ณด ์•”ํ˜ธํ™”
     */
    public function encryptSensitiveData(array $data): string
    {
        return Crypt::encryptString(json_encode($data));
    }
    
    /**
     * ์•”ํ˜ธํ™”๋œ ์ •๋ณด ๋ณตํ˜ธํ™”
     */
    public function decryptSensitiveData(string $encrypted): array
    {
        return json_decode(Crypt::decryptString($encrypted), true);
    }
    
    /**
     * ์ง€๊ฐ‘ ์ฃผ์†Œ ๋งˆ์Šคํ‚น
     */
    public function maskWalletAddress(string $address): string
    {
        if (strlen($address) < 10) {
            return $address;
        }
        
        return substr($address, 0, 6) . '...' . substr($address, -4);
    }
    
    /**
     * ์ต๋ช… ๊ธฐ๋ถ€์ž ํ‘œ์‹œ๋ช… ์ƒ์„ฑ
     */
    public function generateAnonymousName(int $donationId): string
    {
        $adjectives = ['๋”ฐ๋œปํ•œ', '์„ ํ•œ', '๋น›๋‚˜๋Š”', '์•„๋ฆ„๋‹ค์šด', '๊ณ ๋งˆ์šด'];
        $nouns = ['๋งˆ์Œ', '์ฒœ์‚ฌ', '๋ณ„', '์†๊ธธ', 'ํฌ๋ง'];
        
        $adj = $adjectives[$donationId % count($adjectives)];
        $noun = $nouns[($donationId * 7) % count($nouns)];
        
        return $adj . ' ' . $noun;
    }
}

๐Ÿ” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ณด์•ˆ ํŒจํ„ด

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ํ•œ๋ฒˆ ๋ฐฐํฌํ•˜๋ฉด ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•ด. ๊ทธ๋ž˜์„œ ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด! ๋‹ค์Œ ํŒจํ„ด๋“ค์„ ๋ฐ˜๋“œ์‹œ ์ ์šฉํ•ด์•ผ ํ•ด:
1. Checks-Effects-Interactions ํŒจํ„ด

์žฌ์ง„์ž… ๊ณต๊ฒฉ(Reentrancy Attack)์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํŒจํ„ด์ด์•ผ:
function withdraw(uint256 amount) public {
    // 1. Checks: ์กฐ๊ฑด ํ™•์ธ
    require(balances[msg.sender] >= amount, "Insufficient balance");
    
    // 2. Effects: ์ƒํƒœ ๋ณ€๊ฒฝ
    balances[msg.sender] -= amount;
    
    // 3. Interactions: ์™ธ๋ถ€ ํ˜ธ์ถœ
    payable(msg.sender).transfer(amount);
}
2. Pull over Push ํŒจํ„ด

์ž๊ธˆ์„ ๋ณด๋‚ด๋Š” ๋Œ€์‹  ๋ฐ›์•„๊ฐ€๊ฒŒ ํ•˜๋Š” ํŒจํ„ด:
mapping(address => uint256) public pendingWithdrawals;

function initiateWithdrawal(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    pendingWithdrawals[msg.sender] += amount;
}

function withdraw() public {
    uint256 amount = pendingWithdrawals[msg.sender];
    require(amount > 0);
    pendingWithdrawals[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
}
3. ๊ธด๊ธ‰ ์ •์ง€(Circuit Breaker) ํŒจํ„ด

๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ผ์‹œ ์ •์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ:
bool public paused = false;
address public owner;

modifier whenNotPaused() {
    require(!paused, "Contract is paused");
    _;
}

modifier onlyOwner() {
    require(msg.sender == owner);
    _;
}

function pause() public onlyOwner {
    paused = true;
}

function unpause() public onlyOwner {
    paused = false;
}

function donate() public payable whenNotPaused {
    // ๊ธฐ๋ถ€ ๋กœ์ง
}
โš ๏ธ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:
  • โœ… ์žฌ์ง„์ž… ๊ณต๊ฒฉ ๋ฐฉ์ง€
  • โœ… ์ •์ˆ˜ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ/์–ธ๋”ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€ (Solidity 0.8.0 ์ด์ƒ์—์„œ๋Š” ์ž๋™)
  • โœ… ๊ฐ€์Šค ํ•œ๋„ ๊ณ ๋ ค
  • โœ… ์ ‘๊ทผ ์ œ์–ด ๊ตฌํ˜„
  • โœ… ์™ธ๋ถ€ ํ˜ธ์ถœ ์ตœ์†Œํ™”
  • โœ… ์ด๋ฒคํŠธ ๋กœ๊น…
  • โœ… ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฐ€๋Šฅ์„ฑ ๊ณ ๋ ค (ํ”„๋ก์‹œ ํŒจํ„ด)
  • โœ… ๊ฐ์‚ฌ(Audit) ๋ฐ›๊ธฐ

๐Ÿš€ ๋ฐฐํฌ ๋ฐ ์šด์˜

์ž, ์ด์ œ ์‹ค์ œ๋กœ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ํ…Œ์ŠคํŠธ๋„ท๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ๋ฉ”์ธ๋„ท๊นŒ์ง€ ๋‹จ๊ณ„๋ณ„๋กœ ์ง„ํ–‰ํ•  ๊ฑฐ์•ผ.

๐Ÿงช ํ…Œ์ŠคํŠธ๋„ท ๋ฐฐํฌ (Sepolia)

๋จผ์ € ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด. ์‹ค์ˆ˜ํ•ด๋„ ์‹ค์ œ ๋ˆ์ด ๋‚ ์•„๊ฐ€์ง€ ์•Š์œผ๋‹ˆ๊นŒ! ๐Ÿ˜…
# Hardhat ์„ค์น˜ ๋ฐ ์„ค์ •
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers

# hardhat.config.js
require("@nomiclabs/hardhat-ethers");

module.exports = {
  solidity: "0.8.19",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
      chainId: 11155111
    }
  }
};

# ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ (scripts/deploy.js)
const hre = require("hardhat");

async function main() {
  const DonationCampaign = await hre.ethers.getContractFactory("DonationCampaign");
  
  const beneficiary = "0xBeneficiaryAddress";
  const platformAdmin = "0xPlatformAdminAddress";
  const goalAmount = hre.ethers.utils.parseEther("10.0");
  const durationInDays = 30;
  
  console.log("Deploying DonationCampaign...");
  
  const campaign = await DonationCampaign.deploy(
    beneficiary,
    platformAdmin,
    goalAmount,
    durationInDays
  );
  
  await campaign.deployed();
  
  console.log("DonationCampaign deployed to:", campaign.address);
  
  // ๋ฐฐํฌ ์ •๋ณด ์ €์žฅ
  const fs = require('fs');
  const deploymentInfo = {
    address: campaign.address,
    network: "sepolia",
    deployer: (await hre.ethers.getSigners())[0].address,
    timestamp: new Date().toISOString(),
    constructorArgs: {
      beneficiary,
      platformAdmin,
      goalAmount: goalAmount.toString(),
      durationInDays
    }
  };
  
  fs.writeFileSync(
    'deployments/sepolia.json',
    JSON.stringify(deploymentInfo, null, 2)
  );
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

๐Ÿญ ๋ผ๋ผ๋ฒจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ

๋ผ๋ผ๋ฒจ ์•ฑ์€ ์ผ๋ฐ˜์ ์ธ PHP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ ๋ฐฉ์‹์„ ๋”ฐ๋ฅด๋ฉด ๋ผ. ๋‹ค๋งŒ ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ์„ค์ •์„ ์ถ”๊ฐ€๋กœ ํ•ด์ค˜์•ผ ํ•ด:
# .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-donation-platform.com

# Database
DB_CONNECTION=pgsql
DB_HOST=your-db-host
DB_DATABASE=donation_platform
DB_USERNAME=your-username
DB_PASSWORD=your-password

# Blockchain
BLOCKCHAIN_NETWORK=mainnet
BLOCKCHAIN_RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
BLOCKCHAIN_CHAIN_ID=1

# Redis (for queues)
REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password

# Queue
QUEUE_CONNECTION=redis

# ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ (deploy.sh)
#!/bin/bash

echo "๐Ÿš€ Starting deployment..."

# Git pull
git pull origin main

# Install dependencies
composer install --no-dev --optimize-autoloader

# Run migrations
php artisan migrate --force

# Clear and cache config
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Restart queue workers
php artisan queue:restart

# Restart PHP-FPM
sudo systemctl restart php8.2-fpm

echo "โœ… Deployment completed!"

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

์šด์˜ ์ค‘์—๋Š” ๋ฐ˜๋“œ์‹œ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ํ•ด์•ผ ํ•ด. ํŠนํžˆ ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜์€ ์‹คํŒจํ•  ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ๊นŒ!
<?php

namespace App\Console\Commands;

use App\Models\Donation;
use App\Models\Campaign;
use App\Services\BlockchainService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

class MonitorBlockchainTransactions extends Command
{
    protected $signature = 'blockchain:monitor';
    protected $description = 'Monitor pending blockchain transactions';
    
    protected $blockchainService;
    
    public function __construct(BlockchainService $blockchainService)
    {
        parent::__construct();
        $this->blockchainService = $blockchainService;
    }
    
    public function handle()
    {
        $this->info('Monitoring blockchain transactions...');
        
        // ๋Œ€๊ธฐ ์ค‘์ธ ๊ธฐ๋ถ€ ํŠธ๋žœ์žญ์…˜ ํ™•์ธ
        $pendingDonations = Donation::where('status', 'pending')
            ->whereNotNull('transaction_hash')
            ->where('created_at', '>', now()->subHours(24))
            ->get();
        
        foreach ($pendingDonations as $donation) {
            $this->checkTransactionStatus($donation);
        }
        
        // ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ์™€ DB ๋™๊ธฐํ™” ํ™•์ธ
        $this->syncBlockchainData();
        
        $this->info('Monitoring completed.');
    }
    
    protected function checkTransactionStatus(Donation $donation)
    {
        try {
            $receipt = $this->blockchainService->getTransactionReceipt(
                $donation->transaction_hash
            );
            
            if ($receipt) {
                if ($receipt['status'] === '0x1') {
                    // ์„ฑ๊ณต
                    $donation->update([
                        'status' => 'confirmed',
                        'block_number' => hexdec($receipt['blockNumber']),
                        'blockchain_confirmed_at' => now(),
                    ]);
                    
                    $this->info("โœ… Donation {$donation->id} confirmed");
                } else {
                    // ์‹คํŒจ
                    $donation->update(['status' => 'failed']);
                    $this->error("โŒ Donation {$donation->id} failed");
                    
                    // ๊ด€๋ฆฌ์ž์—๊ฒŒ ์•Œ๋ฆผ
                    Log::error('Donation transaction failed', [
                        'donation_id' => $donation->id,
                        'transaction_hash' => $donation->transaction_hash,
                    ]);
                }
            } else {
                // ์•„์ง ๋Œ€๊ธฐ ์ค‘
                $hoursSinceDonation = $donation->created_at->diffInHours(now());
                
                if ($hoursSinceDonation > 2) {
                    $this->warn("โฐ Donation {$donation->id} pending for {$hoursSinceDonation} hours");
                }
            }
            
        } catch (\Exception $e) {
            $this->error("Error checking donation {$donation->id}: " . $e->getMessage());
        }
    }
    
    protected function syncBlockchainData()
    {
        $campaigns = Campaign::where('status', 'active')
            ->whereNotNull('smart_contract_address')
            ->get();
        
        foreach ($campaigns as $campaign) {
            try {
                $blockchainInfo = $this->blockchainService->getCampaignInfo(
                    $campaign->smart_contract_address
                );
                
                if ($blockchainInfo['success']) {
                    $blockchainAmount = $blockchainInfo['data']['current_amount'];
                    $dbAmount = $campaign->current_amount;
                    
                    // ๊ธˆ์•ก ๋ถˆ์ผ์น˜ ํ™•์ธ
                    if (abs($blockchainAmount - $dbAmount) > 0.0001) {
                        $this->warn("โš ๏ธ  Amount mismatch for campaign {$campaign->id}");
                        $this->warn("   Blockchain: {$blockchainAmount}, DB: {$dbAmount}");
                        
                        Log::warning('Campaign amount mismatch', [
                            'campaign_id' => $campaign->id,
                            'blockchain_amount' => $blockchainAmount,
                            'db_amount' => $dbAmount,
                        ]);
                    }
                }
                
            } catch (\Exception $e) {
                $this->error("Error syncing campaign {$campaign->id}: " . $e->getMessage());
            }
        }
    }
}

// ์Šค์ผ€์ค„๋Ÿฌ์— ๋“ฑ๋ก (app/Console/Kernel.php)
protected function schedule(Schedule $schedule)
{
    // 5๋ถ„๋งˆ๋‹ค ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง
    $schedule->command('blockchain:monitor')
        ->everyFiveMinutes()
        ->withoutOverlapping();
    
    // ๋งค์ผ ์ž์ •์— ํ†ต๊ณ„ ์ง‘๊ณ„
    $schedule->command('statistics:aggregate')
        ->daily();
}

๐Ÿ’ก ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์‹ค์ œ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ฐฐ์šด ๋…ธํ•˜์šฐ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ์ด๊ฑฐ ์ง„์งœ ์ค‘์š”ํ•ด. ๐Ÿ˜Ž

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”

1. ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ ์ตœ์†Œํ™”

๋ธ”๋ก์ฒด์ธ ์กฐํšŒ๋Š” ๋А๋ฆฌ๊ณ  ๋น„์šฉ์ด ๋“ค์–ด. ๊ทธ๋ž˜์„œ ์บ์‹ฑ์„ ์ ๊ทน ํ™œ์šฉํ•ด์•ผ ํ•ด:
<?php

use Illuminate\Support\Facades\Cache;

class CampaignService
{
    public function getCampaignWithBlockchainData($campaignId)
    {
        $cacheKey = "campaign.{$campaignId}.blockchain";
        
        return Cache::remember($cacheKey, 300, function () use ($campaignId) {
            $campaign = Campaign::findOrFail($campaignId);
            
            if ($campaign->smart_contract_address) {
                $blockchainData = $this->blockchainService
                    ->getCampaignInfo($campaign->smart_contract_address);
                
                $campaign->blockchain_data = $blockchainData['data'];
            }
            
            return $campaign;
        });
    }
}
2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ

์ž์ฃผ ์กฐํšŒํ•˜๋Š” ์ปฌ๋Ÿผ์—๋Š” ๋ฐ˜๋“œ์‹œ ์ธ๋ฑ์Šค๋ฅผ ๊ฑธ์–ด์ค˜:
// ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์—์„œ
$table->index(['campaign_id', 'status', 'created_at']);
$table->index('transaction_hash');
$table->index(['donor_id', 'created_at']);
3. Eager Loading ํ™œ์šฉ

N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ด€๊ณ„๋ฅผ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•ด:
// โŒ ๋‚˜์œ ์˜ˆ
$campaigns = Campaign::all();
foreach ($campaigns as $campaign) {
    echo $campaign->beneficiary->name; // N+1 ์ฟผ๋ฆฌ ๋ฐœ์ƒ!
}

// โœ… ์ข‹์€ ์˜ˆ
$campaigns = Campaign::with('beneficiary', 'donations')->get();
foreach ($campaigns as $campaign) {
    echo $campaign->beneficiary->name; // ๋‹จ 2๊ฐœ์˜ ์ฟผ๋ฆฌ๋งŒ!
}

๐Ÿ”„ ์žฌ๋Šฅ๋„ท๊ณผ์˜ ์—ฐ๋™

์ด๋Ÿฐ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์„ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” ๋Šฅ๋ ฅ ์ž์ฒด๊ฐ€ ํ•˜๋‚˜์˜ ์žฌ๋Šฅ์ด์•ผ! ์žฌ๋Šฅ๋„ท์—์„œ๋Š” ์ด๋Ÿฐ ์ „๋ฌธ์ ์ธ ๊ฐœ๋ฐœ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๊ฑฐ๋‚˜, ๋ฐ˜๋Œ€๋กœ ํ•„์š”ํ•œ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฐพ์„ ์ˆ˜๋„ ์žˆ์–ด.

์˜ˆ๋ฅผ ๋“ค์–ด:
- "๋ธ”๋ก์ฒด์ธ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐœ๋ฐœ" ์„œ๋น„์Šค ๋“ฑ๋ก
- "๋ผ๋ผ๋ฒจ ๊ธฐ๋ฐ˜ Web3 ํ”Œ๋žซํผ ๊ตฌ์ถ•" ํ”„๋กœ์ ํŠธ ์˜๋ขฐ
- "๋ธ”๋ก์ฒด์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ" ์ „๋ฌธ๊ฐ€ ์ฐพ๊ธฐ

ํŠนํžˆ ์š”์ฆ˜์€ Web3์™€ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž ์ˆ˜์š”๊ฐ€ ์—„์ฒญ ๋งŽ์•„์„œ, ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ž์‹ ์˜ ์ „๋ฌธ์„ฑ์„ ํ™œ์šฉํ•˜๋ฉด ์ข‹์€ ๊ธฐํšŒ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

๐Ÿ› ๋””๋ฒ„๊น… ํŒ

๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋””๋ฒ„๊น…

ํŠธ๋žœ์žญ์…˜์ด ์‹คํŒจํ–ˆ์„ ๋•Œ ์›์ธ์„ ์ฐพ๋Š” ๋ฐฉ๋ฒ•:

1. Etherscan์—์„œ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ์กฐํšŒ
2. "State" ํƒญ์—์„œ ์ƒํƒœ ๋ณ€ํ™” ํ™•์ธ
3. "Logs" ํƒญ์—์„œ ์ด๋ฒคํŠธ ํ™•์ธ
4. "Internal Txns" ํƒญ์—์„œ ๋‚ด๋ถ€ ํ˜ธ์ถœ ํ™•์ธ
5. Revert ๋ฉ”์‹œ์ง€ ํ™•์ธ

Hardhat Console๋กœ ๋กœ์ปฌ ํ…Œ์ŠคํŠธ:
npx hardhat console --network localhost

const Campaign = await ethers.getContractFactory("DonationCampaign");
const campaign = await Campaign.attach("0xContractAddress");

// ์ƒํƒœ ํ™•์ธ
const goalAmount = await campaign.goalAmount();
console.log("Goal:", ethers.utils.formatEther(goalAmount));

// ํ•จ์ˆ˜ ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ
const tx = await campaign.donate("Test message", { value: ethers.utils.parseEther("0.1") });
await tx.wait();
console.log("Donation successful!");

๐Ÿ’ฐ ๊ฐ€์Šค๋น„ ์ตœ์ ํ™”

์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์—์„œ๋Š” ๊ฐ€์Šค๋น„๊ฐ€ ๋น„์‹ธ. ์ตœ์ ํ™”๊ฐ€ ํ•„์ˆ˜์•ผ!
๊ฐ€์Šค๋น„ ์ ˆ์•ฝ ํ…Œํฌ๋‹‰:

1. Storage ์‚ฌ์šฉ ์ตœ์†Œํ™”
- Storage๋Š” ๊ฐ€์žฅ ๋น„์‹ธ. ๊ฐ€๋Šฅํ•˜๋ฉด Memory๋‚˜ Calldata ์‚ฌ์šฉ
- ๋ถˆํ•„์š”ํ•œ ์ƒํƒœ ๋ณ€์ˆ˜ ์ œ๊ฑฐ

2. ๋ฐฐ์—ด ๋Œ€์‹  ๋งคํ•‘ ์‚ฌ์šฉ
- ๋ฐฐ์—ด ์ˆœํšŒ๋Š” ๊ฐ€์Šค๋น„๊ฐ€ ๋งŽ์ด ๋“ค์–ด
- ๋งคํ•‘์€ O(1) ์กฐํšŒ ๊ฐ€๋Šฅ

3. Short Circuit ํ‰๊ฐ€ ํ™œ์šฉ
- `require(a && b)` ๋Œ€์‹  `require(a); require(b);`
- ์ฒซ ๋ฒˆ์งธ ์กฐ๊ฑด์—์„œ ์‹คํŒจํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ๊ฒ€์‚ฌ ์•ˆ ํ•จ

4. ์ด๋ฒคํŠธ ํ™œ์šฉ
- ๋ฐ์ดํ„ฐ๋ฅผ Storage์— ์ €์žฅํ•˜๋Š” ๋Œ€์‹  Event๋กœ ๋กœ๊น…
- ๋‚˜์ค‘์— ๋กœ๊ทธ์—์„œ ์กฐํšŒ ๊ฐ€๋Šฅ

5. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
- ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌ
- ์˜ˆ: ์—ฌ๋Ÿฌ ๊ธฐ๋ถ€์ž์—๊ฒŒ ํ•œ ๋ฒˆ์— ํ™˜๋ถˆ

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค์™€ ๋‹ค์Œ ๋‹จ๊ณ„

์—ฌ๊ธฐ๊นŒ์ง€ ๋”ฐ๋ผ์™”๋‹ค๋ฉด ์ถ•ํ•˜ํ•ด! ๐ŸŽ‰ ์ด์ œ ๊ธฐ๋ณธ์ ์ธ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ.

ํ•˜์ง€๋งŒ ๋ฐฐ์›€์—๋Š” ๋์ด ์—†์ง€. ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ์ถ”์ฒœํ•ด:

๐Ÿ“š ์ถ”์ฒœ ํ•™์Šต ์ž๋ฃŒ

๋ธ”๋ก์ฒด์ธ & ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ:
- Solidity ๊ณต์‹ ๋ฌธ์„œ (docs.soliditylang.org)
- OpenZeppelin Contracts (๊ฒ€์ฆ๋œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)
- Ethereum.org ๊ฐœ๋ฐœ์ž ๊ฐ€์ด๋“œ
- CryptoZombies (๊ฒŒ์ž„์œผ๋กœ ๋ฐฐ์šฐ๋Š” Solidity)

๋ผ๋ผ๋ฒจ & PHP:
- Laravel ๊ณต์‹ ๋ฌธ์„œ
- Laracasts (๋น„๋””์˜ค ๊ฐ•์˜)
- Laravel Daily (์‹ค์ „ ํŒ)

Web3 ๊ฐœ๋ฐœ:
- web3.js ๋ฌธ์„œ
- ethers.js ๋ฌธ์„œ
- Hardhat ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
- Truffle Suite

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

๊ธฐ๋ณธ ํ”Œ๋žซํผ์„ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด, ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด:

1. NFT ๊ธฐ๋ถ€ ์ฆ์„œ
- ๊ธฐ๋ถ€์ž์—๊ฒŒ NFT ํ˜•ํƒœ์˜ ๊ฐ์‚ฌ ์ฆ์„œ ๋ฐœํ–‰
- ๊ธฐ๋ถ€ ๊ธˆ์•ก์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋“ฑ๊ธ‰์˜ NFT

2. DAO ๊ฑฐ๋ฒ„๋„Œ์Šค
- ๊ธฐ๋ถ€๊ธˆ ์‚ฌ์šฉ์ฒ˜๋ฅผ ๊ธฐ๋ถ€์ž๋“ค์ด ํˆฌํ‘œ๋กœ ๊ฒฐ์ •
- ํ† ํฐ ๊ธฐ๋ฐ˜ ํˆฌํ‘œ ์‹œ์Šคํ…œ

3. ํฌ๋กœ์Šค์ฒด์ธ ์ง€์›
- ์ด๋”๋ฆฌ์›€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํด๋ฆฌ๊ณค, BSC ๋“ฑ ๋‹ค๋ฅธ ์ฒด์ธ ์ง€์›
- ๋ธŒ๋ฆฟ์ง€๋ฅผ ํ†ตํ•œ ์ฒด์ธ ๊ฐ„ ์ž์‚ฐ ์ด๋™

4. DeFi ํ†ตํ•ฉ
- ๊ธฐ๋ถ€๊ธˆ์„ ์Šคํ…Œ์ดํ‚นํ•ด์„œ ์ด์ž ์ˆ˜์ต ์ฐฝ์ถœ
- ์ˆ˜์ต๊ธˆ๋„ ๊ธฐ๋ถ€์— ์‚ฌ์šฉ

5. ์†Œ์…œ ๊ธฐ๋Šฅ
- ๊ธฐ๋ถ€์ž ์ปค๋ฎค๋‹ˆํ‹ฐ
- ์บ ํŽ˜์ธ ๊ณต์œ  ๋ฐ ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ…
- ๊ธฐ๋ถ€ ์ฑŒ๋ฆฐ์ง€ ์ด๋ฒคํŠธ

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐Ÿƒโ€โ™‚๏ธ๐Ÿ’จ

์šฐ๋ฆฌ๋Š” ๋ธ”๋ก์ฒด์ธ์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ, ๋ผ๋ผ๋ฒจ๋กœ ์‹ค์ œ ๋™์ž‘ํ•˜๋Š” ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๊นŒ์ง€ ๋ชจ๋‘ ์‚ดํŽด๋ดค์–ด. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑ, ๋ฐฑ์—”๋“œ API ๊ตฌํ˜„, ํ”„๋ก ํŠธ์—”๋“œ ๋Œ€์‹œ๋ณด๋“œ, ๋ณด์•ˆ, ๋ฐฐํฌ, ๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€... ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜์ง€?

๐ŸŒŸ ํ•ต์‹ฌ ํฌ์ธํŠธ ์ •๋ฆฌ

1. ๋ธ”๋ก์ฒด์ธ์˜ ๊ฐ€์น˜
- ํˆฌ๋ช…์„ฑ: ๋ชจ๋“  ๊ฑฐ๋ž˜๊ฐ€ ๊ณต๊ฐœ์ ์œผ๋กœ ๊ฒ€์ฆ ๊ฐ€๋Šฅ
- ๋ถˆ๋ณ€์„ฑ: ํ•œ๋ฒˆ ๊ธฐ๋ก๋œ ๋ฐ์ดํ„ฐ๋Š” ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€
- ํƒˆ์ค‘์•™ํ™”: ์ค‘์•™ ๊ธฐ๊ด€ ์—†์ด๋„ ์‹ ๋ขฐ ๊ตฌ์ถ•

2. ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ
- ๋ธ”๋ก์ฒด์ธ: ์ค‘์š”ํ•œ ๊ฑฐ๋ž˜ ๊ธฐ๋ก
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: ๋น ๋ฅธ ์กฐํšŒ์™€ ๊ฒ€์ƒ‰
- ๋‘˜์˜ ์žฅ์ ์„ ๊ฒฐํ•ฉํ•œ ์‹ค์šฉ์  ์„ค๊ณ„

3. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
- ๋ณต์žกํ•œ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์„ ์ˆจ๊ธฐ๊ณ 
- ์ง๊ด€์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค ์ œ๊ณต
- ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ๊ณผ ํˆฌ๋ช…ํ•œ ์ •๋ณด ๊ณต๊ฐœ

4. ๋ณด์•ˆ๊ณผ ์‹ ๋ขฐ
- ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ณด์•ˆ ํŒจํ„ด ์ ์šฉ
- ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ์™€ ํˆฌ๋ช…์„ฑ์˜ ๊ท ํ˜•
- ์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๊ฐ์‚ฌ

๐Ÿ’ญ ๋งˆ์ง€๋ง‰ ์กฐ์–ธ

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด. ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๊ณผ ํŒจํ„ด์ด ๋Š์ž„์—†์ด ๋‚˜์˜ค๊ณ  ์žˆ์ง€. ๊ทธ๋ž˜์„œ ์ค‘์š”ํ•œ ๊ฑด ๊ณ„์† ๋ฐฐ์šฐ๊ณ  ์‹คํ—˜ํ•˜๋Š” ์ž์„ธ์•ผ.

์ฒ˜์Œ์—๋Š” ์–ด๋ ต๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด. ๋‚˜๋„ ์ฒ˜์Œ ๋ธ”๋ก์ฒด์ธ์„ ์ ‘ํ–ˆ์„ ๋•Œ "์ด๊ฒŒ ๋ญ” ์†Œ๋ฆฌ์•ผ?" ํ–ˆ๊ฑฐ๋“ . ๐Ÿ˜… ํ•˜์ง€๋งŒ ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผํ•˜๋‹ค ๋ณด๋ฉด, ์–ด๋А์ƒˆ ๋„ค๊ฐ€ ์ง์ ‘ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋ฐฐํฌํ•˜๊ณ  ์žˆ์„ ๊ฑฐ์•ผ!

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์–ตํ•ด. ๊ธฐ์ˆ ์€ ๋„๊ตฌ์ผ ๋ฟ์ด์•ผ. ์ง„์งœ ์ค‘์š”ํ•œ ๊ฑด ์ด ๊ธฐ์ˆ ๋กœ ์–ด๋–ค ๊ฐ€์น˜๋ฅผ ๋งŒ๋“ค์–ด๋‚ผ ๊ฒƒ์ธ๊ฐ€์•ผ. ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ํˆฌ๋ช…ํ•œ ๊ธฐ๋ถ€ ํ”Œ๋žซํผ์ด ์‹ค์ œ๋กœ ๋” ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด ๊ธฐ๋ถ€์— ์ฐธ์—ฌํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ , ์ง„์งœ ๋„์›€์ด ํ•„์š”ํ•œ ๊ณณ์— ์ž์›์ด ์ „๋‹ฌ๋˜๊ฒŒ ํ•œ๋‹ค๋ฉด? ๊ทธ๊ฒŒ ๋ฐ”๋กœ ๊ฐœ๋ฐœ์ž๋กœ์„œ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ๋ฉ‹์ง„ ์ผ์ด ์•„๋‹๊นŒ? ๐ŸŒˆ

์ž, ์ด์ œ ๋„ค ์ฐจ๋ก€์•ผ! ์ด ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด. ๊ทธ๋ฆฌ๊ณ  ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๋ฌผ์–ด๋ณด๊ณ , ์ฝ”๋“œ๋ฅผ ๊ณต์œ ํ•˜๊ณ , ํ•จ๊ป˜ ์„ฑ์žฅํ•˜์ž!

ํ™”์ดํŒ…! ๐Ÿš€โœจ
๐ŸŽ‰ ํˆฌ๋ช…ํ•œ ๋ฏธ๋ž˜๋ฅผ ํ•จ๊ป˜ ๋งŒ๋“ค์–ด๊ฐ€์š”! ๋ธ”๋ก์ฒด์ธ์œผ๋กœ ์‹ ๋ขฐ๋ฅผ ์ฝ”๋”ฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ๐Ÿ’ก ๐Ÿš€ โค๏ธ ๐Ÿ”

๐Ÿ™ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

์ด ๊ธด ๊ธ€์„ ๋๊นŒ์ง€ ์ฝ์–ด์ฃผ์…”์„œ ์ •๋ง ๊ฐ์‚ฌํ•ด์š”.
์—ฌ๋Ÿฌ๋ถ„์˜ ์ฝ”๋“œ๊ฐ€ ์„ธ์ƒ์„ ๋” ํˆฌ๋ช…ํ•˜๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์œผ๋กœ ๋งŒ๋“ค๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

Happy Coding! ๐Ÿ’ปโœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.