์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ: ์‹ค์ „ ๊ฐ€์ด๋“œ

๐Ÿš€ ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ: ์‹ค์ „ ๊ฐ€์ด๋“œ

์›น2.0๊ณผ ์›น3.0์˜ ์™„๋ฒฝํ•œ ๋งŒ๋‚จ, ์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์“ฐ๋Š” ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

๐Ÿ’ก ์•ˆ๋…•! ์˜ค๋Š˜์€ ์ •๋ง ํ•ซํ•œ ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๋ฐ”๋กœ ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ(Solidity)๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ์š”์ฆ˜ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋‚˜ ํฌ๋ฆฝํ†  ๊ฒฐ์ œ ์‹œ์Šคํ…œ์ด ๋Œ€์„ธ์ž–์•„? ๊ทผ๋ฐ ๋ง‰์ƒ ๋งŒ๋“ค๋ ค๊ณ  ํ•˜๋ฉด ์–ด๋””์„œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์•ผ ํ• ์ง€ ๋ง‰๋ง‰ํ•˜๋”๋ผ๊ณ . ๊ทธ๋ž˜์„œ ๋‚ด๊ฐ€ ์ง์ ‘ ๊ฒฝํ—˜ํ•œ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์นœ๊ตฌํ•œํ…Œ ์„ค๋ช…ํ•˜๋“ฏ์ด ์‰ฝ๊ฒŒ ํ’€์–ด๋ณผ๊ฒŒ! ๐Ÿ˜Š

๋ผ๋ผ๋ฒจ PHP ํ”„๋ ˆ์ž„์›Œํฌ ๋ฐฑ์—”๋“œ API ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†”๋ฆฌ๋””ํ‹ฐ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฒฐ์ œ ๋กœ์ง ํ† ํฐ ์ „์†ก ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ ํƒˆ์ค‘์•™ํ™” Web3.js ์—ฐ๋™

๐ŸŽฏ ์™œ ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ• ๊นŒ?

๋จผ์ € ์ด ์กฐํ•ฉ์ด ์™œ ์ข‹์€์ง€๋ถ€ํ„ฐ ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ์‚ฌ์‹ค ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ๋•Œ ๊ฐ€์žฅ ํฐ ๊ณ ๋ฏผ์ด ๋ญ๋ƒ๋ฉด, "์ˆœ์ˆ˜ ๋ธ”๋ก์ฒด์ธ๋งŒ์œผ๋กœ ํ• ๊นŒ? ์•„๋‹ˆ๋ฉด ๊ธฐ์กด ์›น ๊ธฐ์ˆ ๊ณผ ์„ž์„๊นŒ?"์•ผ. ๋‹ต์€ ๋ช…ํ™•ํ•ด. ๋‘˜ ๋‹ค ์จ์•ผ ํ•ด! ๐ŸŽช

๐Ÿ”ฅ ๋ผ๋ผ๋ฒจ์˜ ์—ญํ• 

๋ผ๋ผ๋ฒจ์€ PHP ๊ธฐ๋ฐ˜์˜ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ธ๋ฐ, ์ •๋ง ๊ฐ•๋ ฅํ•œ ์นœ๊ตฌ์•ผ. ์‚ฌ์šฉ์ž ์ธ์ฆ, ์„ธ์…˜ ๊ด€๋ฆฌ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฒ˜๋ฆฌ, API ์—”๋“œํฌ์ธํŠธ ๊ตฌ์ถ• ๊ฐ™์€ ์ „ํ†ต์ ์ธ ์›น ์„œ๋น„์Šค ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•ด. ๋ธ”๋ก์ฒด์ธ์€ ๋А๋ฆฌ๊ณ  ๋น„์šฉ์ด ๋“ค๊ธฐ ๋•Œ๋ฌธ์—, ๋ชจ๋“  ๊ฑธ ๋ธ”๋ก์ฒด์ธ์— ์˜ฌ๋ฆด ์ˆœ ์—†๊ฑฐ๋“ . ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด, ์ฃผ๋ฌธ ๋‚ด์—ญ, ์ƒํ’ˆ ์ •๋ณด ๊ฐ™์€ ๊ฑด ์ผ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๋Š” ๊ฒŒ ํ›จ์”ฌ ํšจ์œจ์ ์ด์ง€.

๋ผ๋ผ๋ฒจ์˜ ์žฅ์ ์„ ์ •๋ฆฌํ•˜๋ฉด:
โœ… Eloquent ORM์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์ด ์ •๋ง ์‰ฌ์›Œ
โœ… ๋ฏธ๋“ค์›จ์–ด๋กœ ์ธ์ฆ/๊ถŒํ•œ ๊ด€๋ฆฌ๊ฐ€ ๊น”๋”ํ•ด
โœ… ํ(Queue) ์‹œ์Šคํ…œ์œผ๋กœ ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
โœ… ์ด๋ฒคํŠธ/๋ฆฌ์Šค๋„ˆ๋กœ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๊ฐ์ง€ ๋ฐ ์ฒ˜๋ฆฌ
โœ… ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„์™€ ํŒจํ‚ค์ง€๋“ค

โ›“๏ธ ์†”๋ฆฌ๋””ํ‹ฐ์˜ ์—ญํ• 

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

์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋“ค:
โœ… ๊ฒฐ์ œ ์ปจํŠธ๋ž™ํŠธ: ETH๋‚˜ ERC-20 ํ† ํฐ ๋ฐ›๊ธฐ
โœ… ์—์Šคํฌ๋กœ ์‹œ์Šคํ…œ: ์•ˆ์ „ํ•œ ๊ฑฐ๋ž˜๋ฅผ ์œ„ํ•œ ์ค‘๊ฐ„ ๋ณด๊ด€
โœ… ํ™˜๋ถˆ ๋กœ์ง: ์กฐ๊ฑด๋ถ€ ์ž๊ธˆ ๋ฐ˜ํ™˜
โœ… ์ˆ˜์ˆ˜๋ฃŒ ๋ถ„๋ฐฐ: ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ ์ž๋™ ์ •์‚ฐ
โœ… ์ด๋ฒคํŠธ ๋ฐœ์ƒ: ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ ์ด๋ฒคํŠธ emit

๐Ÿ’ก ์‹ค๋ฌด ํŒ: ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ๋ฅผ ๋„์ž…ํ•œ๋‹ค๋ฉด, ์‚ฌ์šฉ์ž ์ •๋ณด์™€ ์žฌ๋Šฅ ์ƒํ’ˆ ์ •๋ณด๋Š” ๋ผ๋ผ๋ฒจ DB์—, ์‹ค์ œ ๊ฒฐ์ œ ํŠธ๋žœ์žญ์…˜์€ ๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋กํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ฐฉ์‹์ด ์ตœ์ ์ด์•ผ!

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด์•ผ ํ•ด. ์ƒ๊ฐ๋ณด๋‹ค ์ค€๋น„ํ•  ๊ฒŒ ๋งŽ์œผ๋‹ˆ๊นŒ ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์™€๋ด! ๐Ÿš€

1๏ธโƒฃ ๋ผ๋ผ๋ฒจ ์„ค์น˜ ๋ฐ ์„ค์ •

๋ผ๋ผ๋ฒจ์€ Composer๋ฅผ ํ†ตํ•ด ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์–ด. ์ตœ์‹  ๋ฒ„์ „(Laravel 10 ์ด์ƒ)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ ์ถ”์ฒœํ•ด.

ํ„ฐ๋ฏธ๋„ ๋ช…๋ น์–ด
composer create-project laravel/laravel blockchain-payment
cd blockchain-payment
php artisan serve

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ธฐ๋ณธ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋ผ. http://localhost:8000์— ์ ‘์†ํ•˜๋ฉด ๋ผ๋ผ๋ฒจ ์›ฐ์ปด ํŽ˜์ด์ง€๊ฐ€ ๋ณด์ผ ๊ฑฐ์•ผ.

2๏ธโƒฃ ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜

๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋ ค๋ฉด Web3 ๊ด€๋ จ ํŒจํ‚ค์ง€๊ฐ€ ํ•„์š”ํ•ด. PHP์—์„œ๋Š” web3.php ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŽ์ด ์จ.

Composer ํŒจํ‚ค์ง€ ์„ค์น˜
composer require sc0vu/web3.php
composer require guzzlehttp/guzzle
composer require predis/predis

web3.php: ์ด๋”๋ฆฌ์›€ ๋…ธ๋“œ์™€ ํ†ต์‹ 
guzzle: HTTP ํด๋ผ์ด์–ธํŠธ (API ํ˜ธ์ถœ์šฉ)
predis: Redis ์บ์‹ฑ (ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ์ถ”์ ์šฉ)

3๏ธโƒฃ ์†”๋ฆฌ๋””ํ‹ฐ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐœ๋ฐœ์„ ์œ„ํ•ด์„œ๋Š” ๋ณ„๋„์˜ ํˆด์ฒด์ธ์ด ํ•„์š”ํ•ด. ์š”์ฆ˜์€ Hardhat์ด๋‚˜ Foundry๋ฅผ ๋งŽ์ด ์“ฐ๋Š”๋ฐ, ๋‚˜๋Š” Hardhat์„ ์ถ”์ฒœํ•ด. ์ดˆ๋ณด์ž๋„ ์“ฐ๊ธฐ ์‰ฝ๊ฑฐ๋“ !

Node.js ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”
mkdir smart-contracts
cd smart-contracts
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

Hardhat ์ดˆ๊ธฐํ™” ์‹œ "Create a JavaScript project"๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ผ. ๊ทธ๋Ÿฌ๋ฉด ๊ธฐ๋ณธ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๊ฐ€ ์ƒ์„ฑ๋ผ.

4๏ธโƒฃ ํ…Œ์ŠคํŠธ๋„ท ์„ค์ •

์‹ค์ œ ์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์—์„œ ํ…Œ์ŠคํŠธํ•˜๋ฉด ์ง„์งœ ๋ˆ์ด ๋“ค์–ด๊ฐ€๋‹ˆ๊นŒ, ํ…Œ์ŠคํŠธ๋„ท์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. Sepolia๋‚˜ Goerli ๊ฐ™์€ ํ…Œ์ŠคํŠธ๋„ท์—์„œ ๊ฐœ๋ฐœํ•˜์ž!

ํ•„์š”ํ•œ ๊ฒƒ๋“ค:
๐Ÿ”‘ MetaMask ์ง€๊ฐ‘: ํ…Œ์ŠคํŠธ ๊ณ„์ • ์ƒ์„ฑ
๐Ÿ’ง Faucet: ํ…Œ์ŠคํŠธ ETH ๋ฐ›๊ธฐ (๋ฌด๋ฃŒ!)
๐Ÿ”— Infura/Alchemy: ์ด๋”๋ฆฌ์›€ ๋…ธ๋“œ ์ ‘๊ทผ (API ํ‚ค ๋ฐœ๊ธ‰)
๐Ÿ“ Etherscan: ํŠธ๋žœ์žญ์…˜ ํ™•์ธ์šฉ

Infura๋‚˜ Alchemy์— ๊ฐ€์ž…ํ•˜๋ฉด ๋ฌด๋ฃŒ๋กœ API ํ‚ค๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. ์ด๊ฑธ ๋ผ๋ผ๋ฒจ .env ํŒŒ์ผ์— ์ €์žฅํ•ด๋‘์ž.

.env ํŒŒ์ผ ์„ค์ •
ETHEREUM_NETWORK=sepolia
INFURA_API_KEY=your_infura_key_here
CONTRACT_ADDRESS=
WALLET_PRIVATE_KEY=your_test_wallet_private_key
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜: ์ ˆ๋Œ€๋กœ ๋ฉ”์ธ๋„ท ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ์ฝ”๋“œ์— ๋„ฃ์ง€ ๋งˆ! ํ…Œ์ŠคํŠธ๋„ท ์ „์šฉ ์ง€๊ฐ‘์„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ์‹ค์ˆ˜๋กœ GitHub์— ์˜ฌ๋ฆฌ๋ฉด ํฐ์ผ๋‚˜!

๐Ÿ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์†”๋ฆฌ๋””ํ‹ฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ๊ฒฐ์ œ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ์ธ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ๐Ÿ˜Ž

๐ŸŽจ ๊ธฐ๋ณธ ๊ฒฐ์ œ ์ปจํŠธ๋ž™ํŠธ ๊ตฌ์กฐ

๋จผ์ € ๊ฐ„๋‹จํ•œ ๊ฒฐ์ œ ์ปจํŠธ๋ž™ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณผ๊ฒŒ. ์ด ์ปจํŠธ๋ž™ํŠธ๋Š” ETH๋ฅผ ๋ฐ›๊ณ , ํŒ๋งค์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๊ณ , ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋–ผ๋Š” ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด.

contracts/PaymentContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract PaymentContract {
    address public owner;
    uint256 public platformFeePercent = 3; // 3% ์ˆ˜์ˆ˜๋ฃŒ
    
    struct Payment {
        address buyer;
        address seller;
        uint256 amount;
        uint256 timestamp;
        bool completed;
        bool refunded;
    }
    
    mapping(uint256 => Payment) public payments;
    uint256 public paymentCounter;
    
    event PaymentCreated(
        uint256 indexed paymentId,
        address indexed buyer,
        address indexed seller,
        uint256 amount
    );
    
    event PaymentCompleted(uint256 indexed paymentId);
    event PaymentRefunded(uint256 indexed paymentId);
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this");
        _;
    }
    
    constructor() {
        owner = msg.sender;
    }
    
    function createPayment(address _seller) external payable returns (uint256) {
        require(msg.value > 0, "Payment amount must be greater than 0");
        require(_seller != address(0), "Invalid seller address");
        
        uint256 paymentId = paymentCounter++;
        
        payments[paymentId] = Payment({
            buyer: msg.sender,
            seller: _seller,
            amount: msg.value,
            timestamp: block.timestamp,
            completed: false,
            refunded: false
        });
        
        emit PaymentCreated(paymentId, msg.sender, _seller, msg.value);
        
        return paymentId;
    }
    
    function completePayment(uint256 _paymentId) external {
        Payment storage payment = payments[_paymentId];
        
        require(!payment.completed, "Payment already completed");
        require(!payment.refunded, "Payment was refunded");
        require(
            msg.sender == payment.seller || msg.sender == owner,
            "Only seller or owner can complete"
        );
        
        uint256 platformFee = (payment.amount * platformFeePercent) / 100;
        uint256 sellerAmount = payment.amount - platformFee;
        
        payment.completed = true;
        
        payable(payment.seller).transfer(sellerAmount);
        payable(owner).transfer(platformFee);
        
        emit PaymentCompleted(_paymentId);
    }
    
    function refundPayment(uint256 _paymentId) external onlyOwner {
        Payment storage payment = payments[_paymentId];
        
        require(!payment.completed, "Cannot refund completed payment");
        require(!payment.refunded, "Payment already refunded");
        
        payment.refunded = true;
        
        payable(payment.buyer).transfer(payment.amount);
        
        emit PaymentRefunded(_paymentId);
    }
    
    function updatePlatformFee(uint256 _newFee) external onlyOwner {
        require(_newFee <= 10, "Fee cannot exceed 10%");
        platformFeePercent = _newFee;
    }
    
    function getPayment(uint256 _paymentId) external view returns (
        address buyer,
        address seller,
        uint256 amount,
        uint256 timestamp,
        bool completed,
        bool refunded
    ) {
        Payment memory payment = payments[_paymentId];
        return (
            payment.buyer,
            payment.seller,
            payment.amount,
            payment.timestamp,
            payment.completed,
            payment.refunded
        );
    }
}

์ด ์ปจํŠธ๋ž™ํŠธ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์„ค๋ช…ํ•˜๋ฉด:

1. createPayment(): ๊ตฌ๋งค์ž๊ฐ€ ETH๋ฅผ ๋ณด๋‚ด๋ฉด์„œ ๊ฒฐ์ œ๋ฅผ ์ƒ์„ฑํ•ด. ์ด๋•Œ ํŒ๋งค์ž ์ฃผ์†Œ๋ฅผ ์ง€์ •ํ•˜๊ณ , ๋ˆ์€ ์ปจํŠธ๋ž™ํŠธ์— ์ผ๋‹จ ๋ณด๊ด€๋ผ. ์—์Šคํฌ๋กœ ๋ฐฉ์‹์ด์ง€!

2. completePayment(): ๊ฑฐ๋ž˜๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ํŒ๋งค์ž๋‚˜ ํ”Œ๋žซํผ ๊ด€๋ฆฌ์ž๊ฐ€ ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด. ๊ทธ๋Ÿฌ๋ฉด ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ์ œ์™ธํ•œ ๊ธˆ์•ก์ด ํŒ๋งค์ž์—๊ฒŒ ์ „์†ก๋ผ.

3. refundPayment(): ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ๊ด€๋ฆฌ์ž๊ฐ€ ํ™˜๋ถˆ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ๊ตฌ๋งค์ž์—๊ฒŒ ์ „์•ก ๋ฐ˜ํ™˜!

4. ์ด๋ฒคํŠธ(Event): ๋ชจ๋“  ์ค‘์š”ํ•œ ์•ก์…˜๋งˆ๋‹ค ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ. ๋ผ๋ผ๋ฒจ์—์„œ ์ด๊ฑธ ๊ฐ์ง€ํ•ด์„œ DB์— ๊ธฐ๋กํ•  ๊ฑฐ์•ผ.

๐Ÿ’ก ์‹ค๋ฌด ํŒ: ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ์ด๋ณด๋‹ค ํ›จ์”ฌ ๋ณต์žกํ•œ ๋กœ์ง์ด ํ•„์š”ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ถ€๋ถ„ ํ™˜๋ถˆ, ๋ถ„ํ•  ๊ฒฐ์ œ, ๋‹ค์ค‘ ์„œ๋ช…, ํƒ€์ž„๋ฝ ๊ฐ™์€ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋Š” ์ด๊ฒŒ ์ „๋ถ€์•ผ!

๐Ÿš€ ERC-20 ํ† ํฐ ๊ฒฐ์ œ ์ง€์›

ETH๋งŒ ๋ฐ›๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ USDT, USDC ๊ฐ™์€ ์Šคํ…Œ์ด๋ธ”์ฝ”์ธ๋„ ๋ฐ›๊ณ  ์‹ถ๋‹ค๋ฉด? ERC-20 ํ† ํฐ์„ ์ง€์›ํ•˜๋Š” ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ด!

contracts/TokenPaymentContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
}

contract TokenPaymentContract {
    address public owner;
    uint256 public platformFeePercent = 3;
    
    struct TokenPayment {
        address buyer;
        address seller;
        address tokenAddress;
        uint256 amount;
        uint256 timestamp;
        bool completed;
        bool refunded;
    }
    
    mapping(uint256 => TokenPayment) public payments;
    uint256 public paymentCounter;
    
    event TokenPaymentCreated(
        uint256 indexed paymentId,
        address indexed buyer,
        address indexed seller,
        address tokenAddress,
        uint256 amount
    );
    
    event TokenPaymentCompleted(uint256 indexed paymentId);
    event TokenPaymentRefunded(uint256 indexed paymentId);
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner");
        _;
    }
    
    constructor() {
        owner = msg.sender;
    }
    
    function createTokenPayment(
        address _seller,
        address _tokenAddress,
        uint256 _amount
    ) external returns (uint256) {
        require(_amount > 0, "Amount must be greater than 0");
        require(_seller != address(0), "Invalid seller");
        require(_tokenAddress != address(0), "Invalid token");
        
        IERC20 token = IERC20(_tokenAddress);
        require(
            token.transferFrom(msg.sender, address(this), _amount),
            "Token transfer failed"
        );
        
        uint256 paymentId = paymentCounter++;
        
        payments[paymentId] = TokenPayment({
            buyer: msg.sender,
            seller: _seller,
            tokenAddress: _tokenAddress,
            amount: _amount,
            timestamp: block.timestamp,
            completed: false,
            refunded: false
        });
        
        emit TokenPaymentCreated(paymentId, msg.sender, _seller, _tokenAddress, _amount);
        
        return paymentId;
    }
    
    function completeTokenPayment(uint256 _paymentId) external {
        TokenPayment storage payment = payments[_paymentId];
        
        require(!payment.completed, "Already completed");
        require(!payment.refunded, "Was refunded");
        require(
            msg.sender == payment.seller || msg.sender == owner,
            "Unauthorized"
        );
        
        uint256 platformFee = (payment.amount * platformFeePercent) / 100;
        uint256 sellerAmount = payment.amount - platformFee;
        
        payment.completed = true;
        
        IERC20 token = IERC20(payment.tokenAddress);
        require(token.transfer(payment.seller, sellerAmount), "Transfer to seller failed");
        require(token.transfer(owner, platformFee), "Transfer fee failed");
        
        emit TokenPaymentCompleted(_paymentId);
    }
    
    function refundTokenPayment(uint256 _paymentId) external onlyOwner {
        TokenPayment storage payment = payments[_paymentId];
        
        require(!payment.completed, "Cannot refund completed");
        require(!payment.refunded, "Already refunded");
        
        payment.refunded = true;
        
        IERC20 token = IERC20(payment.tokenAddress);
        require(token.transfer(payment.buyer, payment.amount), "Refund failed");
        
        emit TokenPaymentRefunded(_paymentId);
    }
}

ERC-20 ํ† ํฐ ๊ฒฐ์ œ์˜ ํ•ต์‹ฌ์€ transferFrom() ํ•จ์ˆ˜์•ผ. ์‚ฌ์šฉ์ž๊ฐ€ ๋จผ์ € ์ปจํŠธ๋ž™ํŠธ์— approve๋ฅผ ํ•ด์ค˜์•ผ ํ•˜๊ณ , ๊ทธ ๋‹ค์Œ์— ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ์‚ฌ์šฉ์ž ์ง€๊ฐ‘์—์„œ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. ์ด ๋ถ€๋ถ„์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด!

๐Ÿ”จ ์ปจํŠธ๋ž™ํŠธ ์ปดํŒŒ์ผ ๋ฐ ๋ฐฐํฌ

์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ์œผ๋ฉด ์ด์ œ ์ปดํŒŒ์ผํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋„ท์— ๋ฐฐํฌํ•ด์•ผ ํ•ด.

hardhat.config.js ์„ค์ •
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();

module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
};
๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
// scripts/deploy.js
const hre = require("hardhat");

async function main() {
  const PaymentContract = await hre.ethers.getContractFactory("PaymentContract");
  const payment = await PaymentContract.deploy();
  
  await payment.waitForDeployment();
  
  const address = await payment.getAddress();
  console.log("PaymentContract deployed to:", address);
  
  // TokenPaymentContract๋„ ๋ฐฐํฌ
  const TokenPaymentContract = await hre.ethers.getContractFactory("TokenPaymentContract");
  const tokenPayment = await TokenPaymentContract.deploy();
  
  await tokenPayment.waitForDeployment();
  
  const tokenAddress = await tokenPayment.getAddress();
  console.log("TokenPaymentContract deployed to:", tokenAddress);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
๋ฐฐํฌ ์‹คํ–‰
npx hardhat compile
npx hardhat run scripts/deploy.js --network sepolia

๋ฐฐํฌ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๊ฐ€ ์ถœ๋ ฅ๋ผ. ์ด ์ฃผ์†Œ๋ฅผ ๋ผ๋ผ๋ฒจ .env ํŒŒ์ผ์— ์ €์žฅํ•ด๋‘ฌ์•ผ ํ•ด!

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

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ์—์„œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ์ด๊ฒŒ ํ•ต์‹ฌ์ด๊ฑฐ๋“ ! ๐ŸŽฏ

๐Ÿ“ฆ Web3 ์„œ๋น„์Šค ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

๋จผ์ € ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ์ „์šฉ ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋†’์•„์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ์ ธ.

app/Services/Web3Service.php
<?php

namespace App\Services;

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

class Web3Service
{
    protected $web3;
    protected $contract;
    protected $contractAddress;
    protected $abi;
    
    public function __construct()
    {
        $providerUrl = 'https://sepolia.infura.io/v3/' . env('INFURA_API_KEY');
        $this->web3 = new Web3($providerUrl);
        $this->contractAddress = env('CONTRACT_ADDRESS');
        
        // ABI๋Š” ์ปดํŒŒ์ผ ํ›„ ์ƒ์„ฑ๋œ JSON ํŒŒ์ผ์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•ด
        $this->abi = json_decode(file_get_contents(storage_path('contracts/PaymentContract.json')), true);
        
        $this->contract = new Contract($providerUrl, $this->abi);
    }
    
    public function createPayment($sellerAddress, $amount)
    {
        try {
            $amountInWei = Utils::toWei($amount, 'ether');
            
            // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            $data = $this->contract->at($this->contractAddress)->getData('createPayment', $sellerAddress);
            
            return [
                'to' => $this->contractAddress,
                'data' => $data,
                'value' => '0x' . dechex($amountInWei->toString())
            ];
            
        } catch (\Exception $e) {
            Log::error('Create payment error: ' . $e->getMessage());
            throw $e;
        }
    }
    
    public function getPaymentDetails($paymentId)
    {
        try {
            $payment = null;
            
            $this->contract->at($this->contractAddress)->call('getPayment', $paymentId, function ($err, $result) use (&$payment) {
                if ($err !== null) {
                    throw new \Exception($err->getMessage());
                }
                $payment = $result;
            });
            
            return [
                'buyer' => $payment[0],
                'seller' => $payment[1],
                'amount' => Utils::fromWei($payment[2], 'ether'),
                'timestamp' => $payment[3],
                'completed' => $payment[4],
                'refunded' => $payment[5]
            ];
            
        } catch (\Exception $e) {
            Log::error('Get payment details error: ' . $e->getMessage());
            throw $e;
        }
    }
    
    public function completePayment($paymentId, $privateKey)
    {
        try {
            // ์‹ค์ œ๋กœ๋Š” ์„œ๋ฒ„์—์„œ ์ง์ ‘ ํŠธ๋žœ์žญ์…˜์„ ๋ณด๋‚ด๋Š” ๊ฑด ๋ณด์•ˆ์ƒ ์œ„ํ—˜ํ•  ์ˆ˜ ์žˆ์–ด
            // ๋Œ€์‹  ์„œ๋ช…๋œ ํŠธ๋žœ์žญ์…˜์„ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ๋‚˜์•„
            
            $data = $this->contract->at($this->contractAddress)->getData('completePayment', $paymentId);
            
            return [
                'to' => $this->contractAddress,
                'data' => $data
            ];
            
        } catch (\Exception $e) {
            Log::error('Complete payment error: ' . $e->getMessage());
            throw $e;
        }
    }
    
    public function listenToPaymentEvents($fromBlock = 'latest')
    {
        try {
            $filter = [
                'fromBlock' => $fromBlock,
                'toBlock' => 'latest',
                'address' => $this->contractAddress,
                'topics' => []
            ];
            
            $this->web3->eth->getLogs($filter, function ($err, $logs) {
                if ($err !== null) {
                    Log::error('Event listening error: ' . $err->getMessage());
                    return;
                }
                
                foreach ($logs as $log) {
                    $this->processEvent($log);
                }
            });
            
        } catch (\Exception $e) {
            Log::error('Listen to events error: ' . $e->getMessage());
            throw $e;
        }
    }
    
    protected function processEvent($log)
    {
        // ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง
        // PaymentCreated, PaymentCompleted, PaymentRefunded ๋“ฑ
        Log::info('Blockchain event received', ['log' => $log]);
    }
    
    public function getTransactionReceipt($txHash)
    {
        try {
            $receipt = null;
            
            $this->web3->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
                if ($err !== null) {
                    throw new \Exception($err->getMessage());
                }
                $receipt = $result;
            });
            
            return $receipt;
            
        } catch (\Exception $e) {
            Log::error('Get transaction receipt error: ' . $e->getMessage());
            throw $e;
        }
    }
    
    public function estimateGas($transactionData)
    {
        try {
            $gas = null;
            
            $this->web3->eth->estimateGas($transactionData, function ($err, $result) use (&$gas) {
                if ($err !== null) {
                    throw new \Exception($err->getMessage());
                }
                $gas = $result;
            });
            
            return $gas;
            
        } catch (\Exception $e) {
            Log::error('Estimate gas error: ' . $e->getMessage());
            throw $e;
        }
    }
}

์ด ์„œ๋น„์Šค ํด๋ž˜์Šค๋Š” ๋ธ”๋ก์ฒด์ธ๊ณผ์˜ ๋ชจ๋“  ์ƒํ˜ธ์ž‘์šฉ์„ ๋‹ด๋‹นํ•ด. ์ฃผ์š” ๊ธฐ๋Šฅ๋“ค์„ ์‚ดํŽด๋ณด๋ฉด:

createPayment(): ๊ฒฐ์ œ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•ด. ์‹ค์ œ ์ „์†ก์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ MetaMask๋ฅผ ํ†ตํ•ด ํ•ด.

getPaymentDetails(): ํŠน์ • ๊ฒฐ์ œ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ธ”๋ก์ฒด์ธ์—์„œ ์กฐํšŒํ•ด.

listenToPaymentEvents(): ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•ด์„œ DB์— ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ์–ด.

getTransactionReceipt(): ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธํ•ด.

estimateGas(): ๊ฐ€์Šค๋น„๋ฅผ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์–ด.

๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ ์„ค๊ณ„

๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ผ๋ผ๋ฒจ DB์—๋„ ์ €์žฅํ•ด์•ผ ํ•ด. ์™œ๋ƒํ•˜๋ฉด ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ๋Š” ๋А๋ฆฌ๊ณ  ๋น„์šฉ์ด ๋“ค๊ธฐ ๋•Œ๋ฌธ์ด์•ผ. ๋น ๋ฅธ ์กฐํšŒ๋ฅผ ์œ„ํ•ด DB์— ์บ์‹ฑํ•˜๋Š” ๊ฑฐ์ง€!

database/migrations/create_payments_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('payments', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('blockchain_payment_id')->nullable();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->foreignId('seller_id')->constrained('users')->onDelete('cascade');
            $table->string('buyer_address', 42);
            $table->string('seller_address', 42);
            $table->decimal('amount', 20, 8);
            $table->string('currency', 10)->default('ETH');
            $table->string('token_address', 42)->nullable();
            $table->string('transaction_hash', 66)->nullable();
            $table->enum('status', ['pending', 'processing', 'completed', 'refunded', 'failed'])->default('pending');
            $table->text('description')->nullable();
            $table->timestamp('completed_at')->nullable();
            $table->timestamp('refunded_at')->nullable();
            $table->timestamps();
            
            $table->index('blockchain_payment_id');
            $table->index('transaction_hash');
            $table->index('status');
        });
    }

    public function down()
    {
        Schema::dropIfExists('payments');
    }
};
app/Models/Payment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Payment extends Model
{
    protected $fillable = [
        'blockchain_payment_id',
        'user_id',
        'seller_id',
        'buyer_address',
        'seller_address',
        'amount',
        'currency',
        'token_address',
        'transaction_hash',
        'status',
        'description',
        'completed_at',
        'refunded_at'
    ];
    
    protected $casts = [
        'amount' => 'decimal:8',
        'completed_at' => 'datetime',
        'refunded_at' => 'datetime'
    ];
    
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
    
    public function seller(): BelongsTo
    {
        return $this->belongsTo(User::class, 'seller_id');
    }
    
    public function isPending(): bool
    {
        return $this->status === 'pending';
    }
    
    public function isCompleted(): bool
    {
        return $this->status === 'completed';
    }
    
    public function isRefunded(): bool
    {
        return $this->status === 'refunded';
    }
    
    public function markAsCompleted(): void
    {
        $this->update([
            'status' => 'completed',
            'completed_at' => now()
        ]);
    }
    
    public function markAsRefunded(): void
    {
        $this->update([
            'status' => 'refunded',
            'refunded_at' => now()
        ]);
    }
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ์™€ DB ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ์–ด. blockchain_payment_id๊ฐ€ ๋ธ”๋ก์ฒด์ธ์˜ ๊ฒฐ์ œ ID์™€ ๋งคํ•‘๋˜๋Š” ๊ฑฐ์ง€!

๐ŸŽฎ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„

์ด์ œ ์‹ค์ œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” REST API์•ผ.

app/Http/Controllers/PaymentController.php
<?php

namespace App\Http\Controllers;

use App\Models\Payment;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class PaymentController extends Controller
{
    protected $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        $this->web3Service = $web3Service;
    }
    
    public function createPayment(Request $request)
    {
        $validated = $request->validate([
            'seller_id' => 'required|exists:users,id',
            'seller_address' => 'required|string|size:42',
            'amount' => 'required|numeric|min:0.001',
            'currency' => 'required|in:ETH,USDT,USDC',
            'description' => 'nullable|string|max:500'
        ]);
        
        try {
            DB::beginTransaction();
            
            // DB์— ๊ฒฐ์ œ ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ
            $payment = Payment::create([
                'user_id' => Auth::id(),
                'seller_id' => $validated['seller_id'],
                'buyer_address' => $request->user()->wallet_address,
                'seller_address' => $validated['seller_address'],
                'amount' => $validated['amount'],
                'currency' => $validated['currency'],
                'status' => 'pending',
                'description' => $validated['description'] ?? null
            ]);
            
            // ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            $txData = $this->web3Service->createPayment(
                $validated['seller_address'],
                $validated['amount']
            );
            
            DB::commit();
            
            return response()->json([
                'success' => true,
                'payment_id' => $payment->id,
                'transaction_data' => $txData,
                'message' => 'Payment created. Please sign the transaction in your wallet.'
            ]);
            
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error('Payment creation failed: ' . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to create payment: ' . $e->getMessage()
            ], 500);
        }
    }
    
    public function confirmPayment(Request $request, $id)
    {
        $validated = $request->validate([
            'transaction_hash' => 'required|string|size:66',
            'blockchain_payment_id' => 'required|integer'
        ]);
        
        try {
            $payment = Payment::findOrFail($id);
            
            if ($payment->user_id !== Auth::id()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Unauthorized'
                ], 403);
            }
            
            // ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ํ™•์ธ
            $receipt = $this->web3Service->getTransactionReceipt($validated['transaction_hash']);
            
            if (!$receipt || !isset($receipt->status) || $receipt->status !== '0x1') {
                return response()->json([
                    'success' => false,
                    'message' => 'Transaction failed or not found'
                ], 400);
            }
            
            // DB ์—…๋ฐ์ดํŠธ
            $payment->update([
                'blockchain_payment_id' => $validated['blockchain_payment_id'],
                'transaction_hash' => $validated['transaction_hash'],
                'status' => 'processing'
            ]);
            
            return response()->json([
                'success' => true,
                'message' => 'Payment confirmed successfully'
            ]);
            
        } catch (\Exception $e) {
            Log::error('Payment confirmation failed: ' . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to confirm payment: ' . $e->getMessage()
            ], 500);
        }
    }
    
    public function completePayment(Request $request, $id)
    {
        try {
            $payment = Payment::findOrFail($id);
            
            // ํŒ๋งค์ž๋‚˜ ๊ด€๋ฆฌ์ž๋งŒ ์™„๋ฃŒ ๊ฐ€๋Šฅ
            if ($payment->seller_id !== Auth::id() && !Auth::user()->isAdmin()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Unauthorized'
                ], 403);
            }
            
            if ($payment->status !== 'processing') {
                return response()->json([
                    'success' => false,
                    'message' => 'Payment is not in processing status'
                ], 400);
            }
            
            // ๋ธ”๋ก์ฒด์ธ์—์„œ ์™„๋ฃŒ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            $txData = $this->web3Service->completePayment($payment->blockchain_payment_id);
            
            return response()->json([
                'success' => true,
                'transaction_data' => $txData,
                'message' => 'Please sign the completion transaction'
            ]);
            
        } catch (\Exception $e) {
            Log::error('Payment completion failed: ' . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to complete payment: ' . $e->getMessage()
            ], 500);
        }
    }
    
    public function getPaymentDetails($id)
    {
        try {
            $payment = Payment::with(['user', 'seller'])->findOrFail($id);
            
            // ๊ถŒํ•œ ์ฒดํฌ
            if ($payment->user_id !== Auth::id() && $payment->seller_id !== Auth::id() && !Auth::user()->isAdmin()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Unauthorized'
                ], 403);
            }
            
            // ๋ธ”๋ก์ฒด์ธ์—์„œ ์ตœ์‹  ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
            if ($payment->blockchain_payment_id) {
                $blockchainData = $this->web3Service->getPaymentDetails($payment->blockchain_payment_id);
                
                // DB์™€ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”
                if ($blockchainData['completed'] && !$payment->isCompleted()) {
                    $payment->markAsCompleted();
                } elseif ($blockchainData['refunded'] && !$payment->isRefunded()) {
                    $payment->markAsRefunded();
                }
            }
            
            return response()->json([
                'success' => true,
                'payment' => $payment
            ]);
            
        } catch (\Exception $e) {
            Log::error('Get payment details failed: ' . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to get payment details: ' . $e->getMessage()
            ], 500);
        }
    }
    
    public function listPayments(Request $request)
    {
        try {
            $query = Payment::with(['user', 'seller'])
                ->where(function($q) {
                    $q->where('user_id', Auth::id())
                      ->orWhere('seller_id', Auth::id());
                });
            
            if ($request->has('status')) {
                $query->where('status', $request->status);
            }
            
            if ($request->has('currency')) {
                $query->where('currency', $request->currency);
            }
            
            $payments = $query->orderBy('created_at', 'desc')
                             ->paginate(20);
            
            return response()->json([
                'success' => true,
                'payments' => $payments
            ]);
            
        } catch (\Exception $e) {
            Log::error('List payments failed: ' . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to list payments: ' . $e->getMessage()
            ], 500);
        }
    }
}

์ด ์ปจํŠธ๋กค๋Ÿฌ์˜ ํ๋ฆ„์„ ์ •๋ฆฌํ•˜๋ฉด:

1. createPayment(): DB์— ๊ฒฐ์ œ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค๊ณ , ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•ด์„œ ํ”„๋ก ํŠธ์—”๋“œ๋กœ ๋ฐ˜ํ™˜ํ•ด.

2. confirmPayment(): ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์ „์†กํ•œ ํ›„, ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๋ฅผ ๋ฐ›์•„์„œ ์˜์ˆ˜์ฆ์„ ํ™•์ธํ•˜๊ณ  DB๋ฅผ ์—…๋ฐ์ดํŠธํ•ด.

3. completePayment(): ํŒ๋งค์ž๊ฐ€ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•œ ํ›„ ๊ฒฐ์ œ๋ฅผ ์™„๋ฃŒํ•˜๋Š” ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•ด.

4. getPaymentDetails(): ๋ธ”๋ก์ฒด์ธ๊ณผ DB ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋ฉด์„œ ์ตœ์‹  ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ด.

5. listPayments(): ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์กฐํšŒํ•ด.

๐Ÿ’ก ์‹ค๋ฌด ํŒ: ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ฒดํฌํ•˜๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์ด ํ•„์š”ํ•ด. ๋ผ๋ผ๋ฒจ์˜ Queue์™€ Schedule ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด ๋ผ!

โšก ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ๋ฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ

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

๐Ÿ“จ Job ํด๋ž˜์Šค ์ƒ์„ฑ

ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ๋ฅผ ์ฒดํฌํ•˜๊ณ  DB๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” Job์„ ๋งŒ๋“ค์–ด๋ณด์ž.

app/Jobs/CheckTransactionStatus.php
<?php

namespace App\Jobs;

use App\Models\Payment;
use App\Services\Web3Service;
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 CheckTransactionStatus implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    protected $payment;
    public $tries = 5;
    public $backoff = [60, 120, 300, 600, 1200]; // ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ (์ดˆ)
    
    public function __construct(Payment $payment)
    {
        $this->payment = $payment;
    }
    
    public function handle(Web3Service $web3Service)
    {
        try {
            if (!$this->payment->transaction_hash) {
                Log::warning("Payment {$this->payment->id} has no transaction hash");
                return;
            }
            
            // ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ์กฐํšŒ
            $receipt = $web3Service->getTransactionReceipt($this->payment->transaction_hash);
            
            if (!$receipt) {
                // ์•„์ง ๋งˆ์ด๋‹ ์•ˆ๋จ, ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„
                $this->release(30);
                return;
            }
            
            // ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ
            if ($receipt->status === '0x1') {
                // ์„ฑ๊ณต
                if ($this->payment->status === 'pending') {
                    $this->payment->update(['status' => 'processing']);
                    Log::info("Payment {$this->payment->id} transaction confirmed");
                }
                
                // ๋ธ”๋ก์ฒด์ธ์—์„œ ๊ฒฐ์ œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                if ($this->payment->blockchain_payment_id) {
                    $blockchainData = $web3Service->getPaymentDetails($this->payment->blockchain_payment_id);
                    
                    // ์™„๋ฃŒ ์ƒํƒœ ๋™๊ธฐํ™”
                    if ($blockchainData['completed'] && !$this->payment->isCompleted()) {
                        $this->payment->markAsCompleted();
                        Log::info("Payment {$this->payment->id} marked as completed");
                        
                        // ์™„๋ฃŒ ์•Œ๋ฆผ ๋ฐœ์†ก
                        event(new \App\Events\PaymentCompleted($this->payment));
                    }
                    
                    // ํ™˜๋ถˆ ์ƒํƒœ ๋™๊ธฐํ™”
                    if ($blockchainData['refunded'] && !$this->payment->isRefunded()) {
                        $this->payment->markAsRefunded();
                        Log::info("Payment {$this->payment->id} marked as refunded");
                        
                        // ํ™˜๋ถˆ ์•Œ๋ฆผ ๋ฐœ์†ก
                        event(new \App\Events\PaymentRefunded($this->payment));
                    }
                }
            } else {
                // ์‹คํŒจ
                $this->payment->update(['status' => 'failed']);
                Log::error("Payment {$this->payment->id} transaction failed");
                
                // ์‹คํŒจ ์•Œ๋ฆผ ๋ฐœ์†ก
                event(new \App\Events\PaymentFailed($this->payment));
            }
            
        } catch (\Exception $e) {
            Log::error("CheckTransactionStatus job failed for payment {$this->payment->id}: " . $e->getMessage());
            
            if ($this->attempts() >= $this->tries) {
                // ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ
                $this->payment->update(['status' => 'failed']);
            } else {
                // ์žฌ์‹œ๋„
                $this->release($this->backoff[$this->attempts() - 1] ?? 600);
            }
        }
    }
}

์ด Job์€ ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ฒดํฌํ•ด์„œ DB๋ฅผ ์—…๋ฐ์ดํŠธํ•ด. ์‹คํŒจํ•˜๋ฉด ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„ํ•˜๊ณ , ์ตœ๋Œ€ 5๋ฒˆ๊นŒ์ง€ ์‹œ๋„ํ•ด.

๐ŸŽช ์ด๋ฒคํŠธ ๋ฐ ๋ฆฌ์Šค๋„ˆ ์ƒ์„ฑ

๊ฒฐ์ œ ์™„๋ฃŒ๋‚˜ ํ™˜๋ถˆ ๊ฐ™์€ ์ค‘์š”ํ•œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์•Œ๋ฆผ์„ ๋ณด๋‚ด์•ผ ํ•ด. ๋ผ๋ผ๋ฒจ์˜ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜์ž!

app/Events/PaymentCompleted.php
<?php

namespace App\Events;

use App\Models\Payment;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PaymentCompleted implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
    
    public $payment;
    
    public function __construct(Payment $payment)
    {
        $this->payment = $payment;
    }
    
    public function broadcastOn()
    {
        return [
            new Channel('payment.' . $this->payment->id),
            new Channel('user.' . $this->payment->user_id),
            new Channel('user.' . $this->payment->seller_id)
        ];
    }
    
    public function broadcastAs()
    {
        return 'payment.completed';
    }
    
    public function broadcastWith()
    {
        return [
            'payment_id' => $this->payment->id,
            'amount' => $this->payment->amount,
            'currency' => $this->payment->currency,
            'completed_at' => $this->payment->completed_at
        ];
    }
}
app/Listeners/SendPaymentCompletedNotification.php
<?php

namespace App\Listeners;

use App\Events\PaymentCompleted;
use App\Notifications\PaymentCompletedNotification;
use Illuminate\Support\Facades\Log;

class SendPaymentCompletedNotification
{
    public function handle(PaymentCompleted $event)
    {
        try {
            $payment = $event->payment;
            
            // ๊ตฌ๋งค์ž์—๊ฒŒ ์•Œ๋ฆผ
            $payment->user->notify(new PaymentCompletedNotification($payment, 'buyer'));
            
            // ํŒ๋งค์ž์—๊ฒŒ ์•Œ๋ฆผ
            $payment->seller->notify(new PaymentCompletedNotification($payment, 'seller'));
            
            Log::info("Payment completed notifications sent for payment {$payment->id}");
            
        } catch (\Exception $e) {
            Log::error("Failed to send payment completed notification: " . $e->getMessage());
        }
    }
}

์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ ์ž๋™์œผ๋กœ ์ด๋ฉ”์ผ, ํ‘ธ์‹œ ์•Œ๋ฆผ, ์›น์†Œ์ผ“ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋“ฑ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์–ด. ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์ง€!

โฐ ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ •

์ฃผ๊ธฐ์ ์œผ๋กœ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์„ค์ •ํ•˜์ž.

app/Console/Kernel.php
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        // 1๋ถ„๋งˆ๋‹ค pending ์ƒํƒœ์ธ ๊ฒฐ์ œ๋“ค์˜ ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ์ฒดํฌ
        $schedule->call(function () {
            $payments = \App\Models\Payment::where('status', 'pending')
                ->whereNotNull('transaction_hash')
                ->where('created_at', '>', now()->subHours(24))
                ->get();
            
            foreach ($payments as $payment) {
                \App\Jobs\CheckTransactionStatus::dispatch($payment);
            }
        })->everyMinute();
        
        // 5๋ถ„๋งˆ๋‹ค processing ์ƒํƒœ์ธ ๊ฒฐ์ œ๋“ค ์ฒดํฌ
        $schedule->call(function () {
            $payments = \App\Models\Payment::where('status', 'processing')
                ->where('created_at', '>', now()->subDays(7))
                ->get();
            
            foreach ($payments as $payment) {
                \App\Jobs\CheckTransactionStatus::dispatch($payment);
            }
        })->everyFiveMinutes();
        
        // 10๋ถ„๋งˆ๋‹ค ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹
        $schedule->call(function () {
            $web3Service = app(\App\Services\Web3Service::class);
            $web3Service->listenToPaymentEvents();
        })->everyTenMinutes();
        
        // ๋งค์ผ ์ž์ •์— ์˜ค๋ž˜๋œ failed ๊ฒฐ์ œ ์ •๋ฆฌ
        $schedule->call(function () {
            \App\Models\Payment::where('status', 'failed')
                ->where('created_at', '<', now()->subDays(30))
                ->delete();
        })->daily();
    }
}

์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด ์„œ๋ฒ„์˜ cron์— ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ๋“ฑ๋กํ•ด์•ผ ํ•ด:

Cron ์„ค์ •
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ž๋™์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ๋ฅผ ์ถ”์ ํ•˜๊ณ  DB๋ฅผ ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ์–ด!

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ํ†ตํ•ฉ

๋ฐฑ์—”๋“œ๊ฐ€ ์ค€๋น„๋์œผ๋‹ˆ ์ด์ œ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ MetaMask์™€ ์—ฐ๋™ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. Vue.js ์˜ˆ์ œ๋กœ ์„ค๋ช…ํ• ๊ฒŒ! ๐Ÿ’ป

๐ŸฆŠ MetaMask ์—ฐ๊ฒฐ

๋จผ์ € ์‚ฌ์šฉ์ž์˜ ์ง€๊ฐ‘์„ ์—ฐ๊ฒฐํ•˜๋Š” ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ๊ตฌํ˜„ํ•ด์•ผ ํ•ด.

composables/useWeb3.js
import { ref, computed } from 'vue';
import { ethers } from 'ethers';

export function useWeb3() {
    const account = ref(null);
    const chainId = ref(null);
    const provider = ref(null);
    const signer = ref(null);
    
    const isConnected = computed(() => !!account.value);
    
    const connectWallet = async () => {
        try {
            if (!window.ethereum) {
                throw new Error('MetaMask is not installed');
            }
            
            // ๊ณ„์ • ์š”์ฒญ
            const accounts = await window.ethereum.request({
                method: 'eth_requestAccounts'
            });
            
            account.value = accounts[0];
            
            // Provider ๋ฐ Signer ์„ค์ •
            provider.value = new ethers.BrowserProvider(window.ethereum);
            signer.value = await provider.value.getSigner();
            
            // ์ฒด์ธ ID ๊ฐ€์ ธ์˜ค๊ธฐ
            const network = await provider.value.getNetwork();
            chainId.value = network.chainId.toString();
            
            // ๊ณ„์ • ๋ณ€๊ฒฝ ๊ฐ์ง€
            window.ethereum.on('accountsChanged', (accounts) => {
                if (accounts.length === 0) {
                    disconnectWallet();
                } else {
                    account.value = accounts[0];
                }
            });
            
            // ๋„คํŠธ์›Œํฌ ๋ณ€๊ฒฝ ๊ฐ์ง€
            window.ethereum.on('chainChanged', () => {
                window.location.reload();
            });
            
            return account.value;
            
        } catch (error) {
            console.error('Failed to connect wallet:', error);
            throw error;
        }
    };
    
    const disconnectWallet = () => {
        account.value = null;
        chainId.value = null;
        provider.value = null;
        signer.value = null;
    };
    
    const switchNetwork = async (targetChainId) => {
        try {
            await window.ethereum.request({
                method: 'wallet_switchEthereumChain',
                params: [{ chainId: `0x${parseInt(targetChainId).toString(16)}` }]
            });
        } catch (error) {
            if (error.code === 4902) {
                // ๋„คํŠธ์›Œํฌ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ
                throw new Error('Please add this network to MetaMask');
            }
            throw error;
        }
    };
    
    const getBalance = async (address) => {
        try {
            if (!provider.value) {
                throw new Error('Provider not initialized');
            }
            
            const balance = await provider.value.getBalance(address || account.value);
            return ethers.formatEther(balance);
            
        } catch (error) {
            console.error('Failed to get balance:', error);
            throw error;
        }
    };
    
    return {
        account,
        chainId,
        provider,
        signer,
        isConnected,
        connectWallet,
        disconnectWallet,
        switchNetwork,
        getBalance
    };
}

์ด composable์€ MetaMask ์—ฐ๊ฒฐ, ๊ณ„์ • ๊ด€๋ฆฌ, ๋„คํŠธ์›Œํฌ ์ „ํ™˜ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. Vue ์ปดํฌ๋„ŒํŠธ์—์„œ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ’ณ ๊ฒฐ์ œ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„

์‹ค์ œ ๊ฒฐ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” Vue ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

components/PaymentForm.vue
<template>
  <div class="payment-form">
    <h2>๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ</h2>
    
    <div v-if="!isConnected" class="connect-wallet">
      <button @click="handleConnect" class="btn-primary">
        ๐ŸฆŠ MetaMask ์—ฐ๊ฒฐ
      </button>
    </div>
    
    <div v-else class="payment-details">
      <div class="wallet-info">
        <p>์—ฐ๊ฒฐ๋œ ์ง€๊ฐ‘: {{ shortenAddress(account) }}</p>
        <p>์ž”์•ก: {{ balance }} ETH</p>
      </div>
      
      <form @submit.prevent="handlePayment">
        <div class="form-group">
          <label>ํŒ๋งค์ž ์ฃผ์†Œ</label>
          <input 
            v-model="form.sellerAddress" 
            type="text" 
            placeholder="0x..."
            required
          />
        </div>
        
        <div class="form-group">
          <label>๊ฒฐ์ œ ๊ธˆ์•ก (ETH)</label>
          <input 
            v-model="form.amount" 
            type="number" 
            step="0.001"
            min="0.001"
            required
          />
        </div>
        
        <div class="form-group">
          <label>์„ค๋ช… (์„ ํƒ)</label>
          <textarea 
            v-model="form.description" 
            rows="3"
            placeholder="๊ฒฐ์ œ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”"
          ></textarea>
        </div>
        
        <div v-if="estimatedGas" class="gas-estimate">
          <p>์˜ˆ์ƒ ๊ฐ€์Šค๋น„: {{ estimatedGas }} ETH</p>
          <p>์ด ๊ธˆ์•ก: {{ totalAmount }} ETH</p>
        </div>
        
        <button 
          type="submit" 
          class="btn-primary"
          :disabled="isProcessing"
        >
          {{ isProcessing ? '์ฒ˜๋ฆฌ ์ค‘...' : '๊ฒฐ์ œํ•˜๊ธฐ' }}
        </button>
      </form>
      
      <div v-if="transactionHash" class="transaction-info">
        <p>โœ… ํŠธ๋žœ์žญ์…˜ ์ „์†ก ์™„๋ฃŒ!</p>
        <a 
          :href="`https://sepolia.etherscan.io/tx/${transactionHash}`" 
          target="_blank"
        >
          Etherscan์—์„œ ํ™•์ธ
        </a>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue';
import { ethers } from 'ethers';
import { useWeb3 } from '../composables/useWeb3';
import axios from 'axios';

const { 
  account, 
  isConnected, 
  connectWallet, 
  signer,
  getBalance 
} = useWeb3();

const form = ref({
  sellerAddress: '',
  amount: '',
  description: ''
});

const balance = ref('0');
const estimatedGas = ref(null);
const isProcessing = ref(false);
const transactionHash = ref(null);

const CONTRACT_ADDRESS = import.meta.env.VITE_CONTRACT_ADDRESS;
const CONTRACT_ABI = [
  "function createPayment(address _seller) external payable returns (uint256)",
  "event PaymentCreated(uint256 indexed paymentId, address indexed buyer, address indexed seller, uint256 amount)"
];

const totalAmount = computed(() => {
  if (!form.value.amount || !estimatedGas.value) return '0';
  return (parseFloat(form.value.amount) + parseFloat(estimatedGas.value)).toFixed(6);
});

const handleConnect = async () => {
  try {
    await connectWallet();
    await updateBalance();
  } catch (error) {
    alert('์ง€๊ฐ‘ ์—ฐ๊ฒฐ ์‹คํŒจ: ' + error.message);
  }
};

const updateBalance = async () => {
  try {
    balance.value = await getBalance();
  } catch (error) {
    console.error('Failed to get balance:', error);
  }
};

const estimateGas = async () => {
  try {
    if (!form.value.sellerAddress || !form.value.amount) return;
    
    const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer.value);
    const amountInWei = ethers.parseEther(form.value.amount);
    
    const gasEstimate = await contract.createPayment.estimateGas(
      form.value.sellerAddress,
      { value: amountInWei }
    );
    
    const gasPrice = await signer.value.provider.getFeeData();
    const gasCost = gasEstimate * gasPrice.gasPrice;
    
    estimatedGas.value = ethers.formatEther(gasCost);
    
  } catch (error) {
    console.error('Failed to estimate gas:', error);
  }
};

const handlePayment = async () => {
  try {
    isProcessing.value = true;
    
    // 1. ๋ฐฑ์—”๋“œ์— ๊ฒฐ์ œ ์ƒ์„ฑ ์š”์ฒญ
    const response = await axios.post('/api/payments', {
      seller_id: 1, // ์‹ค์ œ๋กœ๋Š” ๋™์ ์œผ๋กœ ๋ฐ›์•„์•ผ ํ•จ
      seller_address: form.value.sellerAddress,
      amount: form.value.amount,
      currency: 'ETH',
      description: form.value.description
    });
    
    const { payment_id, transaction_data } = response.data;
    
    // 2. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ˜ธ์ถœ
    const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer.value);
    
    const tx = await contract.createPayment(
      form.value.sellerAddress,
      { 
        value: ethers.parseEther(form.value.amount),
        gasLimit: 300000
      }
    );
    
    transactionHash.value = tx.hash;
    
    // 3. ํŠธ๋žœ์žญ์…˜ ๋Œ€๊ธฐ
    const receipt = await tx.wait();
    
    // 4. ์ด๋ฒคํŠธ์—์„œ paymentId ์ถ”์ถœ
    const event = receipt.logs.find(log => {
      try {
        const parsed = contract.interface.parseLog(log);
        return parsed.name === 'PaymentCreated';
      } catch {
        return false;
      }
    });
    
    let blockchainPaymentId = null;
    if (event) {
      const parsed = contract.interface.parseLog(event);
      blockchainPaymentId = parsed.args.paymentId.toString();
    }
    
    // 5. ๋ฐฑ์—”๋“œ์— ํŠธ๋žœ์žญ์…˜ ํ™•์ธ ์š”์ฒญ
    await axios.post(`/api/payments/${payment_id}/confirm`, {
      transaction_hash: tx.hash,
      blockchain_payment_id: blockchainPaymentId
    });
    
    alert('๊ฒฐ์ œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
    
    // ํผ ์ดˆ๊ธฐํ™”
    form.value = {
      sellerAddress: '',
      amount: '',
      description: ''
    };
    
    await updateBalance();
    
  } catch (error) {
    console.error('Payment failed:', error);
    alert('๊ฒฐ์ œ ์‹คํŒจ: ' + error.message);
  } finally {
    isProcessing.value = false;
  }
};

const shortenAddress = (address) => {
  if (!address) return '';
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
};

onMounted(async () => {
  if (isConnected.value) {
    await updateBalance();
  }
});
</script>

<style scoped>
.payment-form {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.wallet-info {
  background: #f0f0f0;
  padding: 15px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: 600;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.gas-estimate {
  background: #fff3cd;
  padding: 10px;
  border-radius: 4px;
  margin-bottom: 15px;
}

.btn-primary {
  width: 100%;
  padding: 12px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  cursor: pointer;
}

.btn-primary:hover {
  background: #5568d3;
}

.btn-primary:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.transaction-info {
  margin-top: 20px;
  padding: 15px;
  background: #d4edda;
  border-radius: 8px;
}

.transaction-info a {
  color: #155724;
  text-decoration: underline;
}
</style>

์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์™„์ „ํ•œ ๊ฒฐ์ œ ํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•ด:

1๏ธโƒฃ MetaMask ์—ฐ๊ฒฐ
2๏ธโƒฃ ์ž”์•ก ํ™•์ธ
3๏ธโƒฃ ๊ฐ€์Šค๋น„ ์˜ˆ์ธก
4๏ธโƒฃ ๋ฐฑ์—”๋“œ์— ๊ฒฐ์ œ ์ƒ์„ฑ
5๏ธโƒฃ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ˜ธ์ถœ
6๏ธโƒฃ ํŠธ๋žœ์žญ์…˜ ๋Œ€๊ธฐ ๋ฐ ํ™•์ธ
7๏ธโƒฃ ๋ฐฑ์—”๋“œ์— ๊ฒฐ๊ณผ ์ „์†ก

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ:
โ€ข ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ ˆ๋Œ€ ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ๋‹ค๋ฃจ์ง€ ๋งˆ!
โ€ข ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์€ ์‚ฌ์šฉ์ž๊ฐ€ MetaMask์—์„œ ์ง์ ‘ ์„œ๋ช…ํ•ด์•ผ ํ•ด
โ€ข ๋ฐฑ์—”๋“œ API๋Š” ๋ฐ˜๋“œ์‹œ ์ธ์ฆ/๊ถŒํ•œ ์ฒดํฌ๋ฅผ ํ•ด์•ผ ํ•ด
โ€ข XSS, CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด๋ฅผ ์ฒ ์ €ํžˆ ํ•ด์•ผ ํ•ด

๐Ÿ›ก๏ธ ๋ณด์•ˆ ๋ฐ ์ตœ์ ํ™”

๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์—์„œ ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด! ์‹ค์ œ ๋ˆ์ด ์˜ค๊ฐ€๋Š” ๊ฑฐ๋‹ˆ๊นŒ ์ฒ ์ €ํ•˜๊ฒŒ ์ค€๋น„ํ•ด์•ผ ํ•ด. ๐Ÿ”’

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

1. Reentrancy ๊ณต๊ฒฉ ๋ฐฉ์–ด
์™ธ๋ถ€ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์— ์ƒํƒœ๋ฅผ ๋จผ์ € ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•ด. Checks-Effects-Interactions ํŒจํ„ด์„ ๋”ฐ๋ฅด์ž!

์•ˆ์ „ํ•œ ํŒจํ„ด ์˜ˆ์‹œ
function completePayment(uint256 _paymentId) external {
    Payment storage payment = payments[_paymentId];
    
    // 1. Checks (๊ฒ€์ฆ)
    require(!payment.completed, "Already completed");
    require(msg.sender == payment.seller, "Unauthorized");
    
    // 2. Effects (์ƒํƒœ ๋ณ€๊ฒฝ)
    payment.completed = true;
    uint256 amount = payment.amount;
    
    // 3. Interactions (์™ธ๋ถ€ ํ˜ธ์ถœ)
    payable(payment.seller).transfer(amount);
}

2. Integer Overflow/Underflow ๋ฐฉ์ง€
Solidity 0.8.0 ์ด์ƒ์—์„œ๋Š” ์ž๋™์œผ๋กœ ์ฒดํฌ๋˜์ง€๋งŒ, ๋ช…์‹œ์ ์œผ๋กœ SafeMath๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์•„.

3. Access Control
OpenZeppelin์˜ Ownable, AccessControl ๊ฐ™์€ ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ž.

OpenZeppelin ์‚ฌ์šฉ ์˜ˆ์‹œ
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

contract PaymentContract is Ownable, ReentrancyGuard, Pausable {
    // nonReentrant ๋ชจ๋””ํŒŒ์ด์–ด๋กœ ์žฌ์ง„์ž… ๊ณต๊ฒฉ ๋ฐฉ์–ด
    function completePayment(uint256 _paymentId) 
        external 
        nonReentrant 
        whenNotPaused 
    {
        // ๊ตฌํ˜„...
    }
    
    // ๊ธด๊ธ‰ ์ƒํ™ฉ ์‹œ ์ปจํŠธ๋ž™ํŠธ ์ผ์‹œ ์ •์ง€
    function pause() external onlyOwner {
        _pause();
    }
    
    function unpause() external onlyOwner {
        _unpause();
    }
}

4. ๊ฐ€์Šค ์ตœ์ ํ™”
โ€ข Storage ๋ณ€์ˆ˜ ์‚ฌ์šฉ์„ ์ตœ์†Œํ™”ํ•ด (๋น„์‹ธ!)
โ€ข ๋ฐ˜๋ณต๋ฌธ์—์„œ ๋ฐฐ์—ด ๊ธธ์ด๋ฅผ ์บ์‹ฑํ•ด
โ€ข uint256 ๋Œ€์‹  uint8, uint16 ๊ฐ™์€ ์ž‘์€ ํƒ€์ž… ์‚ฌ์šฉ (packing ๊ฐ€๋Šฅํ•  ๋•Œ)
โ€ข ๋ถˆํ•„์š”ํ•œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์ค„์ด๊ธฐ

๐Ÿ”’ ๋ฐฑ์—”๋“œ ๋ณด์•ˆ

1. API ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ

๋ฏธ๋“ค์›จ์–ด ์˜ˆ์‹œ
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class VerifyPaymentOwnership
{
    public function handle(Request $request, Closure $next)
    {
        $paymentId = $request->route('id');
        $payment = \App\Models\Payment::find($paymentId);
        
        if (!$payment) {
            return response()->json(['error' => 'Payment not found'], 404);
        }
        
        $user = $request->user();
        
        // ๋ณธ์ธ ๋˜๋Š” ํŒ๋งค์ž ๋˜๋Š” ๊ด€๋ฆฌ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
        if ($payment->user_id !== $user->id && 
            $payment->seller_id !== $user->id && 
            !$user->isAdmin()) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }
        
        return $next($request);
    }
}

2. Rate Limiting
๋ธ”๋ก์ฒด์ธ API ํ˜ธ์ถœ์€ ๋น„์šฉ์ด ๋“ค ์ˆ˜ ์žˆ์œผ๋‹ˆ Rate Limiting์„ ๊ผญ ์„ค์ •ํ•ด์•ผ ํ•ด.

routes/api.php
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
    Route::post('/payments', [PaymentController::class, 'createPayment']);
    Route::get('/payments', [PaymentController::class, 'listPayments']);
});

3. ์ž…๋ ฅ ๊ฒ€์ฆ
์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ํ˜•์‹, ๊ธˆ์•ก ๋ฒ”์œ„ ๋“ฑ์„ ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•ด์•ผ ํ•ด.

์ปค์Šคํ…€ Validation Rule
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class EthereumAddress implements Rule
{
    public function passes($attribute, $value)
    {
        // 0x๋กœ ์‹œ์ž‘ํ•˜๊ณ  40์ž๋ฆฌ 16์ง„์ˆ˜์ธ์ง€ ํ™•์ธ
        return preg_match('/^0x[a-fA-F0-9]{40}$/', $value);
    }
    
    public function message()
    {
        return 'The :attribute must be a valid Ethereum address.';
    }
}

// ์‚ฌ์šฉ
$request->validate([
    'seller_address' => ['required', new EthereumAddress]
]);

4. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ
์ ˆ๋Œ€๋กœ ํ”„๋ผ์ด๋น— ํ‚ค๋‚˜ API ํ‚ค๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ! .env ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๊ณ , ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•ด.

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

1. ์บ์‹ฑ ์ „๋žต
๋ธ”๋ก์ฒด์ธ ์กฐํšŒ๋Š” ๋А๋ฆฌ๋‹ˆ๊นŒ Redis๋กœ ์บ์‹ฑํ•˜์ž!

์บ์‹ฑ ์˜ˆ์‹œ
<?php

use Illuminate\Support\Facades\Cache;

public function getPaymentDetails($paymentId)
{
    $cacheKey = "payment:{$paymentId}";
    
    return Cache::remember($cacheKey, 300, function () use ($paymentId) {
        return $this->web3Service->getPaymentDetails($paymentId);
    });
}

// ๊ฒฐ์ œ ์™„๋ฃŒ ์‹œ ์บ์‹œ ๋ฌดํšจํ™”
public function markAsCompleted()
{
    $this->update(['status' => 'completed', 'completed_at' => now()]);
    Cache::forget("payment:{$this->id}");
}

2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ
์ž์ฃผ ์กฐํšŒํ•˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด.

์ธ๋ฑ์Šค ์ถ”๊ฐ€
$table->index(['user_id', 'status']);
$table->index('transaction_hash');
$table->index('created_at');

3. ํ ์‹œ์Šคํ…œ ํ™œ์šฉ
๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ๋Š” ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋‹ˆ๊นŒ ํ๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.

config/queue.php
'connections' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
        'queue' => env('REDIS_QUEUE', 'default'),
        'retry_after' => 90,
        'block_for' => null,
    ],
],

4. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋ฉด ํšจ์œจ์ ์ด์•ผ.

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ
$payments = Payment::where('status', 'pending')
    ->whereNotNull('transaction_hash')
    ->limit(100)
    ->get();

$jobs = $payments->map(function ($payment) {
    return new CheckTransactionStatus($payment);
});

Bus::batch($jobs)->dispatch();

๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์—์„œ ํ…Œ์ŠคํŠธ๋Š” ์ •๋ง ์ค‘์š”ํ•ด! ์‹ค์ˆ˜ํ•˜๋ฉด ์ง„์งœ ๋ˆ์„ ์žƒ์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์ฒ ์ €ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•˜์ž! ๐Ÿ”ฌ

๐Ÿ”จ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ…Œ์ŠคํŠธ

Hardhat์œผ๋กœ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž. JavaScript๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์„œ ํŽธํ•ด!

test/PaymentContract.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("PaymentContract", function () {
    let paymentContract;
    let owner, buyer, seller;
    
    beforeEach(async function () {
        [owner, buyer, seller] = await ethers.getSigners();
        
        const PaymentContract = await ethers.getContractFactory("PaymentContract");
        paymentContract = await PaymentContract.deploy();
        await paymentContract.waitForDeployment();
    });
    
    describe("createPayment", function () {
        it("should create a payment successfully", async function () {
            const amount = ethers.parseEther("1.0");
            
            const tx = await paymentContract.connect(buyer).createPayment(
                seller.address,
                { value: amount }
            );
            
            const receipt = await tx.wait();
            
            // ์ด๋ฒคํŠธ ํ™•์ธ
            const event = receipt.logs.find(log => {
                try {
                    const parsed = paymentContract.interface.parseLog(log);
                    return parsed.name === 'PaymentCreated';
                } catch {
                    return false;
                }
            });
            
            expect(event).to.not.be.undefined;
            
            const parsed = paymentContract.interface.parseLog(event);
            expect(parsed.args.buyer).to.equal(buyer.address);
            expect(parsed.args.seller).to.equal(seller.address);
            expect(parsed.args.amount).to.equal(amount);
        });
        
        it("should fail with zero amount", async function () {
            await expect(
                paymentContract.connect(buyer).createPayment(seller.address, { value: 0 })
            ).to.be.revertedWith("Payment amount must be greater than 0");
        });
        
        it("should fail with invalid seller address", async function () {
            const amount = ethers.parseEther("1.0");
            
            await expect(
                paymentContract.connect(buyer).createPayment(
                    ethers.ZeroAddress,
                    { value: amount }
                )
            ).to.be.revertedWith("Invalid seller address");
        });
    });
    
    describe("completePayment", function () {
        let paymentId;
        const amount = ethers.parseEther("1.0");
        
        beforeEach(async function () {
            const tx = await paymentContract.connect(buyer).createPayment(
                seller.address,
                { value: amount }
            );
            
            const receipt = await tx.wait();
            const event = receipt.logs.find(log => {
                try {
                    const parsed = paymentContract.interface.parseLog(log);
                    return parsed.name === 'PaymentCreated';
                } catch {
                    return false;
                }
            });
            
            const parsed = paymentContract.interface.parseLog(event);
            paymentId = parsed.args.paymentId;
        });
        
        it("should complete payment and transfer funds", async function () {
            const sellerBalanceBefore = await ethers.provider.getBalance(seller.address);
            const ownerBalanceBefore = await ethers.provider.getBalance(owner.address);
            
            await paymentContract.connect(seller).completePayment(paymentId);
            
            const sellerBalanceAfter = await ethers.provider.getBalance(seller.address);
            const ownerBalanceAfter = await ethers.provider.getBalance(owner.address);
            
            const platformFee = amount * 3n / 100n;
            const sellerAmount = amount - platformFee;
            
            // ๊ฐ€์Šค๋น„ ๋•Œ๋ฌธ์— ์ •ํ™•ํ•œ ๋น„๊ต๋Š” ์–ด๋ ต์ง€๋งŒ, ๋Œ€๋žต์ ์œผ๋กœ ํ™•์ธ
            expect(sellerBalanceAfter).to.be.gt(sellerBalanceBefore);
            expect(ownerBalanceAfter).to.be.gt(ownerBalanceBefore);
        });
        
        it("should fail if already completed", async function () {
            await paymentContract.connect(seller).completePayment(paymentId);
            
            await expect(
                paymentContract.connect(seller).completePayment(paymentId)
            ).to.be.revertedWith("Payment already completed");
        });
        
        it("should fail if unauthorized", async function () {
            await expect(
                paymentContract.connect(buyer).completePayment(paymentId)
            ).to.be.revertedWith("Only seller or owner can complete");
        });
    });
    
    describe("refundPayment", function () {
        let paymentId;
        const amount = ethers.parseEther("1.0");
        
        beforeEach(async function () {
            const tx = await paymentContract.connect(buyer).createPayment(
                seller.address,
                { value: amount }
            );
            
            const receipt = await tx.wait();
            const event = receipt.logs.find(log => {
                try {
                    const parsed = paymentContract.interface.parseLog(log);
                    return parsed.name === 'PaymentCreated';
                } catch {
                    return false;
                }
            });
            
            const parsed = paymentContract.interface.parseLog(event);
            paymentId = parsed.args.paymentId;
        });
        
        it("should refund payment successfully", async function () {
            const buyerBalanceBefore = await ethers.provider.getBalance(buyer.address);
            
            await paymentContract.connect(owner).refundPayment(paymentId);
            
            const buyerBalanceAfter = await ethers.provider.getBalance(buyer.address);
            
            expect(buyerBalanceAfter).to.be.gt(buyerBalanceBefore);
        });
        
        it("should fail if not owner", async function () {
            await expect(
                paymentContract.connect(seller).refundPayment(paymentId)
            ).to.be.revertedWith("Only owner can call this");
        });
    });
});

ํ…Œ์ŠคํŠธ ์‹คํ–‰:

ํ„ฐ๋ฏธ๋„
npx hardhat test
npx hardhat coverage  # ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ

100% ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋ชฉํ‘œ๋กœ ํ•˜์ž! ํŠนํžˆ ๋ˆ๊ณผ ๊ด€๋ จ๋œ ๋กœ์ง์€ ๋ชจ๋“  ๊ฒฝ์šฐ์˜ ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด.

๐Ÿงช ๋ผ๋ผ๋ฒจ ํ…Œ์ŠคํŠธ

๋ฐฑ์—”๋“œ API๋„ ์ฒ ์ €ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด.

tests/Feature/PaymentTest.php
<?php

namespace Tests\Feature;

use App\Models\User;
use App\Models\Payment;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PaymentTest extends TestCase
{
    use RefreshDatabase;
    
    protected $user;
    protected $seller;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->user = User::factory()->create([
            'wallet_address' => '0x1234567890123456789012345678901234567890'
        ]);
        
        $this->seller = User::factory()->create([
            'wallet_address' => '0x0987654321098765432109876543210987654321'
        ]);
    }
    
    public function test_create_payment_successfully()
    {
        $response = $this->actingAs($this->user)->postJson('/api/payments', [
            'seller_id' => $this->seller->id,
            'seller_address' => $this->seller->wallet_address,
            'amount' => 1.5,
            'currency' => 'ETH',
            'description' => 'Test payment'
        ]);
        
        $response->assertStatus(200)
                 ->assertJsonStructure([
                     'success',
                     'payment_id',
                     'transaction_data',
                     'message'
                 ]);
        
        $this->assertDatabaseHas('payments', [
            'user_id' => $this->user->id,
            'seller_id' => $this->seller->id,
            'amount' => 1.5,
            'status' => 'pending'
        ]);
    }
    
    public function test_create_payment_validation_fails()
    {
        $response = $this->actingAs($this->user)->postJson('/api/payments', [
            'seller_id' => $this->seller->id,
            'seller_address' => 'invalid_address',
            'amount' => -1,
            'currency' => 'INVALID'
        ]);
        
        $response->assertStatus(422)
                 ->assertJsonValidationErrors(['seller_address', 'amount', 'currency']);
    }
    
    public function test_confirm_payment_successfully()
    {
        $payment = Payment::factory()->create([
            'user_id' => $this->user->id,
            'seller_id' => $this->seller->id,
            'status' => 'pending'
        ]);
        
        $response = $this->actingAs($this->user)->postJson("/api/payments/{$payment->id}/confirm", [
            'transaction_hash' => '0x' . str_repeat('a', 64),
            'blockchain_payment_id' => 1
        ]);
        
        $response->assertStatus(200);
        
        $payment->refresh();
        $this->assertEquals('processing', $payment->status);
        $this->assertNotNull($payment->transaction_hash);
    }
    
    public function test_unauthorized_user_cannot_confirm_payment()
    {
        $payment = Payment::factory()->create([
            'user_id' => $this->seller->id,
            'seller_id' => $this->user->id
        ]);
        
        $response = $this->actingAs($this->user)->postJson("/api/payments/{$payment->id}/confirm", [
            'transaction_hash' => '0x' . str_repeat('a', 64),
            'blockchain_payment_id' => 1
        ]);
        
        $response->assertStatus(403);
    }
    
    public function test_list_payments_returns_user_payments_only()
    {
        Payment::factory()->count(5)->create(['user_id' => $this->user->id]);
        Payment::factory()->count(3)->create(['seller_id' => $this->user->id]);
        Payment::factory()->count(2)->create(); // ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž
        
        $response = $this->actingAs($this->user)->getJson('/api/payments');
        
        $response->assertStatus(200)
                 ->assertJsonCount(8, 'payments.data');
    }
}

ํ…Œ์ŠคํŠธ ์‹คํ–‰:

ํ„ฐ๋ฏธ๋„
php artisan test
php artisan test --coverage  # ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ
๐Ÿ’ก ์‹ค๋ฌด ํŒ: CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ฉํ•ด์„œ, ๋ฐฐํฌ ์ „์— ์ž๋™์œผ๋กœ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธํ•˜์ž. GitHub Actions๋‚˜ GitLab CI๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฌ์›Œ!

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

๊ฐœ๋ฐœ์ด ๋๋‚ฌ์œผ๋ฉด ์ด์ œ ์‹ค์ œ ์„œ๋น„์Šค๋กœ ๋ฐฐํฌํ•ด์•ผ์ง€! ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์„ค์ •์„ ์•Œ์•„๋ณด์ž. ๐ŸŒ

๐Ÿ“ฆ ๋ฉ”์ธ๋„ท ๋ฐฐํฌ ์ค€๋น„

ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธํ–ˆ๋‹ค๋ฉด ์ด์ œ ๋ฉ”์ธ๋„ท์— ๋ฐฐํฌํ•  ์ฐจ๋ก€์•ผ. ํ•˜์ง€๋งŒ ์กฐ์‹ฌํ•ด์•ผ ํ•ด!

๋ฐฐํฌ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

โœ… ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐ์‚ฌ(Audit) ์™„๋ฃŒ
โœ… ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ
โœ… ๊ฐ€์Šค ์ตœ์ ํ™” ์™„๋ฃŒ
โœ… ๊ธด๊ธ‰ ์ •์ง€(Pause) ๊ธฐ๋Šฅ ๊ตฌํ˜„
โœ… ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฐ€๋Šฅ์„ฑ ๊ณ ๋ ค (Proxy ํŒจํ„ด)
โœ… ์ถฉ๋ถ„ํ•œ ETH ์ค€๋น„ (๋ฐฐํฌ ๊ฐ€์Šค๋น„)
โœ… ๋ฐฑ์—… ๋ฐ ๋กค๋ฐฑ ๊ณ„ํš ์ˆ˜๋ฆฝ

hardhat.config.js (๋ฉ”์ธ๋„ท ์„ค์ •)
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();

module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    mainnet: {
      url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
      accounts: [process.env.MAINNET_PRIVATE_KEY],
      gasPrice: 'auto',
      gas: 'auto'
    }
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
};
๋ฉ”์ธ๋„ท ๋ฐฐํฌ
# ๋ฐฐํฌ ์ „ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
npx hardhat run scripts/deploy.js --network mainnet --dry-run

# ์‹ค์ œ ๋ฐฐํฌ
npx hardhat run scripts/deploy.js --network mainnet

# Etherscan์— ์†Œ์Šค์ฝ”๋“œ ๊ฒ€์ฆ
npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS
โš ๏ธ ๋ฉ”์ธ๋„ท ๋ฐฐํฌ ์ฃผ์˜์‚ฌํ•ญ:
โ€ข ๋ฐฐํฌ๋Š” ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์–ด! ์‹ ์ค‘ํ•˜๊ฒŒ ํ•ด์•ผ ํ•ด
โ€ข ๊ฐ€์Šค๋น„๊ฐ€ ๋น„์Œ€ ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ฐ€์Šค ๊ฐ€๊ฒฉ์ด ๋‚ฎ์„ ๋•Œ ๋ฐฐํฌํ•˜์ž
โ€ข ๋ฐฐํฌ ํ›„ ๋ฐ˜๋“œ์‹œ Etherscan์—์„œ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•ด์•ผ ์‹ ๋ขฐ๋„๊ฐ€ ๋†’์•„์ ธ
โ€ข ์ฒซ ๋ฐฐํฌ ํ›„ ์ž‘์€ ๊ธˆ์•ก์œผ๋กœ ๋จผ์ € ํ…Œ์ŠคํŠธํ•ด๋ณด์ž

๐Ÿ–ฅ๏ธ ๋ผ๋ผ๋ฒจ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ

๋ฐฑ์—”๋“œ๋„ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์„ค์ •ํ•ด์•ผ ํ•ด.

.env (ํ”„๋กœ๋•์…˜)
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com

DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_DATABASE=your-database
DB_USERNAME=your-username
DB_PASSWORD=your-secure-password

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password

ETHEREUM_NETWORK=mainnet
INFURA_API_KEY=your-infura-key
CONTRACT_ADDRESS=your-deployed-contract-address

# ์ ˆ๋Œ€๋กœ ๋ฉ”์ธ๋„ท ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ์—ฌ๊ธฐ์— ๋„ฃ์ง€ ๋งˆ!
# ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์„œ๋ช…์ด ํ•„์š”ํ•˜๋ฉด AWS KMS ๊ฐ™์€ ๋ณด์•ˆ ์†”๋ฃจ์…˜ ์‚ฌ์šฉ
๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ
#!/bin/bash

# ์ €์žฅ์†Œ์—์„œ ์ตœ์‹  ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ
git pull origin main

# ์˜์กด์„ฑ ์„ค์น˜
composer install --no-dev --optimize-autoloader

# ์บ์‹œ ์ตœ์ ํ™”
php artisan config:cache
php artisan route:cache
php artisan view:cache

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰
php artisan migrate --force

# ํ ์›Œ์ปค ์žฌ์‹œ์ž‘
php artisan queue:restart

# ์Šค์ผ€์ค„๋Ÿฌ ํ™•์ธ
php artisan schedule:list

echo "Deployment completed!"

์„œ๋ฒ„ ์„ค์ •:

โ€ข Nginx/Apache: ์›น ์„œ๋ฒ„ ์„ค์ •
โ€ข PHP-FPM: PHP ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ
โ€ข Supervisor: ํ ์›Œ์ปค ๊ด€๋ฆฌ
โ€ข Redis: ์บ์‹ฑ ๋ฐ ํ
โ€ข MySQL/PostgreSQL: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
โ€ข SSL ์ธ์ฆ์„œ: HTTPS ํ•„์ˆ˜!

Supervisor ์„ค์ • (ํ ์›Œ์ปค)
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/project/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/path/to/your/project/storage/logs/worker.log
stopwaitsecs=3600

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น…

ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง์ด ์ •๋ง ์ค‘์š”ํ•ด! ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ๋น ๋ฅด๊ฒŒ ๋Œ€์‘ํ•ด์•ผ ํ•˜๊ฑฐ๋“ .

๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ:

๐Ÿ” Laravel Telescope: ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ๋””๋ฒ„๊น…
๐Ÿ“ˆ Laravel Horizon: ํ ๋ชจ๋‹ˆํ„ฐ๋ง
๐Ÿšจ Sentry: ์—๋Ÿฌ ์ถ”์ 
๐Ÿ“Š New Relic/DataDog: APM (Application Performance Monitoring)
๐Ÿ“‰ Grafana + Prometheus: ๋ฉ”ํŠธ๋ฆญ ์‹œ๊ฐํ™”
๐Ÿ”” PagerDuty/Opsgenie: ์•Œ๋ฆผ ๊ด€๋ฆฌ

Sentry ์„ค์ •
composer require sentry/sentry-laravel

# config/sentry.php
return [
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'traces_sample_rate' => 1.0,
    'environment' => env('APP_ENV'),
];
์ปค์Šคํ…€ ๋กœ๊น…
<?php

use Illuminate\Support\Facades\Log;

// ๊ฒฐ์ œ ๊ด€๋ จ ์ค‘์š” ์ด๋ฒคํŠธ ๋กœ๊น…
Log::channel('payments')->info('Payment created', [
    'payment_id' => $payment->id,
    'user_id' => $payment->user_id,
    'amount' => $payment->amount,
    'currency' => $payment->currency
]);

// ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋กœ๊น…
Log::channel('blockchain')->info('Transaction sent', [
    'tx_hash' => $txHash,
    'from' => $fromAddress,
    'to' => $toAddress,
    'value' => $value
]);

// ์—๋Ÿฌ ๋กœ๊น…
Log::channel('slack')->error('Payment processing failed', [
    'payment_id' => $payment->id,
    'error' => $exception->getMessage(),
    'trace' => $exception->getTraceAsString()
]);

๐Ÿ”„ ๋ฐฑ์—… ๋ฐ ์žฌํ•ด ๋ณต๊ตฌ

๋งŒ์•ฝ์„ ๋Œ€๋น„ํ•œ ๋ฐฑ์—… ์ „๋žต๋„ ํ•„์ˆ˜์•ผ!

๋ฐฑ์—… ์ „๋žต:

๐Ÿ’พ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: ๋งค์ผ ์ž๋™ ๋ฐฑ์—…, 7์ผ์น˜ ๋ณด๊ด€
๐Ÿ“ ํŒŒ์ผ ์‹œ์Šคํ…œ: ์ฃผ๊ธฐ์  ์Šค๋ƒ…์ƒท
๐Ÿ”‘ ํ™˜๊ฒฝ ๋ณ€์ˆ˜: ์•ˆ์ „ํ•œ ๊ณณ์— ์•”ํ˜ธํ™” ์ €์žฅ
๐Ÿ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ: ์†Œ์Šค์ฝ”๋“œ ๋ฐ ABI ๋ฒ„์ „ ๊ด€๋ฆฌ
๐Ÿ—‚๏ธ ๋กœ๊ทธ: ์žฅ๊ธฐ ๋ณด๊ด€ (์ตœ์†Œ 1๋…„)

์ž๋™ ๋ฐฑ์—… ์Šคํฌ๋ฆฝํŠธ
#!/bin/bash

DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
DB_NAME="your_database"

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—…
mysqldump -u $DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/db_$DATE.sql.gz

# ํŒŒ์ผ ๋ฐฑ์—…
tar -czf $BACKUP_DIR/files_$DATE.tar.gz /path/to/your/project/storage

# S3์— ์—…๋กœ๋“œ
aws s3 cp $BACKUP_DIR/db_$DATE.sql.gz s3://your-bucket/backups/
aws s3 cp $BACKUP_DIR/files_$DATE.tar.gz s3://your-bucket/backups/

# 7์ผ ์ด์ƒ ๋œ ๋ฐฑ์—… ์‚ญ์ œ
find $BACKUP_DIR -name "*.gz" -mtime +7 -delete

echo "Backup completed: $DATE"

๐Ÿ’ฐ ๋น„์šฉ ์ตœ์ ํ™” ๋ฐ ์ˆ˜์ต ๋ชจ๋ธ

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

โ›ฝ ๊ฐ€์Šค๋น„ ์ตœ์ ํ™” ์ „๋žต

1. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๋ฉด ๊ฐ€์Šค๋น„๋ฅผ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด.

๋ฐฐ์น˜ ๊ฒฐ์ œ ์ปจํŠธ๋ž™ํŠธ
function batchCompletePayments(uint256[] calldata _paymentIds) external onlyOwner {
    for (uint256 i = 0; i < _paymentIds.length; i++) {
        _completePayment(_paymentIds[i]);
    }
}

function _completePayment(uint256 _paymentId) internal {
    // ๊ฐœ๋ณ„ ๊ฒฐ์ œ ์™„๋ฃŒ ๋กœ์ง
}

2. Layer 2 ์†”๋ฃจ์…˜ ํ™œ์šฉ
Polygon, Arbitrum, Optimism ๊ฐ™์€ L2๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ€์Šค๋น„๊ฐ€ 1/100 ์ˆ˜์ค€์œผ๋กœ ์ค„์–ด๋“ค์–ด!

๋ฉ€ํ‹ฐ์ฒด์ธ ์ง€์›
<?php

class Web3Service
{
    protected function getProvider($network)
    {
        $providers = [
            'ethereum' => 'https://mainnet.infura.io/v3/' . env('INFURA_API_KEY'),
            'polygon' => 'https://polygon-rpc.com',
            'arbitrum' => 'https://arb1.arbitrum.io/rpc',
            'optimism' => 'https://mainnet.optimism.io'
        ];
        
        return $providers[$network] ?? $providers['ethereum'];
    }
}

3. ๊ฐ€์Šค ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง
๊ฐ€์Šค ๊ฐ€๊ฒฉ์ด ๋‚ฎ์„ ๋•Œ ํŠธ๋žœ์žญ์…˜์„ ๋ณด๋‚ด๋ฉด ๋น„์šฉ์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด.

๊ฐ€์Šค ๊ฐ€๊ฒฉ ์ฒดํฌ
public function getOptimalGasPrice()
{
    $gasPrice = null;
    
    $this->web3->eth->gasPrice(function ($err, $price) use (&$gasPrice) {
        if ($err === null) {
            $gasPrice = $price;
        }
    });
    
    // Gwei๋กœ ๋ณ€ํ™˜
    $gasPriceGwei = hexdec($gasPrice->toString()) / 1e9;
    
    // 50 Gwei ์ดํ•˜์ผ ๋•Œ๋งŒ ์ „์†ก
    if ($gasPriceGwei <= 50) {
        return $gasPrice;
    }
    
    return null; // ๊ฐ€์Šค๋น„๊ฐ€ ๋„ˆ๋ฌด ๋น„์‹ธ๋ฉด ๋Œ€๊ธฐ
}

4. ์˜คํ”„์ฒด์ธ ์ฒ˜๋ฆฌ
๋ชจ๋“  ๊ฑธ ๋ธ”๋ก์ฒด์ธ์— ์˜ฌ๋ฆด ํ•„์š”๋Š” ์—†์–ด. ์ค‘์š”ํ•œ ๊ฒƒ๋งŒ ์˜จ์ฒด์ธ์—!

๋ฐ์ดํ„ฐ ์ข…๋ฅ˜ ์ €์žฅ ์œ„์น˜ ์ด์œ 
๊ฒฐ์ œ ๊ธˆ์•ก, ์ฃผ์†Œ ์˜จ์ฒด์ธ ๋ถˆ๋ณ€์„ฑ, ํˆฌ๋ช…์„ฑ ํ•„์š”
์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์˜คํ”„์ฒด์ธ ์ž์ฃผ ๋ณ€๊ฒฝ, ๊ฐœ์ธ์ •๋ณด
์ƒํ’ˆ ์„ค๋ช…, ์ด๋ฏธ์ง€ ์˜คํ”„์ฒด์ธ/IPFS ์šฉ๋Ÿ‰ ํผ, ๊ฐ€์Šค๋น„ ์ ˆ์•ฝ
์ฑ„ํŒ…, ๋Œ“๊ธ€ ์˜คํ”„์ฒด์ธ ์‹ค์‹œ๊ฐ„์„ฑ, ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ

๐Ÿ’ต ์ˆ˜์ต ๋ชจ๋ธ

๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค๋กœ ์–ด๋–ป๊ฒŒ ์ˆ˜์ต์„ ๋‚ผ ์ˆ˜ ์žˆ์„๊นŒ?

1. ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ
๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ชจ๋ธ์ด์•ผ. ๊ฑฐ๋ž˜ ๊ธˆ์•ก์˜ ์ผ์ • ๋น„์œจ์„ ์ˆ˜์ˆ˜๋ฃŒ๋กœ ๋ฐ›๋Š” ๊ฑฐ์ง€.

์ˆ˜์ˆ˜๋ฃŒ ๊ตฌ์กฐ ์˜ˆ์‹œ
// ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์—์„œ ์ž๋™ ์ •์‚ฐ
uint256 platformFeePercent = 3; // 3%
uint256 platformFee = (amount * platformFeePercent) / 100;
uint256 sellerAmount = amount - platformFee;

payable(seller).transfer(sellerAmount);
payable(platformOwner).transfer(platformFee);

2. ํ”„๋ฆฌ๋ฏธ์—„ ๊ธฐ๋Šฅ
โ€ข ๋น ๋ฅธ ์ •์‚ฐ (์ฆ‰์‹œ ์ถœ๊ธˆ)
โ€ข ๋†’์€ ๊ฑฐ๋ž˜ ํ•œ๋„
โ€ข ์šฐ์„  ๊ณ ๊ฐ ์ง€์›
โ€ข ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ
โ€ข API ์ ‘๊ทผ

3. ๊ตฌ๋… ๋ชจ๋ธ
์›”์ •์•ก์œผ๋กœ ๋ฌด์ œํ•œ ๊ฑฐ๋ž˜ ๋˜๋Š” ๋‚ฎ์€ ์ˆ˜์ˆ˜๋ฃŒ ์ œ๊ณต

๊ตฌ๋… ๊ด€๋ฆฌ
<?php

class Subscription extends Model
{
    const PLAN_FREE = 'free';
    const PLAN_BASIC = 'basic';
    const PLAN_PRO = 'pro';
    const PLAN_ENTERPRISE = 'enterprise';
    
    public function getFeeRate()
    {
        $rates = [
            self::PLAN_FREE => 5.0,        // 5%
            self::PLAN_BASIC => 3.0,       // 3%
            self::PLAN_PRO => 2.0,         // 2%
            self::PLAN_ENTERPRISE => 1.0   // 1%
        ];
        
        return $rates[$this->plan] ?? $rates[self::PLAN_FREE];
    }
}

4. ํ† ํฐ ์ด์ฝ”๋…ธ๋ฏธ
์ž์ฒด ํ† ํฐ์„ ๋ฐœํ–‰ํ•ด์„œ ์ƒํƒœ๊ณ„๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด. ํ† ํฐ ํ™€๋”์—๊ฒŒ ์ˆ˜์ˆ˜๋ฃŒ ํ• ์ธ, ๊ฑฐ๋ฒ„๋„Œ์Šค ๊ถŒํ•œ ๋“ฑ์„ ์ œ๊ณตํ•˜๋Š” ๊ฑฐ์ง€.

๐Ÿ’ก ์žฌ๋Šฅ๋„ท ํ™œ์šฉ ํŒ: ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋ฉด, ํฌ๋ฆฌ์—์ดํ„ฐ๋“ค์—๊ฒŒ ํˆฌ๋ช…ํ•œ ์ •์‚ฐ๊ณผ ๋‚ฎ์€ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๊ฐ•์กฐํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ํ•ด์™ธ ๊ฒฐ์ œ๋‚˜ ์†Œ์•ก ๊ฒฐ์ œ์—์„œ ๊ธฐ์กด ๊ฒฐ์ œ ์‹œ์Šคํ…œ๋ณด๋‹ค ์œ ๋ฆฌํ•˜๊ฑฐ๋“ !

๐ŸŽ“ ์‹ค์ „ ์˜ˆ์ œ: ๊ฐ„๋‹จํ•œ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ๊ฐ„๋‹จํ•œ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์‹ค์ œ๋กœ ๋Œ์•„๊ฐ€๋Š” ์˜ˆ์ œ์•ผ. ๐ŸŽจ

๐Ÿ–ผ๏ธ NFT ๊ฒฐ์ œ ์ปจํŠธ๋ž™ํŠธ

contracts/NFTMarketplace.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFTMarketplace is ReentrancyGuard, Ownable {
    struct Listing {
        address seller;
        address nftContract;
        uint256 tokenId;
        uint256 price;
        bool active;
    }
    
    mapping(uint256 => Listing) public listings;
    uint256 public listingCounter;
    uint256 public platformFeePercent = 2; // 2%
    
    event NFTListed(
        uint256 indexed listingId,
        address indexed seller,
        address indexed nftContract,
        uint256 tokenId,
        uint256 price
    );
    
    event NFTSold(
        uint256 indexed listingId,
        address indexed buyer,
        address indexed seller,
        uint256 price
    );
    
    event ListingCancelled(uint256 indexed listingId);
    
    constructor() Ownable(msg.sender) {}
    
    function listNFT(
        address _nftContract,
        uint256 _tokenId,
        uint256 _price
    ) external returns (uint256) {
        require(_price > 0, "Price must be greater than 0");
        
        IERC721 nft = IERC721(_nftContract);
        require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
        require(
            nft.isApprovedForAll(msg.sender, address(this)) ||
            nft.getApproved(_tokenId) == address(this),
            "Marketplace not approved"
        );
        
        uint256 listingId = listingCounter++;
        
        listings[listingId] = Listing({
            seller: msg.sender,
            nftContract: _nftContract,
            tokenId: _tokenId,
            price: _price,
            active: true
        });
        
        emit NFTListed(listingId, msg.sender, _nftContract, _tokenId, _price);
        
        return listingId;
    }
    
    function buyNFT(uint256 _listingId) external payable nonReentrant {
        Listing storage listing = listings[_listingId];
        
        require(listing.active, "Listing not active");
        require(msg.value >= listing.price, "Insufficient payment");
        
        listing.active = false;
        
        uint256 platformFee = (listing.price * platformFeePercent) / 100;
        uint256 sellerAmount = listing.price - platformFee;
        
        IERC721(listing.nftContract).safeTransferFrom(
            listing.seller,
            msg.sender,
            listing.tokenId
        );
        
        payable(listing.seller).transfer(sellerAmount);
        payable(owner()).transfer(platformFee);
        
        if (msg.value > listing.price) {
            payable(msg.sender).transfer(msg.value - listing.price);
        }
        
        emit NFTSold(_listingId, msg.sender, listing.seller, listing.price);
    }
    
    function cancelListing(uint256 _listingId) external {
        Listing storage listing = listings[_listingId];
        
        require(listing.active, "Listing not active");
        require(listing.seller == msg.sender, "Not the seller");
        
        listing.active = false;
        
        emit ListingCancelled(_listingId);
    }
    
    function updatePlatformFee(uint256 _newFee) external onlyOwner {
        require(_newFee <= 10, "Fee too high");
        platformFeePercent = _newFee;
    }
}

๐Ÿ”ง ๋ผ๋ผ๋ฒจ ํ†ตํ•ฉ

app/Http/Controllers/NFTMarketplaceController.php
<?php

namespace App\Http\Controllers;

use App\Models\NFTListing;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class NFTMarketplaceController extends Controller
{
    protected $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        $this->web3Service = $web3Service;
    }
    
    public function listNFT(Request $request)
    {
        $validated = $request->validate([
            'nft_contract' => 'required|string|size:42',
            'token_id' => 'required|integer',
            'price' => 'required|numeric|min:0.001',
            'title' => 'required|string|max:200',
            'description' => 'nullable|string|max:1000',
            'image_url' => 'required|url'
        ]);
        
        try {
            // DB์— ๋ฆฌ์ŠคํŒ… ์ƒ์„ฑ
            $listing = NFTListing::create([
                'user_id' => Auth::id(),
                'nft_contract' => $validated['nft_contract'],
                'token_id' => $validated['token_id'],
                'price' => $validated['price'],
                'title' => $validated['title'],
                'description' => $validated['description'] ?? null,
                'image_url' => $validated['image_url'],
                'status' => 'pending'
            ]);
            
            // ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            $txData = $this->web3Service->createNFTListing(
                $validated['nft_contract'],
                $validated['token_id'],
                $validated['price']
            );
            
            return response()->json([
                'success' => true,
                'listing_id' => $listing->id,
                'transaction_data' => $txData
            ]);
            
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
    
    public function getListings(Request $request)
    {
        $query = NFTListing::where('status', 'active');
        
        if ($request->has('min_price')) {
            $query->where('price', '>=', $request->min_price);
        }
        
        if ($request->has('max_price')) {
            $query->where('price', '<=', $request->max_price);
        }
        
        if ($request->has('search')) {
            $query->where(function($q) use ($request) {
                $q->where('title', 'like', "%{$request->search}%")
                  ->orWhere('description', 'like', "%{$request->search}%");
            });
        }
        
        $listings = $query->orderBy('created_at', 'desc')
                         ->paginate(20);
        
        return response()->json([
            'success' => true,
            'listings' => $listings
        ]);
    }
    
    public function buyNFT(Request $request, $id)
    {
        try {
            $listing = NFTListing::findOrFail($id);
            
            if ($listing->status !== 'active') {
                return response()->json([
                    'success' => false,
                    'message' => 'Listing is not active'
                ], 400);
            }
            
            // ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋งค ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            $txData = $this->web3Service->buyNFT(
                $listing->blockchain_listing_id,
                $listing->price
            );
            
            return response()->json([
                'success' => true,
                'transaction_data' => $txData
            ]);
            
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
}

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ NFT ๊ฐค๋Ÿฌ๋ฆฌ

components/NFTGallery.vue
<template>
  <div class="nft-gallery">
    <div class="filters">
      <input 
        v-model="filters.search" 
        type="text" 
        placeholder="๊ฒ€์ƒ‰..."
        @input="searchNFTs"
      />
      
      <div class="price-range">
        <input 
          v-model="filters.minPrice" 
          type="number" 
          placeholder="์ตœ์†Œ ๊ฐ€๊ฒฉ"
        />
        <input 
          v-model="filters.maxPrice" 
          type="number" 
          placeholder="์ตœ๋Œ€ ๊ฐ€๊ฒฉ"
        />
        <button @click="applyFilters">์ ์šฉ</button>
      </div>
    </div>
    
    <div class="nft-grid">
      <div 
        v-for="nft in nfts" 
        :key="nft.id" 
        class="nft-card"
      >
        <img :src="nft.image_url" :alt="nft.title" />
        
        <div class="nft-info">
          <h3>{{ nft.title }}</h3>
          <p class="description">{{ nft.description }}</p>
          
          <div class="price-section">
            <span class="price">{{ nft.price }} ETH</span>
            <button 
              @click="buyNFT(nft)" 
              class="btn-buy"
              :disabled="!isConnected"
            >
              ๊ตฌ๋งคํ•˜๊ธฐ
            </button>
          </div>
        </div>
      </div>
    </div>
    
    <div v-if="loading" class="loading">
      ๋กœ๋”ฉ ์ค‘...
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useWeb3 } from '../composables/useWeb3';
import axios from 'axios';
import { ethers } from 'ethers';

const { isConnected, signer } = useWeb3();

const nfts = ref([]);
const loading = ref(false);
const filters = ref({
  search: '',
  minPrice: null,
  maxPrice: null
});

const MARKETPLACE_ADDRESS = import.meta.env.VITE_MARKETPLACE_ADDRESS;
const MARKETPLACE_ABI = [
  "function buyNFT(uint256 _listingId) external payable"
];

const loadNFTs = async () => {
  try {
    loading.value = true;
    
    const params = new URLSearchParams();
    if (filters.value.search) params.append('search', filters.value.search);
    if (filters.value.minPrice) params.append('min_price', filters.value.minPrice);
    if (filters.value.maxPrice) params.append('max_price', filters.value.maxPrice);
    
    const response = await axios.get(`/api/nft-listings?${params}`);
    nfts.value = response.data.listings.data;
    
  } catch (error) {
    console.error('Failed to load NFTs:', error);
    alert('NFT ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
  } finally {
    loading.value = false;
  }
};

const buyNFT = async (nft) => {
  try {
    if (!isConnected.value) {
      alert('๋จผ์ € ์ง€๊ฐ‘์„ ์—ฐ๊ฒฐํ•ด์ฃผ์„ธ์š”.');
      return;
    }
    
    // ๋ฐฑ์—”๋“œ์—์„œ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    const response = await axios.post(`/api/nft-listings/${nft.id}/buy`);
    const { transaction_data } = response.data;
    
    // ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ˜ธ์ถœ
    const contract = new ethers.Contract(
      MARKETPLACE_ADDRESS,
      MARKETPLACE_ABI,
      signer.value
    );
    
    const tx = await contract.buyNFT(
      nft.blockchain_listing_id,
      {
        value: ethers.parseEther(nft.price.toString()),
        gasLimit: 300000
      }
    );
    
    alert('ํŠธ๋žœ์žญ์…˜์ด ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”...');
    
    await tx.wait();
    
    alert('NFT ๊ตฌ๋งค๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
    
    // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
    await loadNFTs();
    
  } catch (error) {
    console.error('Failed to buy NFT:', error);
    alert('NFT ๊ตฌ๋งค์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: ' + error.message);
  }
};

const searchNFTs = debounce(() => {
  loadNFTs();
}, 500);

const applyFilters = () => {
  loadNFTs();
};

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

onMounted(() => {
  loadNFTs();
});
</script>

<style scoped>
.nft-gallery {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.filters {
  margin-bottom: 30px;
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
}

.filters input {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 8px;
  flex: 1;
  min-width: 200px;
}

.price-range {
  display: flex;
  gap: 10px;
}

.nft-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 25px;
}

.nft-card {
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
}

.nft-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}

.nft-card img {
  width: 100%;
  height: 280px;
  object-fit: cover;
}

.nft-info {
  padding: 15px;
}

.nft-info h3 {
  margin: 0 0 10px 0;
  font-size: 18px;
}

.description {
  color: #666;
  font-size: 14px;
  margin-bottom: 15px;
  height: 40px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.price-section {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.price {
  font-size: 20px;
  font-weight: bold;
  color: #667eea;
}

.btn-buy {
  padding: 8px 20px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

.btn-buy:hover {
  background: #5568d3;
}

.btn-buy:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.loading {
  text-align: center;
  padding: 40px;
  font-size: 18px;
  color: #666;
}
</style>

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์™„์ „ํžˆ ์ž‘๋™ํ•˜๋Š” NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๊ฐ€ ์™„์„ฑ๋ผ! ์‚ฌ์šฉ์ž๋Š” NFT๋ฅผ ๋“ฑ๋กํ•˜๊ณ , ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์€ ETH๋กœ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ์–ด. ๋ชจ๋“  ๊ฑฐ๋ž˜๋Š” ๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋ก๋˜๊ณ , ํ”Œ๋žซํผ์€ ์ž๋™์œผ๋กœ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋ฐ›์ง€. ๐Ÿ˜Ž

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋‹ค๋ค„๋ดค์–ด. ์ •๋ฆฌํ•ด๋ณผ๊นŒ? ๐Ÿ“š

โœ… ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด ๊ฒƒ๋“ค

1๏ธโƒฃ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์ถ•: ๋ผ๋ผ๋ฒจ, Hardhat, MetaMask ์„ค์ •
2๏ธโƒฃ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ: ์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๊ฒฐ์ œ ๋กœ์ง ๊ตฌํ˜„
3๏ธโƒฃ ๋ฐฑ์—”๋“œ ํ†ตํ•ฉ: Web3.php๋กœ ๋ธ”๋ก์ฒด์ธ ์—ฐ๋™
4๏ธโƒฃ ํ”„๋ก ํŠธ์—”๋“œ: Vue.js์™€ ethers.js๋กœ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค
5๏ธโƒฃ ๋ณด์•ˆ: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฐ ๋ฐฑ์—”๋“œ ๋ณด์•ˆ ๊ฐ•ํ™”
6๏ธโƒฃ ์ตœ์ ํ™”: ๊ฐ€์Šค๋น„ ์ ˆ๊ฐ ๋ฐ ์„ฑ๋Šฅ ๊ฐœ์„ 
7๏ธโƒฃ ๋ฐฐํฌ: ํ…Œ์ŠคํŠธ๋„ท์—์„œ ๋ฉ”์ธ๋„ท๊นŒ์ง€
8๏ธโƒฃ ์šด์˜: ๋ชจ๋‹ˆํ„ฐ๋ง, ๋ฐฑ์—…, ์žฌํ•ด ๋ณต๊ตฌ
9๏ธโƒฃ ์‹ค์ „ ์˜ˆ์ œ: NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ๊ตฌํ˜„

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๊ธฐ

์ด์ œ ๊ธฐ๋ณธ์€ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋‹ˆ, ๋” ๋ฐœ์ „์‹œ์ผœ๋ณด์ž!

๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€:
โ€ข ๋‹ค์ค‘ ์„œ๋ช… ์ง€๊ฐ‘ (Multisig)
โ€ข ํƒ€์ž„๋ฝ (Timelock) ๊ธฐ๋Šฅ
โ€ข DAO ๊ฑฐ๋ฒ„๋„Œ์Šค
โ€ข ์Šคํ…Œ์ดํ‚น ์‹œ์Šคํ…œ
โ€ข ํฌ๋กœ์Šค์ฒด์ธ ๋ธŒ๋ฆฟ์ง€

๋” ๊ณต๋ถ€ํ•  ๊ฒƒ๋“ค:
โ€ข Solidity ๊ณ ๊ธ‰ ํŒจํ„ด (Proxy, Diamond)
โ€ข DeFi ํ”„๋กœํ† ์ฝœ (Uniswap, Aave ๋“ฑ)
โ€ข Zero-Knowledge Proofs
โ€ข MEV (Maximal Extractable Value)
โ€ข ๋ณด์•ˆ ๊ฐ์‚ฌ (Audit) ๋ฐฉ๋ฒ•๋ก 

์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ:
โ€ข GitHub์—์„œ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ ๊ธฐ์—ฌ
โ€ข Ethereum Stack Exchange์—์„œ ์งˆ๋ฌธ/๋‹ต๋ณ€
โ€ข ํ•ด์ปคํ†ค ์ฐธ๊ฐ€
โ€ข ๋ธ”๋ก์ฒด์ธ ์ปจํผ๋Ÿฐ์Šค ์ฐธ์„

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ์กฐ์–ธ: ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด. ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๊ณผ ํŠธ๋ Œ๋“œ๋ฅผ ๊พธ์ค€ํžˆ ๋”ฐ๋ผ๊ฐ€๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ๊ทธ๋ฆฌ๊ณ  ๋ณด์•ˆ์€ ์ ˆ๋Œ€ ํƒ€ํ˜‘ํ•˜์ง€ ๋งˆ! ์‹ค์ œ ๋ˆ์ด ์˜ค๊ฐ€๋Š” ์‹œ์Šคํ…œ์ด๋‹ˆ๊นŒ ํ•ญ์ƒ ์‹ ์ค‘ํ•˜๊ฒŒ ๊ฐœ๋ฐœํ•ด์•ผ ํ•ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•˜๋ฉด, ํฌ๋ฆฌ์—์ดํ„ฐ์™€ ์‚ฌ์šฉ์ž ๋ชจ๋‘์—๊ฒŒ ๋” ํˆฌ๋ช…ํ•˜๊ณ  ๊ณต์ •ํ•œ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด. ํ™”์ดํŒ…! ๐Ÿš€
์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค ๊ฐœ๋ฐœ ์™„๋ฃŒ โœ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ โœ“ ๋ฐฑ์—”๋“œ API โœ“ ํ”„๋ก ํŠธ์—”๋“œ

์ด์ œ ๋‹น์‹ ๋„ ๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰
์‹ค์ „์—์„œ ๋ถ€๋”ชํžˆ๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ•์ด์•ผ.
์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ํ‚ค์›Œ๋‚˜๊ฐ€๋ณด์ž!

๐ŸŒŸ ์žฌ๋Šฅ๋„ท์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜,
์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์•„๋ณด๋Š” ๊ฑด ์–ด๋•Œ?
ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ์–ด! ๐Ÿš€

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

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

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

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