์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ—ณ๏ธ ๋ผ๋ผ๋ฒจ๋กœ DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ—ณ๏ธ ๋ผ๋ผ๋ฒจ๋กœ DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ๊ณผ PHP์˜ ๋งŒ๋‚จ, ํƒˆ์ค‘์•™ํ™” ๋ฏผ์ฃผ์ฃผ์˜๋ฅผ ์ฝ”๋“œ๋กœ ์‹คํ˜„ํ•˜๋‹ค! ๐Ÿš€
DAO Voting PHP Laravel WEB3 Blockchain ๐Ÿ‘ค ๐Ÿ‘ค ๐Ÿ‘ค ๐Ÿ‘ค ๐Ÿ‘ค Decentralized Voting System ํˆฌ๋ช…ํ•˜๊ณ  ๊ณต์ •ํ•œ ์˜์‚ฌ๊ฒฐ์ • ํ”Œ๋žซํผ

๐ŸŽฏ DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ, ์™œ ์ง€๊ธˆ ์ฃผ๋ชฉ๋ฐ›์„๊นŒ?

์š”์ฆ˜ ๋ธ”๋ก์ฒด์ธ ์ƒํƒœ๊ณ„์—์„œ ๊ฐ€์žฅ ํ•ซํ•œ ํ‚ค์›Œ๋“œ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ DAO(Decentralized Autonomous Organization)์•ผ. ํƒˆ์ค‘์•™ํ™” ์ž์œจ ์กฐ์ง์ด๋ผ๋Š” ๊ฑฐ์ฐฝํ•œ ์ด๋ฆ„์ด์ง€๋งŒ, ์‚ฌ์‹ค ํ•ต์‹ฌ์€ ๊ฐ„๋‹จํ•ด. "์ค‘์•™์˜ ๊ถŒ๋ ฅ ์—†์ด๋„ ๊ตฌ์„ฑ์›๋“ค์ด ํˆฌ๋ช…ํ•˜๊ฒŒ ์˜์‚ฌ๊ฒฐ์ •์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ"์ด๋ผ๋Š” ๊ฑฐ์ง€! ๐ŸŽช

์ „ํ†ต์ ์ธ ์กฐ์ง์—์„œ๋Š” CEO๋‚˜ ์ด์‚ฌํšŒ๊ฐ€ ๋ชจ๋“  ๊ฒฐ์ •์„ ๋‚ด๋ฆฌ์ž–์•„? ๊ทผ๋ฐ DAO๋Š” ๋‹ฌ๋ผ. ํ† ํฐ์„ ๋ณด์œ ํ•œ ๋ชจ๋“  ๊ตฌ์„ฑ์›์ด ํˆฌํ‘œ๊ถŒ์„ ๊ฐ€์ง€๊ณ , ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ž๋™์œผ๋กœ ์‹คํ–‰ํ•ด. ์™„์ „ ๋ฏผ์ฃผ์ ์ด๋ฉด์„œ๋„ ํšจ์œจ์ ์ด์ง€! ๐Ÿ’ช

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

๐Ÿ’ก ์‹ค์ œ ํ™œ์šฉ ์‚ฌ๋ก€๋“ค

โ€ข MakerDAO: ์Šคํ…Œ์ด๋ธ”์ฝ”์ธ DAI์˜ ๊ธˆ๋ฆฌ์™€ ๋‹ด๋ณด ๋น„์œจ์„ ํˆฌํ‘œ๋กœ ๊ฒฐ์ •
โ€ข Uniswap: ํ”„๋กœํ† ์ฝœ ์—…๊ทธ๋ ˆ์ด๋“œ์™€ ์ˆ˜์ˆ˜๋ฃŒ ์ •์ฑ…์„ ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ๊ฒฐ์ •
โ€ข Decentraland: ๊ฐ€์ƒ์„ธ๊ณ„์˜ ์ •์ฑ…๊ณผ ๊ฐœ๋ฐœ ๋ฐฉํ–ฅ์„ ํ† ํฐ ํ™€๋”๋“ค์ด ํˆฌํ‘œ
โ€ข ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ๋„ ํ–ฅํ›„ ๊ฑฐ๋ฒ„๋„Œ์Šค ํ† ํฐ์„ ๋„์ž…ํ•˜๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ์ง์ ‘ ํ”Œ๋žซํผ ์ •์ฑ…์„ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€? ๐ŸŽจ

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

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

๐Ÿ“ 3-Tier ์•„ํ‚คํ…์ฒ˜

1 ํ”„๋ก ํŠธ์—”๋“œ ๋ ˆ์ด์–ด (Vue.js + Web3.js)

์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ถ€๋ถ„์ด์•ผ. ์ œ์•ˆ์„œ๋ฅผ ๋ณด๊ณ , ํˆฌํ‘œํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋Š” UI๋ฅผ ์ œ๊ณตํ•ด. Web3.js๋ฅผ ํ†ตํ•ด ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๊ฐ™์€ ์ง€๊ฐ‘๊ณผ ์—ฐ๊ฒฐ๋˜์ง€. ๋ผ๋ผ๋ฒจ์˜ Blade ํ…œํ”Œ๋ฆฟ๊ณผ Vue.js๋ฅผ ์กฐํ•ฉํ•˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ
2 ๋ฐฑ์—”๋“œ ๋ ˆ์ด์–ด (Laravel + MySQL)

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ํ•ต์‹ฌ์ด์•ผ. ์‚ฌ์šฉ์ž ์ธ์ฆ, ์ œ์•ˆ์„œ ๊ด€๋ฆฌ, ํˆฌํ‘œ ๋ฐ์ดํ„ฐ ์บ์‹ฑ, ์•Œ๋ฆผ ๋ฐœ์†ก ๋“ฑ์„ ๋‹ด๋‹นํ•ด. ๋ชจ๋“  ํˆฌํ‘œ๋ฅผ ๋ธ”๋ก์ฒด์ธ์— ์ง์ ‘ ๊ธฐ๋กํ•˜๋ฉด ๋น„์šฉ์ด ๋„ˆ๋ฌด ๋น„์‹ธ๋‹ˆ๊นŒ, ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์˜จ์ฒด์ธ์— ์˜ฌ๋ฆฌ๊ณ  ๋‚˜๋จธ์ง€๋Š” ์˜คํ”„์ฒด์ธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑฐ์ง€. ๋ผ๋ผ๋ฒจ์˜ Eloquent ORM๊ณผ Queue ์‹œ์Šคํ…œ์ด ์—ฌ๊ธฐ์„œ ๋น›์„ ๋ฐœํ•ด! โœจ
3 ๋ธ”๋ก์ฒด์ธ ๋ ˆ์ด์–ด (Ethereum/Polygon + Smart Contract)

ํˆฌ๋ช…์„ฑ๊ณผ ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์ง„์‹ค์˜ ์›์ฒœ์ด์•ผ. ์ตœ์ข… ํˆฌํ‘œ ๊ฒฐ๊ณผ, ์ œ์•ˆ์„œ ํ•ด์‹œ, ์ค‘์š”ํ•œ ๊ฑฐ๋ฒ„๋„Œ์Šค ๊ฒฐ์ •๋“ค์ด ์—ฌ๊ธฐ ๊ธฐ๋ก๋ผ. Solidity๋กœ ์ž‘์„ฑ๋œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ํˆฌํ‘œ ๊ทœ์น™์„ ๊ฐ•์ œํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ ์ž๋™์œผ๋กœ ์‹คํ–‰ํ•ด. ์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์€ ๋น„์‹ธ๋‹ˆ๊นŒ Polygon ๊ฐ™์€ L2 ์†”๋ฃจ์…˜์„ ์“ฐ๋Š” ๊ฒŒ ํ˜„๋ช…ํ•ด! ๐Ÿ”—
๐ŸŽ“ Pro Tip: ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•œ ํƒˆ์ค‘์•™ํ™”๋ฅผ ๋ชฉํ‘œ๋กœ ํ•˜์ง€ ๋งˆ! ์ ์ง„์ ์œผ๋กœ ํƒˆ์ค‘์•™ํ™”ํ•˜๋Š” ๊ฒŒ ํ˜„์‹ค์ ์ด์•ผ. ์ดˆ๊ธฐ์—๋Š” ์˜คํ”„์ฒด์ธ ๋น„์ค‘์„ ๋†’์ด๊ณ , ์‹œ์Šคํ…œ์ด ์•ˆ์ •ํ™”๋˜๋ฉด ์ ์  ๋” ๋งŽ์€ ๋ถ€๋ถ„์„ ์˜จ์ฒด์ธ์œผ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฑฐ์ง€. ์ด๊ฑธ "Progressive Decentralization"์ด๋ผ๊ณ  ํ•ด. ๐Ÿ“ˆ

๐Ÿ”„ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ดํ•ดํ•˜๊ธฐ

ํˆฌํ‘œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋Š”์ง€ ๋‹จ๊ณ„๋ณ„๋กœ ์‚ดํŽด๋ณด์ž:

1๋‹จ๊ณ„: ์ œ์•ˆ์„œ ์ƒ์„ฑ ๐Ÿ“
์‚ฌ์šฉ์ž๊ฐ€ ์ œ์•ˆ์„œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด, ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ๊ฐ€ MySQL์— ์ €์žฅํ•ด. ๋™์‹œ์— ์ œ์•ˆ์„œ ๋‚ด์šฉ์˜ ํ•ด์‹œ๊ฐ’์„ ์ƒ์„ฑํ•ด์„œ IPFS์— ์—…๋กœ๋“œํ•˜๊ณ , ๊ทธ CID(Content Identifier)๋ฅผ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์— ๊ธฐ๋กํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ œ์•ˆ์„œ๊ฐ€ ์œ„๋ณ€์กฐ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฑธ ์ฆ๋ช…ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ”

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

3๋‹จ๊ณ„: ๊ฒฐ๊ณผ ์ง‘๊ณ„ ๐Ÿ“Š
ํˆฌํ‘œ ๊ธฐ๊ฐ„์ด ๋๋‚˜๋ฉด, ๋ผ๋ผ๋ฒจ์˜ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ง‘๊ณ„ํ•ด. ๋ธ”๋ก์ฒด์ธ์—์„œ ๋ชจ๋“  ํˆฌํ‘œ ์ด๋ฒคํŠธ๋ฅผ ์ฝ์–ด์™€์„œ MySQL์— ์บ์‹ฑํ•˜๊ณ , ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์— ๊ธฐ๋กํ•ด. ์ด๋•Œ Merkle Tree๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ€์Šค๋น„๋ฅผ ํฌ๊ฒŒ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŒณ

4๋‹จ๊ณ„: ์ž๋™ ์‹คํ–‰ โšก
์ œ์•ˆ์ด ํ†ต๊ณผ๋˜๋ฉด, ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ๋ฏธ๋ฆฌ ์ •์˜๋œ ์•ก์…˜์„ ์ž๋™์œผ๋กœ ์‹คํ–‰ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด "ํ”„๋กœํ† ์ฝœ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ 2%์—์„œ 1.5%๋กœ ๋ณ€๊ฒฝ"์ด๋ผ๋Š” ์ œ์•ˆ์ด ํ†ต๊ณผ๋˜๋ฉด, ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ์ฆ‰์‹œ ์ˆ˜์ˆ˜๋ฃŒ ๋ณ€์ˆ˜๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฑฐ์ง€. ์‚ฌ๋žŒ์˜ ๊ฐœ์ž…์ด ์ „ํ˜€ ํ•„์š” ์—†์–ด! ๐Ÿค–

โš™๏ธ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ ์„ธํŒ…ํ•˜๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ๋จผ์ € ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ๋ฅผ ์„ธํŒ…ํ•˜๊ณ  ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์„ค์น˜ํ•˜์ž. ๐Ÿ› ๏ธ

๐Ÿš€ ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”

// Composer๋กœ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
composer create-project laravel/laravel dao-voting-system

cd dao-voting-system

// ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜
composer require web3p/web3.php
composer require kornrunner/keccak
composer require simplito/elliptic-php
composer require phpseclib/phpseclib

// ํ”„๋ก ํŠธ์—”๋“œ ์˜์กด์„ฑ
npm install web3 ethers @metamask/detect-provider
npm install vue@next @vitejs/plugin-vue
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: web3.php ํŒจํ‚ค์ง€๋Š” PHP 8.0 ์ด์ƒ์—์„œ๋งŒ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•ด. ๊ทธ๋ฆฌ๊ณ  GMP ํ™•์žฅ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด์•ผ ํ•ด. php -m | grep gmp ๋ช…๋ น์–ด๋กœ ํ™•์ธํ•ด๋ณด์ž!

๐Ÿ”ง ํ™˜๊ฒฝ ์„ค์ •

.env ํŒŒ์ผ์— ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜์ž:

// .env ํŒŒ์ผ
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dao_voting
DB_USERNAME=root
DB_PASSWORD=

// ๋ธ”๋ก์ฒด์ธ ์„ค์ •
BLOCKCHAIN_NETWORK=polygon_mumbai
BLOCKCHAIN_RPC_URL=https://rpc-mumbai.maticvigil.com
BLOCKCHAIN_CHAIN_ID=80001

// ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ
VOTING_CONTRACT_ADDRESS=0x...
GOVERNANCE_TOKEN_ADDRESS=0x...

// IPFS ์„ค์ •
IPFS_GATEWAY=https://ipfs.io/ipfs/
IPFS_API_URL=https://api.pinata.cloud

// ๊ด€๋ฆฌ์ž ์ง€๊ฐ‘ (์„œ๋ฒ„ ์‚ฌ์ด๋“œ ํŠธ๋žœ์žญ์…˜์šฉ)
ADMIN_WALLET_ADDRESS=0x...
ADMIN_WALLET_PRIVATE_KEY=...
๐Ÿ’ก ๊ฐœ๋ฐœ ํŒ: ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ ˆ๋Œ€ ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ .env ํŒŒ์ผ์— ํ‰๋ฌธ์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋ผ! AWS KMS๋‚˜ HashiCorp Vault ๊ฐ™์€ ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜์ž. ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ๋Š” ํ…Œ์ŠคํŠธ๋„ท ์ง€๊ฐ‘์„ ์‚ฌ์šฉํ•˜๊ณ , ์ ˆ๋Œ€ ์‹ค์ œ ์ž์‚ฐ์ด ์žˆ๋Š” ์ง€๊ฐ‘์„ ์“ฐ์ง€ ๋ง ๊ฒƒ! ๐Ÿ”’

๐Ÿ“ฆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

ํˆฌํ‘œ ์‹œ์Šคํ…œ์— ํ•„์š”ํ•œ ํ…Œ์ด๋ธ”๋“ค์„ ๋งŒ๋“ค์–ด๋ณด์ž. ๋ผ๋ผ๋ฒจ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋ฅผ ๋ฒ„์ „ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ •๋ง ํŽธํ•ด! ๐Ÿ—„๏ธ

// ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ
php artisan make:migration create_proposals_table
php artisan make:migration create_votes_table
php artisan make:migration create_vote_snapshots_table

๊ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ž‘์„ฑํ•ด๋ณด์ž:

// database/migrations/xxxx_create_proposals_table.php
public function up()
{
    Schema::create('proposals', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->string('ipfs_hash')->nullable(); // IPFS CID
        $table->string('blockchain_tx_hash')->nullable();
        $table->unsignedBigInteger('proposal_id_onchain')->nullable();
        
        $table->string('proposer_address', 42);
        $table->enum('status', ['pending', 'active', 'passed', 'rejected', 'executed'])
              ->default('pending');
        
        $table->timestamp('voting_starts_at');
        $table->timestamp('voting_ends_at');
        
        $table->decimal('quorum_required', 5, 2)->default(10.00); // 10%
        $table->decimal('approval_threshold', 5, 2)->default(50.00); // 50%
        
        $table->bigInteger('votes_for')->default(0);
        $table->bigInteger('votes_against')->default(0);
        $table->bigInteger('votes_abstain')->default(0);
        
        $table->json('metadata')->nullable();
        $table->timestamps();
        $table->softDeletes();
        
        $table->index('proposer_address');
        $table->index('status');
        $table->index(['voting_starts_at', 'voting_ends_at']);
    });
}
// database/migrations/xxxx_create_votes_table.php
public function up()
{
    Schema::create('votes', function (Blueprint $table) {
        $table->id();
        $table->foreignId('proposal_id')->constrained()->onDelete('cascade');
        
        $table->string('voter_address', 42);
        $table->enum('vote_choice', ['for', 'against', 'abstain']);
        $table->bigInteger('voting_power'); // ํ† ํฐ ๋ณด์œ ๋Ÿ‰
        
        $table->string('blockchain_tx_hash')->unique();
        $table->unsignedBigInteger('block_number');
        $table->timestamp('voted_at');
        
        $table->string('signature')->nullable(); // ์˜คํ”„์ฒด์ธ ์„œ๋ช…
        $table->json('metadata')->nullable();
        
        $table->timestamps();
        
        $table->unique(['proposal_id', 'voter_address']);
        $table->index('voter_address');
        $table->index('blockchain_tx_hash');
    });
}
// database/migrations/xxxx_create_vote_snapshots_table.php
public function up()
{
    Schema::create('vote_snapshots', function (Blueprint $table) {
        $table->id();
        $table->foreignId('proposal_id')->constrained()->onDelete('cascade');
        
        $table->string('voter_address', 42);
        $table->bigInteger('token_balance');
        $table->unsignedBigInteger('snapshot_block');
        
        $table->timestamps();
        
        $table->unique(['proposal_id', 'voter_address']);
        $table->index('snapshot_block');
    });
}

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜์ž:

php artisan migrate
๐ŸŽฏ ํ…Œ์ด๋ธ” ์„ค๊ณ„ ํฌ์ธํŠธ

โ€ข proposals ํ…Œ์ด๋ธ”: ์ œ์•ˆ์„œ์˜ ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ €์žฅ. IPFS ํ•ด์‹œ์™€ ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๋ฅผ ํ•จ๊ป˜ ์ €์žฅํ•ด์„œ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•ด.

โ€ข votes ํ…Œ์ด๋ธ”: ๊ฐœ๋ณ„ ํˆฌํ‘œ ๊ธฐ๋ก. ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ ์ œ์•ˆ์— ์ค‘๋ณต ํˆฌํ‘œํ•˜์ง€ ๋ชปํ•˜๋„๋ก unique ์ œ์•ฝ์กฐ๊ฑด์„ ๊ฑธ์—ˆ์–ด.

โ€ข vote_snapshots ํ…Œ์ด๋ธ”: ํˆฌํ‘œ ์‹œ์ ์˜ ํ† ํฐ ๋ณด์œ ๋Ÿ‰์„ ๊ธฐ๋ก. ์ด๊ฒŒ ์—†์œผ๋ฉด ํˆฌํ‘œ ์ค‘์— ํ† ํฐ์„ ์‚ฌ๊ณ ํŒ”์•„์„œ ํˆฌํ‘œ๊ถŒ์„ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ“ธ

๐ŸŽจ Eloquent ๋ชจ๋ธ ๋งŒ๋“ค๊ธฐ

๋ผ๋ผ๋ฒจ์˜ Eloquent ORM์€ ์ •๋ง ๊ฐ•๋ ฅํ•ด! ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์„ ๊ฐ์ฒด์ง€ํ–ฅ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์šฐ๋ฆฌ ํˆฌํ‘œ ์‹œ์Šคํ…œ์— ํ•„์š”ํ•œ ๋ชจ๋ธ๋“ค์„ ๋งŒ๋“ค์–ด๋ณด์ž. ๐Ÿ›๏ธ

// ๋ชจ๋ธ ์ƒ์„ฑ
php artisan make:model Proposal
php artisan make:model Vote
php artisan make:model VoteSnapshot

๐Ÿ“ Proposal ๋ชจ๋ธ

// app/Models/Proposal.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Carbon\Carbon;

class Proposal extends Model
{
    use SoftDeletes;
    
    protected $fillable = [
        'title',
        'description',
        'ipfs_hash',
        'blockchain_tx_hash',
        'proposal_id_onchain',
        'proposer_address',
        'status',
        'voting_starts_at',
        'voting_ends_at',
        'quorum_required',
        'approval_threshold',
        'votes_for',
        'votes_against',
        'votes_abstain',
        'metadata'
    ];
    
    protected $casts = [
        'voting_starts_at' => 'datetime',
        'voting_ends_at' => 'datetime',
        'metadata' => 'array',
        'quorum_required' => 'decimal:2',
        'approval_threshold' => 'decimal:2'
    ];
    
    // ๊ด€๊ณ„ ์ •์˜
    public function votes()
    {
        return $this->hasMany(Vote::class);
    }
    
    public function snapshots()
    {
        return $this->hasMany(VoteSnapshot::class);
    }
    
    // ํˆฌํ‘œ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ์ฒดํฌ
    public function isVotingActive(): bool
    {
        $now = Carbon::now();
        return $this->status === 'active' 
            && $now->between($this->voting_starts_at, $this->voting_ends_at);
    }
    
    // ์ •์กฑ์ˆ˜ ๋‹ฌ์„ฑ ์—ฌ๋ถ€
    public function hasReachedQuorum(): bool
    {
        $totalVotes = $this->votes_for + $this->votes_against + $this->votes_abstain;
        $totalSupply = $this->getTotalTokenSupply();
        
        if ($totalSupply == 0) return false;
        
        $participationRate = ($totalVotes / $totalSupply) * 100;
        return $participationRate >= $this->quorum_required;
    }
    
    // ์Šน์ธ ์—ฌ๋ถ€
    public function isApproved(): bool
    {
        $totalVotes = $this->votes_for + $this->votes_against;
        
        if ($totalVotes == 0) return false;
        
        $approvalRate = ($this->votes_for / $totalVotes) * 100;
        return $approvalRate >= $this->approval_threshold;
    }
    
    // ์ตœ์ข… ๊ฒฐ๊ณผ ๊ณ„์‚ฐ
    public function calculateResult(): string
    {
        if (!$this->hasReachedQuorum()) {
            return 'rejected'; // ์ •์กฑ์ˆ˜ ๋ฏธ๋‹ฌ
        }
        
        return $this->isApproved() ? 'passed' : 'rejected';
    }
    
    // ์ด ํ† ํฐ ๊ณต๊ธ‰๋Ÿ‰ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ธ”๋ก์ฒด์ธ์—์„œ)
    protected function getTotalTokenSupply(): int
    {
        // ์ด ๋ถ€๋ถ„์€ ๋‚˜์ค‘์— Web3 ์„œ๋น„์Šค๋กœ ๊ตฌํ˜„
        return cache()->remember(
            'total_token_supply',
            3600,
            fn() => app('App\Services\Web3Service')->getTotalSupply()
        );
    }
    
    // Accessor: ์ง„ํ–‰๋ฅ 
    public function getProgressPercentageAttribute(): float
    {
        $now = Carbon::now();
        
        if ($now->lt($this->voting_starts_at)) {
            return 0;
        }
        
        if ($now->gt($this->voting_ends_at)) {
            return 100;
        }
        
        $total = $this->voting_ends_at->diffInSeconds($this->voting_starts_at);
        $elapsed = $now->diffInSeconds($this->voting_starts_at);
        
        return ($elapsed / $total) * 100;
    }
    
    // Scope: ํ™œ์„ฑ ์ œ์•ˆ๋งŒ
    public function scopeActive($query)
    {
        return $query->where('status', 'active')
                    ->where('voting_starts_at', '<=', Carbon::now())
                    ->where('voting_ends_at', '>=', Carbon::now());
    }
    
    // Scope: ์ข…๋ฃŒ๋œ ์ œ์•ˆ
    public function scopeEnded($query)
    {
        return $query->where('voting_ends_at', '<', Carbon::now())
                    ->whereIn('status', ['active', 'pending']);
    }
}

๐Ÿ—ณ๏ธ Vote ๋ชจ๋ธ

// app/Models/Vote.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Vote extends Model
{
    protected $fillable = [
        'proposal_id',
        'voter_address',
        'vote_choice',
        'voting_power',
        'blockchain_tx_hash',
        'block_number',
        'voted_at',
        'signature',
        'metadata'
    ];
    
    protected $casts = [
        'voted_at' => 'datetime',
        'metadata' => 'array'
    ];
    
    // ๊ด€๊ณ„
    public function proposal()
    {
        return $this->belongsTo(Proposal::class);
    }
    
    // ํˆฌํ‘œ ์„ ํƒ์„ ์ด๋ชจ์ง€๋กœ ํ‘œ์‹œ
    public function getVoteEmojiAttribute(): string
    {
        return match($this->vote_choice) {
            'for' => 'โœ…',
            'against' => 'โŒ',
            'abstain' => 'โธ๏ธ',
            default => 'โ“'
        };
    }
    
    // ๋ธ”๋ก์ฒด์ธ ํƒ์ƒ‰๊ธฐ ๋งํฌ
    public function getExplorerUrlAttribute(): string
    {
        $network = config('blockchain.network');
        $baseUrl = match($network) {
            'polygon_mumbai' => 'https://mumbai.polygonscan.com',
            'polygon_mainnet' => 'https://polygonscan.com',
            'ethereum_mainnet' => 'https://etherscan.io',
            default => 'https://etherscan.io'
        };
        
        return "{$baseUrl}/tx/{$this->blockchain_tx_hash}";
    }
}

๐Ÿ“ธ VoteSnapshot ๋ชจ๋ธ

// app/Models/VoteSnapshot.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class VoteSnapshot extends Model
{
    protected $fillable = [
        'proposal_id',
        'voter_address',
        'token_balance',
        'snapshot_block'
    ];
    
    public function proposal()
    {
        return $this->belongsTo(Proposal::class);
    }
    
    // ํŠน์ • ์ฃผ์†Œ์˜ ํˆฌํ‘œ๊ถŒ ์กฐํšŒ
    public static function getVotingPower(int $proposalId, string $address): int
    {
        $snapshot = static::where('proposal_id', $proposalId)
                          ->where('voter_address', $address)
                          ->first();
        
        return $snapshot ? $snapshot->token_balance : 0;
    }
}
๐ŸŽ“ ๋ชจ๋ธ ์„ค๊ณ„ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

โ€ข Accessor & Mutator: ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์ €์žฅํ•  ๋•Œ ์ž๋™์œผ๋กœ ๋ณ€ํ™˜. getProgressPercentageAttribute() ๊ฐ™์€ ๋ฉ”์„œ๋“œ๊ฐ€ ๊ทธ ์˜ˆ์‹œ์•ผ.

โ€ข Scope: ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์–ด. Proposal::active()->get() ์ด๋ ‡๊ฒŒ ๊น”๋”ํ•˜๊ฒŒ ์“ธ ์ˆ˜ ์žˆ์ง€!

โ€ข ๊ด€๊ณ„ ์ •์˜: hasMany, belongsTo ๊ฐ™์€ ๊ด€๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ์ •์˜ํ•˜๋ฉด ์กฐ์ธ ์ฟผ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜. ๐Ÿ”—

๐ŸŒ Web3 ์„œ๋น„์Šค ๊ตฌ์ถ•ํ•˜๊ธฐ

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

โšก Web3Service ํด๋ž˜์Šค

// app/Services/Web3Service.php
namespace App\Services;

use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Exception;

class Web3Service
{
    protected $web3;
    protected $eth;
    protected $votingContract;
    protected $tokenContract;
    
    public function __construct()
    {
        $rpcUrl = config('blockchain.rpc_url');
        $this->web3 = new Web3($rpcUrl);
        $this->eth = $this->web3->eth;
        
        // ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ดˆ๊ธฐํ™”
        $this->initializeContracts();
    }
    
    protected function initializeContracts()
    {
        // Voting Contract ABI ๋กœ๋“œ
        $votingAbi = json_decode(
            file_get_contents(storage_path('contracts/VotingContract.json')),
            true
        );
        
        $this->votingContract = new Contract(
            $this->web3->provider,
            $votingAbi
        );
        
        $this->votingContract->at(config('blockchain.voting_contract_address'));
        
        // Token Contract ์ดˆ๊ธฐํ™”
        $tokenAbi = json_decode(
            file_get_contents(storage_path('contracts/GovernanceToken.json')),
            true
        );
        
        $this->tokenContract = new Contract(
            $this->web3->provider,
            $tokenAbi
        );
        
        $this->tokenContract->at(config('blockchain.token_contract_address'));
    }
    
    // ํ† ํฐ ์ž”์•ก ์กฐํšŒ
    public function getTokenBalance(string $address): int
    {
        $balance = 0;
        
        $this->tokenContract->call('balanceOf', $address, function ($err, $result) use (&$balance) {
            if ($err) {
                throw new Exception("Failed to get token balance: " . $err->getMessage());
            }
            $balance = hexdec($result[0]->toString());
        });
        
        return $balance;
    }
    
    // ํŠน์ • ๋ธ”๋ก์˜ ํ† ํฐ ์ž”์•ก ์กฐํšŒ (์Šค๋ƒ…์ƒท์šฉ)
    public function getTokenBalanceAtBlock(string $address, int $blockNumber): int
    {
        // ERC20Snapshot ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ
        $balance = 0;
        
        $this->tokenContract->call(
            'balanceOfAt',
            $address,
            '0x' . dechex($blockNumber),
            function ($err, $result) use (&$balance) {
                if ($err) {
                    throw new Exception("Failed to get historical balance: " . $err->getMessage());
                }
                $balance = hexdec($result[0]->toString());
            }
        );
        
        return $balance;
    }
    
    // ์ด ํ† ํฐ ๊ณต๊ธ‰๋Ÿ‰
    public function getTotalSupply(): int
    {
        $supply = 0;
        
        $this->tokenContract->call('totalSupply', function ($err, $result) use (&$supply) {
            if ($err) {
                throw new Exception("Failed to get total supply: " . $err->getMessage());
            }
            $supply = hexdec($result[0]->toString());
        });
        
        return $supply;
    }
    
    // ์ œ์•ˆ์„œ๋ฅผ ๋ธ”๋ก์ฒด์ธ์— ์ƒ์„ฑ
    public function createProposal(array $proposalData): string
    {
        $privateKey = config('blockchain.admin_wallet_private_key');
        $fromAddress = config('blockchain.admin_wallet_address');
        
        // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ค€๋น„
        $data = $this->votingContract->getData(
            'createProposal',
            $proposalData['ipfs_hash'],
            $proposalData['voting_starts_at'],
            $proposalData['voting_ends_at'],
            $proposalData['quorum_required'],
            $proposalData['approval_threshold']
        );
        
        // ๊ฐ€์Šค ์ถ”์ •
        $gasEstimate = $this->estimateGas($fromAddress, $data);
        
        // ํŠธ๋žœ์žญ์…˜ ์ „์†ก
        $txHash = $this->sendTransaction([
            'from' => $fromAddress,
            'to' => config('blockchain.voting_contract_address'),
            'data' => $data,
            'gas' => '0x' . dechex($gasEstimate * 1.2), // 20% ๋ฒ„ํผ
            'gasPrice' => $this->getGasPrice()
        ], $privateKey);
        
        return $txHash;
    }
    
    // ํˆฌํ‘œ ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ
    public function verifyVoteTransaction(string $txHash): array
    {
        $receipt = null;
        
        $this->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
            if ($err) {
                throw new Exception("Failed to get transaction receipt: " . $err->getMessage());
            }
            $receipt = $result;
        });
        
        if (!$receipt || $receipt->status !== '0x1') {
            throw new Exception("Transaction failed or not found");
        }
        
        // ์ด๋ฒคํŠธ ๋กœ๊ทธ ํŒŒ์‹ฑ
        $logs = $receipt->logs;
        $voteData = $this->parseVoteEvent($logs);
        
        return [
            'proposal_id' => $voteData['proposalId'],
            'voter' => $voteData['voter'],
            'choice' => $voteData['choice'],
            'voting_power' => $voteData['votingPower'],
            'block_number' => hexdec($receipt->blockNumber)
        ];
    }
    
    // Vote ์ด๋ฒคํŠธ ํŒŒ์‹ฑ
    protected function parseVoteEvent(array $logs): array
    {
        // Vote ์ด๋ฒคํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜
        $voteEventSignature = '0x' . hash('sha3-256', 'VoteCast(uint256,address,uint8,uint256)');
        
        foreach ($logs as $log) {
            if ($log->topics[0] === $voteEventSignature) {
                return [
                    'proposalId' => hexdec($log->topics[1]),
                    'voter' => '0x' . substr($log->topics[2], -40),
                    'choice' => hexdec($log->data),
                    'votingPower' => hexdec(substr($log->data, 66))
                ];
            }
        }
        
        throw new Exception("Vote event not found in transaction logs");
    }
    
    // ๊ฐ€์Šค ๊ฐ€๊ฒฉ ์กฐํšŒ
    protected function getGasPrice(): string
    {
        $gasPrice = null;
        
        $this->eth->gasPrice(function ($err, $result) use (&$gasPrice) {
            if ($err) {
                throw new Exception("Failed to get gas price: " . $err->getMessage());
            }
            $gasPrice = $result->toString();
        });
        
        return $gasPrice;
    }
    
    // ๊ฐ€์Šค ์ถ”์ •
    protected function estimateGas(string $from, string $data): int
    {
        $gas = 0;
        
        $this->eth->estimateGas([
            'from' => $from,
            'to' => config('blockchain.voting_contract_address'),
            'data' => $data
        ], function ($err, $result) use (&$gas) {
            if ($err) {
                throw new Exception("Failed to estimate gas: " . $err->getMessage());
            }
            $gas = hexdec($result->toString());
        });
        
        return $gas;
    }
    
    // ํŠธ๋žœ์žญ์…˜ ์ „์†ก (์„œ๋ช… ํฌํ•จ)
    protected function sendTransaction(array $tx, string $privateKey): string
    {
        // ๋…ผ์Šค ๊ฐ€์ ธ์˜ค๊ธฐ
        $nonce = null;
        $this->eth->getTransactionCount($tx['from'], 'pending', function ($err, $result) use (&$nonce) {
            if ($err) {
                throw new Exception("Failed to get nonce: " . $err->getMessage());
            }
            $nonce = $result->toString();
        });
        
        $tx['nonce'] = $nonce;
        $tx['chainId'] = config('blockchain.chain_id');
        
        // ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…
        $signedTx = $this->signTransaction($tx, $privateKey);
        
        // ์ „์†ก
        $txHash = null;
        $this->eth->sendRawTransaction('0x' . $signedTx, function ($err, $result) use (&$txHash) {
            if ($err) {
                throw new Exception("Failed to send transaction: " . $err->getMessage());
            }
            $txHash = $result;
        });
        
        return $txHash;
    }
    
    // ํŠธ๋žœ์žญ์…˜ ์„œ๋ช… (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„)
    protected function signTransaction(array $tx, string $privateKey): string
    {
        // ์‹ค์ œ๋กœ๋Š” ethereum-tx ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„
        // ์—ฌ๊ธฐ์„œ๋Š” ๊ฐœ๋…์ ์ธ ์ฝ”๋“œ๋งŒ ๋ณด์—ฌ์ค„๊ฒŒ
        
        // RLP ์ธ์ฝ”๋”ฉ ํ›„ ์„œ๋ช…
        // ... (๋ณต์žกํ•œ ์„œ๋ช… ๋กœ์ง)
        
        return $signedTxHex;
    }
    
    // ํ˜„์žฌ ๋ธ”๋ก ๋ฒˆํ˜ธ
    public function getCurrentBlockNumber(): int
    {
        $blockNumber = 0;
        
        $this->eth->blockNumber(function ($err, $result) use (&$blockNumber) {
            if ($err) {
                throw new Exception("Failed to get block number: " . $err->getMessage());
            }
            $blockNumber = hexdec($result->toString());
        });
        
        return $blockNumber;
    }
    
    // ์ œ์•ˆ์„œ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋ก
    public function finalizeProposal(int $proposalId, string $result): string
    {
        $privateKey = config('blockchain.admin_wallet_private_key');
        $fromAddress = config('blockchain.admin_wallet_address');
        
        $data = $this->votingContract->getData(
            'finalizeProposal',
            $proposalId,
            $result === 'passed' ? 1 : 0
        );
        
        $gasEstimate = $this->estimateGas($fromAddress, $data);
        
        $txHash = $this->sendTransaction([
            'from' => $fromAddress,
            'to' => config('blockchain.voting_contract_address'),
            'data' => $data,
            'gas' => '0x' . dechex($gasEstimate * 1.2),
            'gasPrice' => $this->getGasPrice()
        ], $privateKey);
        
        return $txHash;
    }
}
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ: ์œ„ ์ฝ”๋“œ์˜ signTransaction() ๋ฉ”์„œ๋“œ๋Š” ๊ฐœ๋…์ ์ธ ์˜ˆ์‹œ์•ผ. ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” kornrunner/ethereum-tx ๊ฐ™์€ ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด! ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…์€ ๋งค์šฐ ๋ฏผ๊ฐํ•œ ์ž‘์—…์ด๋ผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿ”

๐Ÿ“ฆ IPFS ์„œ๋น„์Šค

์ œ์•ˆ์„œ ๋‚ด์šฉ์„ IPFS์— ์ €์žฅํ•˜๋Š” ์„œ๋น„์Šค๋„ ๋งŒ๋“ค์–ด๋ณด์ž. IPFS๋Š” ๋ถ„์‚ฐ ํŒŒ์ผ ์‹œ์Šคํ…œ์ด๋ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ๋ณด์กด๋ผ! ๐ŸŒ

// app/Services/IpfsService.php
namespace App\Services;

use GuzzleHttp\Client;
use Exception;

class IpfsService
{
    protected $client;
    protected $apiUrl;
    protected $apiKey;
    
    public function __construct()
    {
        $this->apiUrl = config('ipfs.api_url');
        $this->apiKey = config('ipfs.api_key');
        
        $this->client = new Client([
            'base_uri' => $this->apiUrl,
            'headers' => [
                'Authorization' => 'Bearer ' . $this->apiKey
            ]
        ]);
    }
    
    // ์ œ์•ˆ์„œ ๋ฐ์ดํ„ฐ๋ฅผ IPFS์— ์—…๋กœ๋“œ
    public function uploadProposal(array $proposalData): string
    {
        $jsonData = json_encode([
            'title' => $proposalData['title'],
            'description' => $proposalData['description'],
            'proposer' => $proposalData['proposer_address'],
            'created_at' => now()->toIso8601String(),
            'metadata' => $proposalData['metadata'] ?? []
        ], JSON_PRETTY_PRINT);
        
        try {
            $response = $this->client->post('/pinning/pinJSONToIPFS', [
                'json' => [
                    'pinataContent' => json_decode($jsonData),
                    'pinataMetadata' => [
                        'name' => 'Proposal: ' . $proposalData['title']
                    ]
                ]
            ]);
            
            $result = json_decode($response->getBody(), true);
            return $result['IpfsHash'];
            
        } catch (Exception $e) {
            throw new Exception("Failed to upload to IPFS: " . $e->getMessage());
        }
    }
    
    // IPFS์—์„œ ์ œ์•ˆ์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    public function getProposal(string $ipfsHash): array
    {
        try {
            $response = $this->client->get(
                config('ipfs.gateway') . $ipfsHash
            );
            
            return json_decode($response->getBody(), true);
            
        } catch (Exception $e) {
            throw new Exception("Failed to fetch from IPFS: " . $e->getMessage());
        }
    }
    
    // ๋ฐ์ดํ„ฐ ํ•ด์‹œ ๊ฒ€์ฆ
    public function verifyHash(array $data, string $expectedHash): bool
    {
        $jsonData = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $calculatedHash = hash('sha256', $jsonData);
        
        return $calculatedHash === $expectedHash;
    }
}
๐Ÿ’ก IPFS ํ™œ์šฉ ํŒ: Pinata๋‚˜ Infura ๊ฐ™์€ IPFS ํ•€๋‹ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ๋ณด์กด๋ผ. ๋ฌด๋ฃŒ ํ‹ฐ์–ด๋กœ๋„ ์ถฉ๋ถ„ํžˆ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ๋ถ€๋‹ด ์—†์–ด! ๊ทธ๋ฆฌ๊ณ  ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋Š” ํ•ญ์ƒ ํ•ด์‹œ๊ฐ’์„ ๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋กํ•ด์„œ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜์ž. ๐Ÿ“Œ

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

์ด์ œ ์‚ฌ์šฉ์ž ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๋ผ๋ผ๋ฒจ์˜ ๋ฆฌ์†Œ์Šค ์ปจํŠธ๋กค๋Ÿฌ ํŒจํ„ด์„ ํ™œ์šฉํ•˜๋ฉด RESTful API๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŽฏ

๐Ÿ“‹ ProposalController

// app/Http/Controllers/ProposalController.php
namespace App\Http\Controllers;

use App\Models\Proposal;
use App\Models\VoteSnapshot;
use App\Services\Web3Service;
use App\Services\IpfsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;

class ProposalController extends Controller
{
    protected $web3Service;
    protected $ipfsService;
    
    public function __construct(Web3Service $web3Service, IpfsService $ipfsService)
    {
        $this->web3Service = $web3Service;
        $this->ipfsService = $ipfsService;
    }
    
    // ์ œ์•ˆ์„œ ๋ชฉ๋ก ์กฐํšŒ
    public function index(Request $request)
    {
        $status = $request->query('status', 'all');
        $perPage = $request->query('per_page', 20);
        
        $query = Proposal::with('votes')
                        ->orderBy('created_at', 'desc');
        
        if ($status !== 'all') {
            $query->where('status', $status);
        }
        
        $proposals = $query->paginate($perPage);
        
        // ๊ฐ ์ œ์•ˆ์„œ์— ์ถ”๊ฐ€ ์ •๋ณด ์ฒจ๋ถ€
        $proposals->getCollection()->transform(function ($proposal) {
            $proposal->total_votes = $proposal->votes_for + 
                                    $proposal->votes_against + 
                                    $proposal->votes_abstain;
            
            $proposal->participation_rate = $this->calculateParticipationRate($proposal);
            $proposal->approval_rate = $this->calculateApprovalRate($proposal);
            
            return $proposal;
        });
        
        return response()->json([
            'success' => true,
            'data' => $proposals
        ]);
    }
    
    // ์ œ์•ˆ์„œ ์ƒ์„ธ ์กฐํšŒ
    public function show($id)
    {
        $proposal = Proposal::with(['votes' => function ($query) {
            $query->orderBy('voted_at', 'desc');
        }])->findOrFail($id);
        
        // IPFS์—์„œ ์ „์ฒด ๋‚ด์šฉ ๊ฐ€์ ธ์˜ค๊ธฐ
        if ($proposal->ipfs_hash) {
            try {
                $ipfsData = $this->ipfsService->getProposal($proposal->ipfs_hash);
                $proposal->full_content = $ipfsData;
            } catch (\Exception $e) {
                $proposal->full_content = null;
            }
        }
        
        // ํˆฌํ‘œ ํ†ต๊ณ„
        $proposal->vote_statistics = [
            'total_votes' => $proposal->votes->count(),
            'votes_for' => $proposal->votes_for,
            'votes_against' => $proposal->votes_against,
            'votes_abstain' => $proposal->votes_abstain,
            'participation_rate' => $this->calculateParticipationRate($proposal),
            'approval_rate' => $this->calculateApprovalRate($proposal),
            'quorum_reached' => $proposal->hasReachedQuorum(),
            'is_approved' => $proposal->isApproved()
        ];
        
        return response()->json([
            'success' => true,
            'data' => $proposal
        ]);
    }
    
    // ์ƒˆ ์ œ์•ˆ์„œ ์ƒ์„ฑ
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => 'required|string|max:255',
            'description' => 'required|string|min:100',
            'proposer_address' => 'required|string|size:42',
            'voting_duration_hours' => 'required|integer|min:24|max:720',
            'quorum_required' => 'nullable|numeric|min:1|max:100',
            'approval_threshold' => 'nullable|numeric|min:50|max:100',
            'metadata' => 'nullable|array'
        ]);
        
        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ], 422);
        }
        
        // ์ œ์•ˆ์ž์˜ ํ† ํฐ ๋ณด์œ ๋Ÿ‰ ํ™•์ธ
        $proposerBalance = $this->web3Service->getTokenBalance(
            $request->proposer_address
        );
        
        $minTokenRequired = config('dao.min_token_to_propose', 1000);
        
        if ($proposerBalance < $minTokenRequired) {
            return response()->json([
                'success' => false,
                'message' => "Insufficient tokens. You need at least {$minTokenRequired} tokens to create a proposal."
            ], 403);
        }
        
        DB::beginTransaction();
        
        try {
            // IPFS์— ์ œ์•ˆ์„œ ์—…๋กœ๋“œ
            $ipfsHash = $this->ipfsService->uploadProposal([
                'title' => $request->title,
                'description' => $request->description,
                'proposer_address' => $request->proposer_address,
                'metadata' => $request->metadata
            ]);
            
            // ํˆฌํ‘œ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
            $votingStartsAt = Carbon::now()->addHours(1); // 1์‹œ๊ฐ„ ํ›„ ์‹œ์ž‘
            $votingEndsAt = $votingStartsAt->copy()->addHours(
                $request->voting_duration_hours
            );
            
            // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ œ์•ˆ์„œ ์ƒ์„ฑ
            $proposal = Proposal::create([
                'title' => $request->title,
                'description' => $request->description,
                'ipfs_hash' => $ipfsHash,
                'proposer_address' => $request->proposer_address,
                'status' => 'pending',
                'voting_starts_at' => $votingStartsAt,
                'voting_ends_at' => $votingEndsAt,
                'quorum_required' => $request->quorum_required ?? 10,
                'approval_threshold' => $request->approval_threshold ?? 50,
                'metadata' => $request->metadata
            ]);
            
            // ๋ธ”๋ก์ฒด์ธ์— ์ œ์•ˆ์„œ ์ƒ์„ฑ (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ)
            dispatch(function () use ($proposal) {
                try {
                    $txHash = $this->web3Service->createProposal([
                        'ipfs_hash' => $proposal->ipfs_hash,
                        'voting_starts_at' => $proposal->voting_starts_at->timestamp,
                        'voting_ends_at' => $proposal->voting_ends_at->timestamp,
                        'quorum_required' => $proposal->quorum_required,
                        'approval_threshold' => $proposal->approval_threshold
                    ]);
                    
                    $proposal->update([
                        'blockchain_tx_hash' => $txHash,
                        'status' => 'active'
                    ]);
                    
                    // ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์Šค์ผ€์ค„๋ง
                    $this->scheduleSnapshotCreation($proposal);
                    
                } catch (\Exception $e) {
                    \Log::error("Failed to create proposal on blockchain: " . $e->getMessage());
                    $proposal->update(['status' => 'failed']);
                }
            })->afterResponse();
            
            DB::commit();
            
            return response()->json([
                'success' => true,
                'message' => 'Proposal created successfully',
                'data' => $proposal
            ], 201);
            
        } catch (\Exception $e) {
            DB::rollBack();
            
            return response()->json([
                'success' => false,
                'message' => 'Failed to create proposal: ' . $e->getMessage()
            ], 500);
        }
    }
    
    // ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์Šค์ผ€์ค„๋ง
    protected function scheduleSnapshotCreation(Proposal $proposal)
    {
        // ํˆฌํ‘œ ์‹œ์ž‘ ์‹œ์ ์˜ ๋ธ”๋ก ๋ฒˆํ˜ธ์—์„œ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ
        dispatch(function () use ($proposal) {
            // ํˆฌํ‘œ ์‹œ์ž‘๊นŒ์ง€ ๋Œ€๊ธฐ
            sleep($proposal->voting_starts_at->diffInSeconds(now()));
            
            $snapshotBlock = $this->web3Service->getCurrentBlockNumber();
            
            // ๋ชจ๋“  ํ† ํฐ ํ™€๋”์˜ ์ž”์•ก ์Šค๋ƒ…์ƒท (์‹ค์ œ๋กœ๋Š” ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌ)
            // ์—ฌ๊ธฐ์„œ๋Š” ๊ฐœ๋…์ ์ธ ์ฝ”๋“œ๋งŒ ๋ณด์—ฌ์ค„๊ฒŒ
            
            $proposal->update([
                'metadata->snapshot_block' => $snapshotBlock
            ]);
            
        })->delay($proposal->voting_starts_at);
    }
    
    // ์ฐธ์—ฌ์œจ ๊ณ„์‚ฐ
    protected function calculateParticipationRate(Proposal $proposal): float
    {
        $totalVotes = $proposal->votes_for + 
                     $proposal->votes_against + 
                     $proposal->votes_abstain;
        
        $totalSupply = $this->web3Service->getTotalSupply();
        
        if ($totalSupply == 0) return 0;
        
        return round(($totalVotes / $totalSupply) * 100, 2);
    }
    
    // ์ฐฌ์„ฑ๋ฅ  ๊ณ„์‚ฐ
    protected function calculateApprovalRate(Proposal $proposal): float
    {
        $totalVotes = $proposal->votes_for + $proposal->votes_against;
        
        if ($totalVotes == 0) return 0;
        
        return round(($proposal->votes_for / $totalVotes) * 100, 2);
    }
}

๐Ÿ—ณ๏ธ VoteController

// app/Http/Controllers/VoteController.php
namespace App\Http\Controllers;

use App\Models\Proposal;
use App\Models\Vote;
use App\Models\VoteSnapshot;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class VoteController extends Controller
{
    protected $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        $this->web3Service = $web3Service;
    }
    
    // ํˆฌํ‘œํ•˜๊ธฐ
    public function store(Request $request, $proposalId)
    {
        $validator = Validator::make($request->all(), [
            'voter_address' => 'required|string|size:42',
            'vote_choice' => 'required|in:for,against,abstain',
            'transaction_hash' => 'required|string|size:66',
            'signature' => 'nullable|string'
        ]);
        
        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ], 422);
        }
        
        $proposal = Proposal::findOrFail($proposalId);
        
        // ํˆฌํ‘œ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
        if (!$proposal->isVotingActive()) {
            return response()->json([
                'success' => false,
                'message' => 'Voting is not active for this proposal'
            ], 403);
        }
        
        // ์ค‘๋ณต ํˆฌํ‘œ ํ™•์ธ
        $existingVote = Vote::where('proposal_id', $proposalId)
                           ->where('voter_address', $request->voter_address)
                           ->first();
        
        if ($existingVote) {
            return response()->json([
                'success' => false,
                'message' => 'You have already voted on this proposal'
            ], 403);
        }
        
        try {
            // ๋ธ”๋ก์ฒด์ธ์—์„œ ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ
            $txData = $this->web3Service->verifyVoteTransaction(
                $request->transaction_hash
            );
            
            // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ์™€ ์š”์ฒญ ๋ฐ์ดํ„ฐ ์ผ์น˜ ํ™•์ธ
            if ($txData['proposal_id'] != $proposalId ||
                strtolower($txData['voter']) !== strtolower($request->voter_address)) {
                return response()->json([
                    'success' => false,
                    'message' => 'Transaction data does not match request'
                ], 400);
            }
            
            // ํˆฌํ‘œ๊ถŒ ํ™•์ธ (์Šค๋ƒ…์ƒท ๊ธฐ๋ฐ˜)
            $votingPower = VoteSnapshot::getVotingPower(
                $proposalId,
                $request->voter_address
            );
            
            if ($votingPower == 0) {
                // ์Šค๋ƒ…์ƒท์ด ์—†์œผ๋ฉด ํ˜„์žฌ ์ž”์•ก์œผ๋กœ ์ƒ์„ฑ
                $votingPower = $this->web3Service->getTokenBalance(
                    $request->voter_address
                );
                
                if ($votingPower == 0) {
                    return response()->json([
                        'success' => false,
                        'message' => 'You do not have voting power'
                    ], 403);
                }
                
                // ์Šค๋ƒ…์ƒท ์ƒ์„ฑ
                VoteSnapshot::create([
                    'proposal_id' => $proposalId,
                    'voter_address' => $request->voter_address,
                    'token_balance' => $votingPower,
                    'snapshot_block' => $txData['block_number']
                ]);
            }
            
            // ํˆฌํ‘œ ๊ธฐ๋ก ์ƒ์„ฑ
            $vote = Vote::create([
                'proposal_id' => $proposalId,
                'voter_address' => $request->voter_address,
                'vote_choice' => $request->vote_choice,
                'voting_power' => $votingPower,
                'blockchain_tx_hash' => $request->transaction_hash,
                'block_number' => $txData['block_number'],
                'voted_at' => now(),
                'signature' => $request->signature
            ]);
            
            // ์ œ์•ˆ์„œ ํˆฌํ‘œ ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ
            $this->updateProposalVotes($proposal, $request->vote_choice, $votingPower);
            
            return response()->json([
                'success' => true,
                'message' => 'Vote recorded successfully',
                'data' => $vote
            ], 201);
            
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Failed to record vote: ' . $e->getMessage()
            ], 500);
        }
    }
    
    // ์ œ์•ˆ์„œ ํˆฌํ‘œ ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ
    protected function updateProposalVotes(Proposal $proposal, string $choice, int $power)
    {
        switch ($choice) {
            case 'for':
                $proposal->increment('votes_for', $power);
                break;
            case 'against':
                $proposal->increment('votes_against', $power);
                break;
            case 'abstain':
                $proposal->increment('votes_abstain', $power);
                break;
        }
    }
    
    // ํŠน์ • ์ œ์•ˆ์„œ์˜ ํˆฌํ‘œ ๋ชฉ๋ก
    public function index($proposalId)
    {
        $proposal = Proposal::findOrFail($proposalId);
        
        $votes = Vote::where('proposal_id', $proposalId)
                    ->orderBy('voted_at', 'desc')
                    ->paginate(50);
        
        return response()->json([
            'success' => true,
            'data' => $votes
        ]);
    }
    
    // ํŠน์ • ์ฃผ์†Œ์˜ ํˆฌํ‘œ ๋‚ด์—ญ
    public function userVotes(Request $request)
    {
        $address = $request->query('address');
        
        if (!$address) {
            return response()->json([
                'success' => false,
                'message' => 'Address parameter is required'
            ], 400);
        }
        
        $votes = Vote::with('proposal')
                    ->where('voter_address', $address)
                    ->orderBy('voted_at', 'desc')
                    ->paginate(20);
        
        return response()->json([
            'success' => true,
            'data' => $votes
        ]);
    }
    
    // ํˆฌํ‘œ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
    public function checkEligibility($proposalId, Request $request)
    {
        $address = $request->query('address');
        
        if (!$address) {
            return response()->json([
                'success' => false,
                'message' => 'Address parameter is required'
            ], 400);
        }
        
        $proposal = Proposal::findOrFail($proposalId);
        
        // ํˆฌํ‘œ ๊ธฐ๊ฐ„ ํ™•์ธ
        if (!$proposal->isVotingActive()) {
            return response()->json([
                'success' => false,
                'eligible' => false,
                'reason' => 'Voting period has ended or not started yet'
            ]);
        }
        
        // ์ค‘๋ณต ํˆฌํ‘œ ํ™•์ธ
        $hasVoted = Vote::where('proposal_id', $proposalId)
                       ->where('voter_address', $address)
                       ->exists();
        
        if ($hasVoted) {
            return response()->json([
                'success' => false,
                'eligible' => false,
                'reason' => 'You have already voted'
            ]);
        }
        
        // ํˆฌํ‘œ๊ถŒ ํ™•์ธ
        $votingPower = VoteSnapshot::getVotingPower($proposalId, $address);
        
        if ($votingPower == 0) {
            $votingPower = $this->web3Service->getTokenBalance($address);
        }
        
        if ($votingPower == 0) {
            return response()->json([
                'success' => false,
                'eligible' => false,
                'reason' => 'You do not have any voting power'
            ]);
        }
        
        return response()->json([
            'success' => true,
            'eligible' => true,
            'voting_power' => $votingPower
        ]);
    }
}
๐ŸŽฏ ์ปจํŠธ๋กค๋Ÿฌ ์„ค๊ณ„ ํฌ์ธํŠธ

โ€ข ๊ฒ€์ฆ ๋ ˆ์ด์–ด: ๋ชจ๋“  ์ž…๋ ฅ๊ฐ’์„ ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•ด. ๋ธ”๋ก์ฒด์ธ ์ฃผ์†Œ ํ˜•์‹, ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ๋“ฑ์„ ์ฒดํฌํ•˜๋Š” ๊ฑฐ์ง€.

โ€ข ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋‹ˆ๊นŒ dispatch()->afterResponse()๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌํ•ด.

โ€ข ์—๋Ÿฌ ํ•ธ๋“ค๋ง: ๋ธ”๋ก์ฒด์ธ ํ†ต์‹ ์€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ try-catch๋กœ ๊ฐ์‹ธ๊ณ  ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ด. ๐Ÿ›ก๏ธ

โฐ ์Šค์ผ€์ค„๋Ÿฌ์™€ ํ ์ž‘์—… ์„ค์ •ํ•˜๊ธฐ

DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ์€ ์ž๋™ํ™”๊ฐ€ ์ƒ๋ช…์ด์•ผ! ํˆฌํ‘œ ๊ธฐ๊ฐ„ ์ข…๋ฃŒ, ๊ฒฐ๊ณผ ์ง‘๊ณ„, ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ๋“ฑ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๊ฑฐ๋“ . ๋ผ๋ผ๋ฒจ์˜ Task Scheduling๊ณผ Queue ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•ด๋ณด์ž. โš™๏ธ

๐Ÿ“… ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ •

// app/Console/Kernel.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)
    {
        // ๋งค 5๋ถ„๋งˆ๋‹ค ์ข…๋ฃŒ๋œ ์ œ์•ˆ์„œ ํ™•์ธ ๋ฐ ๊ฒฐ๊ณผ ์ง‘๊ณ„
        $schedule->command('dao:finalize-proposals')
                 ->everyFiveMinutes()
                 ->withoutOverlapping();
        
        // ๋งค ์‹œ๊ฐ„๋งˆ๋‹ค ํ™œ์„ฑ ์ œ์•ˆ์„œ ์ƒํƒœ ๋™๊ธฐํ™”
        $schedule->command('dao:sync-proposals')
                 ->hourly()
                 ->withoutOverlapping();
        
        // ๋งค์ผ ์ž์ •์— ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
        $schedule->command('dao:update-statistics')
                 ->daily()
                 ->at('00:00');
        
        // ๋งค 10๋ถ„๋งˆ๋‹ค ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹
        $schedule->command('dao:listen-events')
                 ->everyTenMinutes()
                 ->withoutOverlapping();
    }
}

๐Ÿ”จ Artisan ์ปค๋งจ๋“œ ๊ตฌํ˜„

// app/Console/Commands/FinalizeProposals.php
namespace App\Console\Commands;

use App\Models\Proposal;
use App\Services\Web3Service;
use Illuminate\Console\Command;
use Carbon\Carbon;

class FinalizeProposals extends Command
{
    protected $signature = 'dao:finalize-proposals';
    protected $description = 'Finalize ended proposals and record results on blockchain';
    
    protected $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        parent::__construct();
        $this->web3Service = $web3Service;
    }
    
    public function handle()
    {
        $this->info('Checking for proposals to finalize...');
        
        // ์ข…๋ฃŒ๋˜์—ˆ์ง€๋งŒ ์•„์ง ์ตœ์ข…ํ™”๋˜์ง€ ์•Š์€ ์ œ์•ˆ์„œ๋“ค
        $proposals = Proposal::ended()
                            ->whereIn('status', ['active', 'pending'])
                            ->get();
        
        if ($proposals->isEmpty()) {
            $this->info('No proposals to finalize.');
            return 0;
        }
        
        $this->info("Found {$proposals->count()} proposals to finalize.");
        
        foreach ($proposals as $proposal) {
            try {
                $this->info("Finalizing proposal #{$proposal->id}: {$proposal->title}");
                
                // ์ตœ์ข… ๊ฒฐ๊ณผ ๊ณ„์‚ฐ
                $result = $proposal->calculateResult();
                
                $this->info("Result: {$result}");
                $this->info("Votes For: {$proposal->votes_for}");
                $this->info("Votes Against: {$proposal->votes_against}");
                $this->info("Votes Abstain: {$proposal->votes_abstain}");
                
                // ๋ธ”๋ก์ฒด์ธ์— ๊ฒฐ๊ณผ ๊ธฐ๋ก
                if ($proposal->proposal_id_onchain) {
                    $txHash = $this->web3Service->finalizeProposal(
                        $proposal->proposal_id_onchain,
                        $result
                    );
                    
                    $this->info("Blockchain transaction: {$txHash}");
                }
                
                // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
                $proposal->update([
                    'status' => $result,
                    'metadata->finalized_at' => Carbon::now()->toIso8601String()
                ]);
                
                $this->info("โœ… Proposal #{$proposal->id} finalized successfully.\n");
                
            } catch (\Exception $e) {
                $this->error("โŒ Failed to finalize proposal #{$proposal->id}: " . $e->getMessage());
                continue;
            }
        }
        
        $this->info('Finalization complete!');
        return 0;
    }
}
// app/Console/Commands/SyncProposals.php
namespace App\Console\Commands;

use App\Models\Proposal;
use App\Models\Vote;
use App\Services\Web3Service;
use Illuminate\Console\Command;

class SyncProposals extends Command
{
    protected $signature = 'dao:sync-proposals';
    protected $description = 'Sync proposal data with blockchain';
    
    protected $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        parent::__construct();
        $this->web3Service = $web3Service;
    }
    
    public function handle()
    {
        $this->info('Syncing proposals with blockchain...');
        
        $activeProposals = Proposal::active()->get();
        
        if ($activeProposals->isEmpty()) {
            $this->info('No active proposals to sync.');
            return 0;
        }
        
        foreach ($activeProposals as $proposal) {
            try {
                $this->info("Syncing proposal #{$proposal->id}");
                
                // ๋ธ”๋ก์ฒด์ธ์—์„œ ์ตœ์‹  ํˆฌํ‘œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
                // (์‹ค์ œ๋กœ๋Š” ์ด๋ฒคํŠธ ๋กœ๊ทธ๋ฅผ ํŒŒ์‹ฑํ•ด์„œ ๊ฐ€์ ธ์™€์•ผ ํ•ด)
                
                // ์—ฌ๊ธฐ์„œ๋Š” ๊ฐœ๋…์ ์ธ ์ฝ”๋“œ๋งŒ
                $onchainData = $this->web3Service->getProposalData(
                    $proposal->proposal_id_onchain
                );
                
                // ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”
                if ($onchainData) {
                    $proposal->update([
                        'votes_for' => $onchainData['votesFor'],
                        'votes_against' => $onchainData['votesAgainst'],
                        'votes_abstain' => $onchainData['votesAbstain']
                    ]);
                    
                    $this->info("โœ… Synced proposal #{$proposal->id}");
                }
                
            } catch (\Exception $e) {
                $this->error("โŒ Failed to sync proposal #{$proposal->id}: " . $e->getMessage());
                continue;
            }
        }
        
        $this->info('Sync complete!');
        return 0;
    }
}

๐Ÿ“ฌ ํ ์ž‘์—… (Jobs)

// app/Jobs/ProcessVoteJob.php
namespace App\Jobs;

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

class ProcessVoteJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public $tries = 3;
    public $timeout = 120;
    
    protected $voteData;
    
    public function __construct(array $voteData)
    {
        $this->voteData = $voteData;
    }
    
    public function handle(Web3Service $web3Service)
    {
        try {
            // ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ
            $txData = $web3Service->verifyVoteTransaction(
                $this->voteData['transaction_hash']
            );
            
            // ํˆฌํ‘œ ๊ธฐ๋ก ์ƒ์„ฑ
            $vote = Vote::create([
                'proposal_id' => $this->voteData['proposal_id'],
                'voter_address' => $this->voteData['voter_address'],
                'vote_choice' => $this->voteData['vote_choice'],
                'voting_power' => $txData['voting_power'],
                'blockchain_tx_hash' => $this->voteData['transaction_hash'],
                'block_number' => $txData['block_number'],
                'voted_at' => now()
            ]);
            
            // ์ œ์•ˆ์„œ ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ
            $proposal = Proposal::find($this->voteData['proposal_id']);
            
            switch ($this->voteData['vote_choice']) {
                case 'for':
                    $proposal->increment('votes_for', $txData['voting_power']);
                    break;
                case 'against':
                    $proposal->increment('votes_against', $txData['voting_power']);
                    break;
                case 'abstain':
                    $proposal->increment('votes_abstain', $txData['voting_power']);
                    break;
            }
            
            \Log::info("Vote processed successfully", [
                'vote_id' => $vote->id,
                'proposal_id' => $proposal->id
            ]);
            
        } catch (\Exception $e) {
            \Log::error("Failed to process vote", [
                'error' => $e->getMessage(),
                'vote_data' => $this->voteData
            ]);
            
            throw $e; // ์žฌ์‹œ๋„๋ฅผ ์œ„ํ•ด ์˜ˆ์™ธ๋ฅผ ๋‹ค์‹œ ๋˜์ง
        }
    }
    
    public function failed(\Throwable $exception)
    {
        // ์ž‘์—…์ด ์™„์ „ํžˆ ์‹คํŒจํ–ˆ์„ ๋•Œ ์ฒ˜๋ฆฌ
        \Log::error("Vote processing failed permanently", [
            'error' => $exception->getMessage(),
            'vote_data' => $this->voteData
        ]);
        
        // ๊ด€๋ฆฌ์ž์—๊ฒŒ ์•Œ๋ฆผ ๋ฐœ์†ก ๋“ฑ
    }
}
// app/Jobs/CreateSnapshotJob.php
namespace App\Jobs;

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

class CreateSnapshotJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    protected $proposal;
    
    public function __construct(Proposal $proposal)
    {
        $this->proposal = $proposal;
    }
    
    public function handle(Web3Service $web3Service)
    {
        try {
            $snapshotBlock = $web3Service->getCurrentBlockNumber();
            
            // ๋ชจ๋“  ํ† ํฐ ํ™€๋” ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
            // (์‹ค์ œ๋กœ๋Š” Transfer ์ด๋ฒคํŠธ๋ฅผ ํŒŒ์‹ฑํ•ด์„œ ํ™€๋” ๋ชฉ๋ก์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด)
            $holders = $this->getTokenHolders();
            
            foreach ($holders as $holder) {
                $balance = $web3Service->getTokenBalanceAtBlock(
                    $holder,
                    $snapshotBlock
                );
                
                if ($balance > 0) {
                    VoteSnapshot::create([
                        'proposal_id' => $this->proposal->id,
                        'voter_address' => $holder,
                        'token_balance' => $balance,
                        'snapshot_block' => $snapshotBlock
                    ]);
                }
            }
            
            $this->proposal->update([
                'metadata->snapshot_block' => $snapshotBlock,
                'metadata->snapshot_created_at' => now()->toIso8601String()
            ]);
            
            \Log::info("Snapshot created for proposal #{$this->proposal->id}", [
                'block' => $snapshotBlock,
                'holders_count' => count($holders)
            ]);
            
        } catch (\Exception $e) {
            \Log::error("Failed to create snapshot", [
                'proposal_id' => $this->proposal->id,
                'error' => $e->getMessage()
            ]);
            
            throw $e;
        }
    }
    
    protected function getTokenHolders(): array
    {
        // ์บ์‹œ๋œ ํ™€๋” ๋ชฉ๋ก ๋ฐ˜ํ™˜
        // ์‹ค์ œ๋กœ๋Š” ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ํŒŒ์‹ฑํ•ด์„œ ๋งŒ๋“ค์–ด์•ผ ํ•ด
        return cache()->remember('token_holders', 3600, function () {
            // Transfer ์ด๋ฒคํŠธ ํŒŒ์‹ฑ ๋กœ์ง
            return [];
        });
    }
}
๐Ÿ’ก ํ ์‹œ์Šคํ…œ ํ™œ์šฉ ํŒ: ๋ผ๋ผ๋ฒจ์˜ ํ๋Š” Redis๋‚˜ Database ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ํ”„๋กœ๋•์…˜์—์„œ๋Š” Redis๋ฅผ ์ถ”์ฒœํ•ด! ๊ทธ๋ฆฌ๊ณ  php artisan queue:work ๋ช…๋ น์–ด๋กœ ์›Œ์ปค๋ฅผ ์‹คํ–‰ํ•˜๊ณ , Supervisor๋กœ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉด ์•ˆ์ •์ ์œผ๋กœ ์šด์˜ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿš€

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! Vue.js์™€ Web3.js๋ฅผ ์กฐํ•ฉํ•˜๋ฉด ์ •๋ง ๋ฉ‹์ง„ DApp์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๐Ÿ–ผ๏ธ

๐Ÿ”Œ Web3 ์—ฐ๊ฒฐ ์ปดํฌ๋„ŒํŠธ

// resources/js/components/WalletConnect.vue
<template>
  <div class="wallet-connect">
    <div v-if="!isConnected" class="connect-section">
      <button @click="connectWallet" class="connect-btn">
        ๐ŸฆŠ Connect Wallet
      </button>
      <p class="hint">Connect your MetaMask to participate in DAO voting</p>
    </div>
    
    <div v-else class="connected-section">
      <div class="wallet-info">
        <span class="address">{{ shortAddress }}</span>
        <span class="balance">{{ tokenBalance }} GOV</span>
      </div>
      <button @click="disconnectWallet" class="disconnect-btn">
        Disconnect
      </button>
    </div>
    
    <div v-if="error" class="error-message">
      โš ๏ธ {{ error }}
    </div>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue';
import detectEthereumProvider from '@metamask/detect-provider';
import Web3 from 'web3';

export default {
  name: 'WalletConnect',
  
  setup() {
    const isConnected = ref(false);
    const account = ref(null);
    const tokenBalance = ref(0);
    const error = ref(null);
    const web3 = ref(null);
    
    const shortAddress = computed(() => {
      if (!account.value) return '';
      return `${account.value.slice(0, 6)}...${account.value.slice(-4)}`;
    });
    
    const connectWallet = async () => {
      try {
        error.value = null;
        
        const provider = await detectEthereumProvider();
        
        if (!provider) {
          error.value = 'Please install MetaMask!';
          return;
        }
        
        web3.value = new Web3(provider);
        
        // ๊ณ„์ • ์—ฐ๊ฒฐ ์š”์ฒญ
        const accounts = await provider.request({
          method: 'eth_requestAccounts'
        });
        
        account.value = accounts[0];
        isConnected.value = true;
        
        // ๋„คํŠธ์›Œํฌ ํ™•์ธ
        const chainId = await web3.value.eth.getChainId();
        const expectedChainId = import.meta.env.VITE_CHAIN_ID;
        
        if (chainId !== parseInt(expectedChainId)) {
          await switchNetwork();
        }
        
        // ํ† ํฐ ์ž”์•ก ์กฐํšŒ
        await fetchTokenBalance();
        
        // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
        provider.on('accountsChanged', handleAccountsChanged);
        provider.on('chainChanged', handleChainChanged);
        
      } catch (err) {
        console.error('Failed to connect wallet:', err);
        error.value = err.message;
      }
    };
    
    const disconnectWallet = () => {
      isConnected.value = false;
      account.value = null;
      tokenBalance.value = 0;
    };
    
    const switchNetwork = async () => {
      try {
        await window.ethereum.request({
          method: 'wallet_switchEthereumChain',
          params: [{ chainId: '0x' + parseInt(import.meta.env.VITE_CHAIN_ID).toString(16) }]
        });
      } catch (switchError) {
        // ๋„คํŠธ์›Œํฌ๊ฐ€ ์—†์œผ๋ฉด ์ถ”๊ฐ€
        if (switchError.code === 4902) {
          await addNetwork();
        }
      }
    };
    
    const addNetwork = async () => {
      await window.ethereum.request({
        method: 'wallet_addEthereumChain',
        params: [{
          chainId: '0x' + parseInt(import.meta.env.VITE_CHAIN_ID).toString(16),
          chainName: 'Polygon Mumbai Testnet',
          nativeCurrency: {
            name: 'MATIC',
            symbol: 'MATIC',
            decimals: 18
          },
          rpcUrls: ['https://rpc-mumbai.maticvigil.com'],
          blockExplorerUrls: ['https://mumbai.polygonscan.com']
        }]
      });
    };
    
    const fetchTokenBalance = async () => {
      if (!web3.value || !account.value) return;
      
      const tokenContract = new web3.value.eth.Contract(
        JSON.parse(import.meta.env.VITE_TOKEN_ABI),
        import.meta.env.VITE_TOKEN_ADDRESS
      );
      
      const balance = await tokenContract.methods.balanceOf(account.value).call();
      tokenBalance.value = web3.value.utils.fromWei(balance, 'ether');
    };
    
    const handleAccountsChanged = (accounts) => {
      if (accounts.length === 0) {
        disconnectWallet();
      } else {
        account.value = accounts[0];
        fetchTokenBalance();
      }
    };
    
    const handleChainChanged = () => {
      window.location.reload();
    };
    
    onMounted(async () => {
      // ์ด์ „์— ์—ฐ๊ฒฐํ–ˆ์—ˆ๋Š”์ง€ ํ™•์ธ
      const provider = await detectEthereumProvider();
      if (provider && provider.selectedAddress) {
        await connectWallet();
      }
    });
    
    return {
      isConnected,
      account,
      tokenBalance,
      error,
      shortAddress,
      connectWallet,
      disconnectWallet
    };
  }
};
</script>

<style scoped>
.wallet-connect {
  padding: 20px;
}

.connect-btn {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s;
}

.connect-btn:hover {
  transform: translateY(-2px);
}

.connected-section {
  display: flex;
  align-items: center;
  gap: 15px;
}

.wallet-info {
  background: #f5f7fa;
  padding: 10px 15px;
  border-radius: 8px;
  display: flex;
  gap: 10px;
}

.error-message {
  margin-top: 10px;
  padding: 10px;
  background: #fee;
  color: #c33;
  border-radius: 6px;
}
</style>

๐Ÿ—ณ๏ธ ํˆฌํ‘œ ์ปดํฌ๋„ŒํŠธ

// resources/js/components/VoteProposal.vue
<template>
  <div class="vote-proposal">
    <div class="proposal-header">
      <h2>{{ proposal.title }}</h2>
      <span class="status-badge" :class="proposal.status">
        {{ proposal.status }}
      </span>
    </div>
    
    <div class="proposal-content">
      <p>{{ proposal.description }}</p>
    </div>
    
    <div class="voting-stats">
      <div class="stat-item">
        <span class="label">Voting Power:</span>
        <span class="value">{{ votingPower }} GOV</span>
      </div>
      <div class="stat-item">
        <span class="label">Ends in:</span>
        <span class="value">{{ timeRemaining }}</span>
      </div>
    </div>
    
    <div class="vote-results">
      <div class="result-bar">
        <div class="bar-segment for" :style="{ width: forPercentage + '%' }">
          <span>โœ… For: {{ forPercentage }}%</span>
        </div>
        <div class="bar-segment against" :style="{ width: againstPercentage + '%' }">
          <span>โŒ Against: {{ againstPercentage }}%</span>
        </div>
      </div>
      
      <div class="vote-counts">
        <div>For: {{ proposal.votes_for }}</div>
        <div>Against: {{ proposal.votes_against }}</div>
        <div>Abstain: {{ proposal.votes_abstain }}</div>
      </div>
    </div>
    
    <div v-if="canVote" class="vote-actions">
      <button 
        @click="vote('for')" 
        class="vote-btn for"
        :disabled="isVoting"
      >
        โœ… Vote For
      </button>
      
      <button 
        @click="vote('against')" 
        class="vote-btn against"
        :disabled="isVoting"
      >
        โŒ Vote Against
      </button>
      
      <button 
        @click="vote('abstain')" 
        class="vote-btn abstain"
        :disabled="isVoting"
      >
        โธ๏ธ Abstain
      </button>
    </div>
    
    <div v-if="hasVoted" class="voted-message">
      โœ… You have already voted on this proposal
    </div>
    
    <div v-if="isVoting" class="loading-overlay">
      <div class="spinner"></div>
      <p>Processing your vote...</p>
    </div>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
import Web3 from 'web3';

export default {
  name: 'VoteProposal',
  
  props: {
    proposalId: {
      type: Number,
      required: true
    },
    walletAddress: {
      type: String,
      required: true
    }
  },
  
  setup(props) {
    const proposal = ref(null);
    const votingPower = ref(0);
    const canVote = ref(false);
    const hasVoted = ref(false);
    const isVoting = ref(false);
    const web3 = ref(null);
    
    const forPercentage = computed(() => {
      if (!proposal.value) return 0;
      const total = proposal.value.votes_for + proposal.value.votes_against;
      if (total === 0) return 0;
      return Math.round((proposal.value.votes_for / total) * 100);
    });
    
    const againstPercentage = computed(() => {
      return 100 - forPercentage.value;
    });
    
    const timeRemaining = computed(() => {
      if (!proposal.value) return '';
      const end = new Date(proposal.value.voting_ends_at);
      const now = new Date();
      const diff = end - now;
      
      if (diff <= 0) return 'Ended';
      
      const days = Math.floor(diff / (1000 * 60 * 60 * 24));
      const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      
      return `${days}d ${hours}h`;
    });
    
    const fetchProposal = async () => {
      try {
        const response = await axios.get(`/api/proposals/${props.proposalId}`);
        proposal.value = response.data.data;
      } catch (error) {
        console.error('Failed to fetch proposal:', error);
      }
    };
    
    const checkEligibility = async () => {
      try {
        const response = await axios.get(
          `/api/proposals/${props.proposalId}/check-eligibility`,
          { params: { address: props.walletAddress } }
        );
        
        if (response.data.eligible) {
          canVote.value = true;
          votingPower.value = response.data.voting_power;
        } else {
          canVote.value = false;
          if (response.data.reason.includes('already voted')) {
            hasVoted.value = true;
          }
        }
      } catch (error) {
        console.error('Failed to check eligibility:', error);
      }
    };
    
    const vote = async (choice) => {
      if (!canVote.value || isVoting.value) return;
      
      try {
        isVoting.value = true;
        
        // Web3 ์ดˆ๊ธฐํ™”
        web3.value = new Web3(window.ethereum);
        
        // ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ธ์Šคํ„ด์Šค
        const votingContract = new web3.value.eth.Contract(
          JSON.parse(import.meta.env.VITE_VOTING_ABI),
          import.meta.env.VITE_VOTING_ADDRESS
        );
        
        // ํˆฌํ‘œ ์„ ํƒ์„ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ (0: for, 1: against, 2: abstain)
        const voteChoice = choice === 'for' ? 0 : choice === 'against' ? 1 : 2;
        
        // ํŠธ๋žœ์žญ์…˜ ์ „์†ก
        const tx = await votingContract.methods
          .castVote(proposal.value.proposal_id_onchain, voteChoice)
          .send({ from: props.walletAddress });
        
        console.log('Transaction hash:', tx.transactionHash);
        
        // ๋ฐฑ์—”๋“œ์— ํˆฌํ‘œ ๊ธฐ๋ก
        await axios.post(`/api/proposals/${props.proposalId}/vote`, {
          voter_address: props.walletAddress,
          vote_choice: choice,
          transaction_hash: tx.transactionHash
        });
        
        // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
        hasVoted.value = true;
        canVote.value = false;
        
        // ์ œ์•ˆ์„œ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ
        await fetchProposal();
        
        alert('โœ… Your vote has been recorded successfully!');
        
      } catch (error) {
        console.error('Failed to vote:', error);
        alert('โŒ Failed to vote: ' + error.message);
      } finally {
        isVoting.value = false;
      }
    };
    
    onMounted(async () => {
      await fetchProposal();
      if (props.walletAddress) {
        await checkEligibility();
      }
    });
    
    return {
      proposal,
      votingPower,
      canVote,
      hasVoted,
      isVoting,
      forPercentage,
      againstPercentage,
      timeRemaining,
      vote
    };
  }
};
</script>

<style scoped>
.vote-proposal {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.proposal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.status-badge {
  padding: 6px 12px;
  border-radius: 20px;
  font-size: 14px;
  font-weight: 600;
}

.status-badge.active {
  background: #e8f5e9;
  color: #2e7d32;
}

.vote-results {
  margin: 30px 0;
}

.result-bar {
  display: flex;
  height: 40px;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 15px;
}

.bar-segment {
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 600;
  transition: width 0.3s ease;
}

.bar-segment.for {
  background: #66bb6a;
}

.bar-segment.against {
  background: #ef5350;
}

.vote-actions {
  display: flex;
  gap: 15px;
  margin-top: 30px;
}

.vote-btn {
  flex: 1;
  padding: 15px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s;
}

.vote-btn:hover:not(:disabled) {
  transform: translateY(-2px);
}

.vote-btn.for {
  background: #66bb6a;
  color: white;
}

.vote-btn.against {
  background: #ef5350;
  color: white;
}

.vote-btn.abstain {
  background: #9e9e9e;
  color: white;
}

.vote-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: white;
  z-index: 1000;
}

.spinner {
  width: 50px;
  height: 50px;
  border: 5px solid rgba(255, 255, 255, 0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}
</style>
๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ํŒ

โ€ข ์ƒํƒœ ๊ด€๋ฆฌ: ๋ณต์žกํ•œ ์•ฑ์ด๋ผ๋ฉด Vuex๋‚˜ Pinia ๊ฐ™์€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

โ€ข ์—๋Ÿฌ ํ•ธ๋“ค๋ง: ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๊ฑฐ๋ถ€, ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋“ฑ ๋‹ค์–‘ํ•œ ์—๋Ÿฌ ์ƒํ™ฉ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด.

โ€ข UX ๊ฐœ์„ : ํŠธ๋žœ์žญ์…˜์ด ์ฒ˜๋ฆฌ๋˜๋Š” ๋™์•ˆ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , ์„ฑ๊ณต/์‹คํŒจ ํ”ผ๋“œ๋ฐฑ์„ ๋ช…ํ™•ํžˆ ํ•ด์ค˜. ๐ŸŽฏ

๐Ÿ”’ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ์€ ๋ˆ๊ณผ ๊ถŒ๋ ฅ์ด ๊ฑธ๋ฆฐ ๋ฌธ์ œ๋ผ ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด! ์‹ค์ œ ํ”„๋กœ๋•์…˜์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ์ฒดํฌํ•ด์•ผ ํ•  ๋ณด์•ˆ ์‚ฌํ•ญ๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด. ๐Ÿ›ก๏ธ

๐Ÿšจ ์ฃผ์š” ๋ณด์•ˆ ์œ„ํ˜‘

1. ํ”Œ๋ž˜์‹œ ๋ก  ๊ณต๊ฒฉ (Flash Loan Attack)

๊ณต๊ฒฉ์ž๊ฐ€ ํ”Œ๋ž˜์‹œ ๋ก ์œผ๋กœ ๋Œ€๋Ÿ‰์˜ ๊ฑฐ๋ฒ„๋„Œ์Šค ํ† ํฐ์„ ๋นŒ๋ ค์„œ ํˆฌํ‘œํ•˜๊ณ , ๊ฐ™์€ ๋ธ”๋ก ๋‚ด์—์„œ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ์Šค๋ƒ…์ƒท ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ํˆฌํ‘œ ์‹œ์ž‘ ์ „ ํŠน์ • ๋ธ”๋ก์˜ ํ† ํฐ ๋ณด์œ ๋Ÿ‰์„ ๊ธฐ์ค€์œผ๋กœ ํˆฌํ‘œ๊ถŒ์„ ๋ถ€์—ฌํ•˜๋Š” ๊ฑฐ์ง€. ๐Ÿ“ธ
2. ์‹œ๋นŒ ๊ณต๊ฒฉ (Sybil Attack)

ํ•œ ์‚ฌ๋žŒ์ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ง€๊ฐ‘์„ ๋งŒ๋“ค์–ด์„œ ํˆฌํ‘œ๊ถŒ์„ ๋ถ„์‚ฐ์‹œํ‚ค๋Š” ๊ณต๊ฒฉ์ด์•ผ. ํ† ํฐ ๊ธฐ๋ฐ˜ ํˆฌํ‘œ์—์„œ๋Š” ํ† ํฐ์„ ๋ถ„์‚ฐ์‹œ์ผœ๋„ ์ด ํˆฌํ‘œ๊ถŒ์€ ๊ฐ™์œผ๋‹ˆ๊นŒ ์–ด๋А ์ •๋„ ๋ฐฉ์–ด๊ฐ€ ๋ผ. ํ•˜์ง€๋งŒ ์ถ”๊ฐ€๋กœ ์ตœ์†Œ ํ† ํฐ ๋ณด์œ ๋Ÿ‰ ์š”๊ตฌ๋‚˜ ์œ„์ž„ ํˆฌํ‘œ ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•˜๋ฉด ๋” ์•ˆ์ „ํ•ด. ๐Ÿ”
3. ํ”„๋ก ํŠธ๋Ÿฌ๋‹ (Front-running)

๊ณต๊ฒฉ์ž๊ฐ€ ๋ฉคํ’€(mempool)์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•ด์„œ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ํˆฌํ‘œ ํŠธ๋žœ์žญ์…˜์„ ๋ณด๊ณ , ๋” ๋†’์€ ๊ฐ€์Šค๋น„๋กœ ๋จผ์ € ํˆฌํ‘œํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ์ด๊ฑด ์ปค๋ฐ‹-๋ฆฌ๋นŒ ์Šคํ‚ด(Commit-Reveal Scheme)์œผ๋กœ ๋ฐฉ์–ดํ•  ์ˆ˜ ์žˆ์–ด. ๋จผ์ € ํˆฌํ‘œ ๋‚ด์šฉ์˜ ํ•ด์‹œ๋งŒ ์ œ์ถœํ•˜๊ณ , ๋‚˜์ค‘์— ์‹ค์ œ ํˆฌํ‘œ ๋‚ด์šฉ์„ ๊ณต๊ฐœํ•˜๋Š” ๊ฑฐ์ง€. ๐ŸŽญ

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

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ํ•œ ๋ฒˆ ๋ฐฐํฌํ•˜๋ฉด ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ˆ๊นŒ ๋ฐฐํฌ ์ „์— ์ฒ ์ €ํ•œ ๊ฐ์‚ฌ๊ฐ€ ํ•„์š”ํ•ด! ๐Ÿ’Ž

// ์•ˆ์ „ํ•œ Voting Contract ์˜ˆ์‹œ (Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

contract DAOVoting is Ownable, ReentrancyGuard, Pausable {
    IERC20 public governanceToken;
    
    struct Proposal {
        uint256 id;
        string ipfsHash;
        address proposer;
        uint256 startBlock;
        uint256 endBlock;
        uint256 snapshotBlock;
        uint256 votesFor;
        uint256 votesAgainst;
        uint256 votesAbstain;
        bool executed;
        bool passed;
    }
    
    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;
    mapping(uint256 => mapping(address => uint256)) public votingPower;
    
    uint256 public proposalCount;
    uint256 public quorumPercentage = 10; // 10%
    uint256 public approvalThreshold = 50; // 50%
    
    event ProposalCreated(
        uint256 indexed proposalId,
        string ipfsHash,
        address indexed proposer,
        uint256 startBlock,
        uint256 endBlock
    );
    
    event VoteCast(
        uint256 indexed proposalId,
        address indexed voter,
        uint8 choice,
        uint256 votingPower
    );
    
    event ProposalExecuted(
        uint256 indexed proposalId,
        bool passed
    );
    
    modifier onlyDuringVoting(uint256 proposalId) {
        require(
            block.number >= proposals[proposalId].startBlock &&
            block.number <= proposals[proposalId].endBlock,
            "Voting is not active"
        );
        _;
    }
    
    constructor(address _governanceToken) {
        governanceToken = IERC20(_governanceToken);
    }
    
    function createProposal(
        string memory ipfsHash,
        uint256 votingDuration
    ) external whenNotPaused returns (uint256) {
        // ์ตœ์†Œ ํ† ํฐ ๋ณด์œ ๋Ÿ‰ ํ™•์ธ
        require(
            governanceToken.balanceOf(msg.sender) >= 1000 * 10**18,
            "Insufficient tokens to propose"
        );
        
        uint256 proposalId = proposalCount++;
        uint256 startBlock = block.number + 100; // 100๋ธ”๋ก ํ›„ ์‹œ์ž‘
        uint256 snapshotBlock = startBlock - 1;
        
        proposals[proposalId] = Proposal({
            id: proposalId,
            ipfsHash: ipfsHash,
            proposer: msg.sender,
            startBlock: startBlock,
            endBlock: startBlock + votingDuration,
            snapshotBlock: snapshotBlock,
            votesFor: 0,
            votesAgainst: 0,
            votesAbstain: 0,
            executed: false,
            passed: false
        });
        
        emit ProposalCreated(
            proposalId,
            ipfsHash,
            msg.sender,
            startBlock,
            startBlock + votingDuration
        );
        
        return proposalId;
    }
    
    function castVote(
        uint256 proposalId,
        uint8 choice // 0: for, 1: against, 2: abstain
    ) external nonReentrant onlyDuringVoting(proposalId) whenNotPaused {
        require(choice <= 2, "Invalid vote choice");
        require(!hasVoted[proposalId][msg.sender], "Already voted");
        
        Proposal storage proposal = proposals[proposalId];
        
        // ์Šค๋ƒ…์ƒท ๋ธ”๋ก ๊ธฐ์ค€ ํˆฌํ‘œ๊ถŒ ๊ณ„์‚ฐ
        uint256 power = getVotingPowerAt(
            msg.sender,
            proposal.snapshotBlock
        );
        
        require(power > 0, "No voting power");
        
        hasVoted[proposalId][msg.sender] = true;
        votingPower[proposalId][msg.sender] = power;
        
        if (choice == 0) {
            proposal.votesFor += power;
        } else if (choice == 1) {
            proposal.votesAgainst += power;
        } else {
            proposal.votesAbstain += power;
        }
        
        emit VoteCast(proposalId, msg.sender, choice, power);
    }
    
    function executeProposal(uint256 proposalId) external nonReentrant {
        Proposal storage proposal = proposals[proposalId];
        
        require(block.number > proposal.endBlock, "Voting still active");
        require(!proposal.executed, "Already executed");
        
        uint256 totalVotes = proposal.votesFor + 
                            proposal.votesAgainst + 
                            proposal.votesAbstain;
        
        uint256 totalSupply = governanceToken.totalSupply();
        uint256 participationRate = (totalVotes * 100) / totalSupply;
        
        // ์ •์กฑ์ˆ˜ ํ™•์ธ
        require(
            participationRate >= quorumPercentage,
            "Quorum not reached"
        );
        
        // ์Šน์ธ ์—ฌ๋ถ€ ํ™•์ธ
        uint256 approvalRate = (proposal.votesFor * 100) / 
                              (proposal.votesFor + proposal.votesAgainst);
        
        proposal.passed = approvalRate >= approvalThreshold;
        proposal.executed = true;
        
        emit ProposalExecuted(proposalId, proposal.passed);
    }
    
    function getVotingPowerAt(
        address account,
        uint256 blockNumber
    ) public view returns (uint256) {
        // ERC20Snapshot์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ
        // return governanceToken.balanceOfAt(account, blockNumber);
        
        // ์ผ๋ฐ˜ ERC20์˜ ๊ฒฝ์šฐ ํ˜„์žฌ ์ž”์•ก ๋ฐ˜ํ™˜ (์‹ค์ œ๋กœ๋Š” ์Šค๋ƒ…์ƒท ๊ตฌํ˜„ ํ•„์š”)
        return governanceToken.balanceOf(account);
    }
    
    function pause() external onlyOwner {
        _pause();
    }
    
    function unpause() external onlyOwner {
        _unpause();
    }
}
๐ŸŽ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐ์‚ฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… Reentrancy ๊ณต๊ฒฉ ๋ฐฉ์–ด (ReentrancyGuard ์‚ฌ์šฉ)
  • โœ… Integer Overflow/Underflow ๋ฐฉ์ง€ (Solidity 0.8+ ์‚ฌ์šฉ)
  • โœ… Access Control ๊ฒ€์ฆ (Ownable, Role ๊ธฐ๋ฐ˜ ๊ถŒํ•œ)
  • โœ… ๊ธด๊ธ‰ ์ •์ง€ ๊ธฐ๋Šฅ (Pausable)
  • โœ… ๊ฐ€์Šค ์ตœ์ ํ™”
  • โœ… ์™ธ๋ถ€ ๊ฐ์‚ฌ (CertiK, OpenZeppelin ๋“ฑ)

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

// ๋ผ๋ผ๋ฒจ ๋ฏธ๋“ค์›จ์–ด๋กœ API ๋ณดํ˜ธ
// app/Http/Middleware/VerifySignature.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Elliptic\EC;

class VerifySignature
{
    public function handle(Request $request, Closure $next)
    {
        $signature = $request->header('X-Signature');
        $address = $request->header('X-Address');
        $message = $request->getContent();
        
        if (!$signature || !$address) {
            return response()->json([
                'error' => 'Missing signature or address'
            ], 401);
        }
        
        // ์„œ๋ช… ๊ฒ€์ฆ
        if (!$this->verifySignature($message, $signature, $address)) {
            return response()->json([
                'error' => 'Invalid signature'
            ], 401);
        }
        
        $request->merge(['verified_address' => $address]);
        
        return $next($request);
    }
    
    protected function verifySignature(
        string $message,
        string $signature,
        string $address
    ): bool {
        try {
            $ec = new EC('secp256k1');
            
            // ๋ฉ”์‹œ์ง€ ํ•ด์‹œ
            $messageHash = hash('sha3-256', $message);
            
            // ์„œ๋ช… ํŒŒ์‹ฑ
            $r = substr($signature, 2, 64);
            $s = substr($signature, 66, 64);
            $v = hexdec(substr($signature, 130, 2));
            
            // ๊ณต๊ฐœํ‚ค ๋ณต๊ตฌ
            $publicKey = $ec->recoverPubKey(
                $messageHash,
                ['r' => $r, 's' => $s],
                $v
            );
            
            // ์ฃผ์†Œ ๊ณ„์‚ฐ
            $recoveredAddress = '0x' . substr(
                hash('sha3-256', $publicKey->encode('hex')),
                -40
            );
            
            return strtolower($recoveredAddress) === strtolower($address);
            
        } catch (\Exception $e) {
            \Log::error('Signature verification failed: ' . $e->getMessage());
            return false;
        }
    }
}
๐Ÿ” ์ถ”๊ฐ€ ๋ณด์•ˆ ์กฐ์น˜

โ€ข Rate Limiting: API ์š”์ฒญ ํšŸ์ˆ˜ ์ œํ•œ์œผ๋กœ DDoS ๋ฐฉ์–ด
โ€ข CORS ์„ค์ •: ํ—ˆ์šฉ๋œ ๋„๋ฉ”์ธ์—์„œ๋งŒ API ์ ‘๊ทผ ๊ฐ€๋Šฅ
โ€ข ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์•”ํ˜ธํ™”: ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅ
โ€ข ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง: ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ์‹ค์‹œ๊ฐ„ ๊ฐ์ง€
โ€ข ์ •๊ธฐ ๋ฐฑ์—…: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ƒํƒœ ๋ฐฑ์—…

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

๊ฐœ๋ฐœ์ด ๋๋‚ฌ๋‹ค๋ฉด ์ด์ œ ์‹ค์ œ ์„œ๋น„์Šค๋กœ ๋ฐฐํฌํ•  ์ฐจ๋ก€์•ผ! ์•ˆ์ •์ ์ธ ์šด์˜์„ ์œ„ํ•œ ๋ฐฐํฌ ์ „๋žต๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ๐ŸŒ

๐Ÿ“ฆ ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1 ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ

๋จผ์ € ํ…Œ์ŠคํŠธ๋„ท(Mumbai, Goerli ๋“ฑ)์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธํ•œ ํ›„ ๋ฉ”์ธ๋„ท์— ๋ฐฐํฌํ•ด. Hardhat์ด๋‚˜ Truffle ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽธํ•ด. ๋ฐฐํฌ ํ›„์—๋Š” ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๋ฅผ ๋ฐ˜๋“œ์‹œ ๊ธฐ๋กํ•ด๋‘๊ณ , Etherscan์—์„œ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆ(Verify)ํ•˜์ž! ๐Ÿ”
2 ๋ผ๋ผ๋ฒจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ

AWS, DigitalOcean, ๋˜๋Š” Laravel Forge๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ HTTPS๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , APP_DEBUG=false๋กœ ์„ค์ •ํ•ด์•ผ ํ•ด. ๊ทธ๋ฆฌ๊ณ  ์บ์‹œ ์ตœ์ ํ™”๋„ ์žŠ์ง€ ๋ง์ž!
// ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๋ช…๋ น์–ด
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize

// ํ ์›Œ์ปค ์‹คํ–‰ (Supervisor๋กœ ๊ด€๋ฆฌ)
php artisan queue:work --tries=3 --timeout=90
3 ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ

Vue.js ์•ฑ์„ ํ”„๋กœ๋•์…˜ ๋ชจ๋“œ๋กœ ๋นŒ๋“œํ•˜๊ณ , CDN์„ ํ†ตํ•ด ์ •์  ํŒŒ์ผ์„ ์„œ๋น™ํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ. Cloudflare๋‚˜ AWS CloudFront๋ฅผ ์ถ”์ฒœํ•ด! โ˜๏ธ
// ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ
npm run build

// ๋นŒ๋“œ๋œ ํŒŒ์ผ์„ public ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋ณต์‚ฌ
cp -r dist/* public/

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

์‹œ์Šคํ…œ์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ๋ผ๋ผ๋ฒจ Telescope์™€ Horizon์„ ํ™œ์šฉํ•˜๋ฉด ์ข‹์•„! ๐Ÿ”ญ

// Laravel Telescope ์„ค์น˜
composer require laravel/telescope
php artisan telescope:install
php artisan migrate

// Laravel Horizon ์„ค์น˜ (ํ ๋ชจ๋‹ˆํ„ฐ๋ง)
composer require laravel/horizon
php artisan horizon:install
๐Ÿ“ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ํ•ญ๋ชฉ

โ€ข API ์‘๋‹ต ์‹œ๊ฐ„: ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์ด 200ms ์ดํ•˜์ธ์ง€ ํ™•์ธ
โ€ข ๋ธ”๋ก์ฒด์ธ ๋™๊ธฐํ™” ์ƒํƒœ: ์ตœ์‹  ๋ธ”๋ก๊ณผ์˜ ์ฐจ์ด๊ฐ€ 10๋ธ”๋ก ์ด๋‚ด์ธ์ง€ ์ฒดํฌ
โ€ข ํ ์ž‘์—… ์‹คํŒจ์œจ: ์‹คํŒจ์œจ์ด 5% ์ด์ƒ์ด๋ฉด ์•Œ๋ฆผ
โ€ข ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์„ฑ๋Šฅ: ๋А๋ฆฐ ์ฟผ๋ฆฌ ๋กœ๊น… ๋ฐ ์ตœ์ ํ™”
โ€ข ๊ฐ€์Šค๋น„ ์ถ”์ด: ๊ฐ€์Šค๋น„๊ฐ€ ๊ธ‰๋“ฑํ•˜๋ฉด ํŠธ๋žœ์žญ์…˜ ์ผ์‹œ ์ค‘๋‹จ

๐Ÿ”„ CI/CD ํŒŒ์ดํ”„๋ผ์ธ

GitHub Actions๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ž๋™ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•ด๋ณด์ž! ๐Ÿค–

// .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
      
      - name: Install Dependencies
        run: composer install --no-dev --optimize-autoloader
      
      - name: Run Tests
        run: php artisan test
      
      - name: Run Static Analysis
        run: ./vendor/bin/phpstan analyse

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Deploy to Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/dao-voting
            git pull origin main
            composer install --no-dev
            php artisan migrate --force
            php artisan config:cache
            php artisan route:cache
            php artisan queue:restart

๐Ÿ’ก ์‹ค์ „ ํŒ๊ณผ ์ตœ์ ํ™”

์‹ค์ œ๋กœ DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ์„ ์šด์˜ํ•˜๋ฉด์„œ ๋ฐฐ์šด ๋…ธํ•˜์šฐ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ์ด๋Ÿฐ ํŒ๋“ค์„ ์•Œ๊ณ  ์žˆ์œผ๋ฉด ํ›จ์”ฌ ํšจ์œจ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ์šด์˜ํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŽฏ

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

1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ

ํˆฌํ‘œ ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ๋งŽ์ด ๋ฐœ์ƒํ•˜๋‹ˆ๊นŒ ์ ์ ˆํ•œ ์ธ๋ฑ์Šค๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ํŠนํžˆ proposal_id, voter_address, blockchain_tx_hash ์ปฌ๋Ÿผ์—๋Š” ๋ฐ˜๋“œ์‹œ ์ธ๋ฑ์Šค๋ฅผ ๊ฑธ์ž!
// ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
Schema::table('votes', function (Blueprint $table) {
    $table->index(['proposal_id', 'voter_address']);
    $table->index(['proposal_id', 'voted_at']);
});
2. Redis ์บ์‹ฑ

๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ๋Š” ๋А๋ฆฌ๋‹ˆ๊นŒ Redis๋กœ ์บ์‹ฑํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ. ํŠนํžˆ ํ† ํฐ ์ด ๊ณต๊ธ‰๋Ÿ‰, ์ œ์•ˆ์„œ ๋ชฉ๋ก ๊ฐ™์€ ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹ฑ ํ•„์ˆ˜! ๐Ÿš€
// ์บ์‹ฑ ์˜ˆ์‹œ
public function getActiveProposals()
{
    return Cache::remember('active_proposals', 300, function () {
        return Proposal::active()
                      ->with('votes')
                      ->get();
    });
}

// ํˆฌํ‘œ ํ›„ ์บ์‹œ ๋ฌดํšจํ™”
public function afterVote(Proposal $proposal)
{
    Cache::forget('active_proposals');
    Cache::forget("proposal_{$proposal->id}_stats");
}
3. ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜

๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฆฌ์Šค๋‹ํ•ด์„œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์„ฑ์ด ํ–ฅ์ƒ๋ผ. Web3.js์˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ํ™œ์šฉํ•˜์ž! ๐Ÿ‘‚
// ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹
const votingContract = new web3.eth.Contract(abi, address);

votingContract.events.VoteCast({
    fromBlock: 'latest'
})
.on('data', async (event) => {
    const { proposalId, voter, choice, votingPower } = event.returnValues;
    
    // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—…๋ฐ์ดํŠธ
    await axios.post('/api/internal/sync-vote', {
        proposal_id: proposalId,
        voter_address: voter,
        vote_choice: choice,
        voting_power: votingPower,
        transaction_hash: event.transactionHash,
        block_number: event.blockNumber
    });
})
.on('error', console.error);

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

์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์€ ๊ฐ€์Šค๋น„๊ฐ€ ๋น„์‹ธ๋‹ˆ๊นŒ ์ตœ์ ํ™”๊ฐ€ ํ•„์ˆ˜์•ผ! ๋ช‡ ๊ฐ€์ง€ ์ „๋žต์„ ์†Œ๊ฐœํ• ๊ฒŒ. ๐Ÿ’ธ

์ „๋žต ์„ค๋ช… ์ ˆ๊ฐ ํšจ๊ณผ
L2 ์†”๋ฃจ์…˜ ์‚ฌ์šฉ Polygon, Arbitrum, Optimism ๋“ฑ 90% ์ด์ƒ
๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์—ฌ๋Ÿฌ ํˆฌํ‘œ๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ 30-50%
์˜คํ”„์ฒด์ธ ์„œ๋ช… EIP-712 ์„œ๋ช… ํ™œ์šฉ 70-80%
์Šคํ† ๋ฆฌ์ง€ ์ตœ์ ํ™” uint256 ๋Œ€์‹  uint128 ์‚ฌ์šฉ 10-20%
๐ŸŽฏ ์˜คํ”„์ฒด์ธ ์„œ๋ช… ํˆฌํ‘œ

์‚ฌ์šฉ์ž๊ฐ€ ๋ฉ”ํƒ€๋งˆ์Šคํฌ๋กœ ์„œ๋ช…๋งŒ ํ•˜๊ณ , ์‹ค์ œ ํŠธ๋žœ์žญ์…˜์€ ์„œ๋ฒ„์—์„œ ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. ๊ฐ€์Šค๋น„๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹Œ DAO ์žฌ๋ฌด์—์„œ ๋ถ€๋‹ดํ•  ์ˆ˜ ์žˆ์–ด์„œ UX๊ฐ€ ํฌ๊ฒŒ ๊ฐœ์„ ๋ผ! ์ด๊ฑธ "Gasless Transaction" ๋˜๋Š” "Meta Transaction"์ด๋ผ๊ณ  ํ•ด. ๐ŸŽซ

๐ŸŽจ UX ๊ฐœ์„  ์•„์ด๋””์–ด

๊ธฐ์ˆ ์ ์œผ๋กœ ์™„๋ฒฝํ•ด๋„ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜์˜๋ฉด ์•„๋ฌด๋„ ์•ˆ ์จ. ์‹ค์ œ ์‚ฌ์šฉ์ž๋“ค์ด ์ข‹์•„ํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐Ÿ˜Š

โœจ ์ถ”์ฒœ ๊ธฐ๋Šฅ๋“ค

โ€ข ํˆฌํ‘œ ์œ„์ž„: ๋ฐ”์œ ์‚ฌ์šฉ์ž๋Š” ์‹ ๋ขฐํ•˜๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ํˆฌํ‘œ๊ถŒ์„ ์œ„์ž„ํ•  ์ˆ˜ ์žˆ์–ด
โ€ข ํ† ๋ก  ํฌ๋Ÿผ: ์ œ์•ˆ์„œ์— ๋Œ€ํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ† ๋ก  ๊ณต๊ฐ„
โ€ข ์•Œ๋ฆผ ์‹œ์Šคํ…œ: ์ƒˆ ์ œ์•ˆ, ํˆฌํ‘œ ๋งˆ๊ฐ ์ž„๋ฐ• ๋“ฑ์„ ์ด๋ฉ”์ผ/ํ…”๋ ˆ๊ทธ๋žจ์œผ๋กœ ์•Œ๋ฆผ
โ€ข ํˆฌํ‘œ ํžˆ์Šคํ† ๋ฆฌ: ๊ณผ๊ฑฐ ํˆฌํ‘œ ๋‚ด์—ญ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋Œ€์‹œ๋ณด๋“œ
โ€ข ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์ œ์•ˆ์ด ํ†ต๊ณผ๋˜๋ฉด ์–ด๋–ค ๋ณ€ํ™”๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ
โ€ข ๋ชจ๋ฐ”์ผ ์•ฑ: ์–ธ์ œ ์–ด๋””์„œ๋‚˜ ํˆฌํ‘œํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ์ œ๊ณต

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ DAO ๊ฑฐ๋ฒ„๋„Œ์Šค ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•˜๋ฉด ์ •๋ง ๋ฉ‹์งˆ ๊ฒƒ ๊ฐ™์•„! ์‚ฌ์šฉ์ž๋“ค์ด ์ง์ ‘ ํ”Œ๋žซํผ์˜ ์ •์ฑ…์„ ๊ฒฐ์ •ํ•˜๊ณ , ์ˆ˜์ˆ˜๋ฃŒ ๊ตฌ์กฐ๋ฅผ ํˆฌํ‘œ๋กœ ์ •ํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ •ํ•  ์ˆ˜ ์žˆ์ž–์•„? ์™„์ „ ํ˜์‹ ์ ์ด์ง€! ๐Ÿš€

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ

DAO์™€ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๋Š” ๋ถ„์•ผ์•ผ. ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฆฌ์†Œ์Šค๋“ค์„ ํ™œ์šฉํ•ด๋ด! ๐Ÿ“š

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

๊ณต์‹ ๋ฌธ์„œ
โ€ข Laravel ๊ณต์‹ ๋ฌธ์„œ: https://laravel.com/docs
โ€ข Web3.js ๋ฌธ์„œ: https://web3js.readthedocs.io
โ€ข Solidity ๋ฌธ์„œ: https://docs.soliditylang.org
โ€ข OpenZeppelin Contracts: https://docs.openzeppelin.com

์˜จ๋ผ์ธ ๊ฐ•์˜
โ€ข Udemy: "Ethereum and Solidity: The Complete Developer's Guide"
โ€ข Coursera: "Blockchain Specialization"
โ€ข YouTube: Patrick Collins, Dapp University ์ฑ„๋„

์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข Discord: Laravel, Ethereum, Polygon ๊ณต์‹ ์„œ๋ฒ„
โ€ข Reddit: r/ethdev, r/laravel
โ€ข Stack Overflow: ethereum, laravel ํƒœ๊ทธ
โ€ข GitHub: ์˜คํ”ˆ์†Œ์Šค DAO ํ”„๋กœ์ ํŠธ๋“ค ์ฐธ๊ณ 

๐ŸŒŸ ์œ ๋ช… DAO ํ”„๋กœ์ ํŠธ ๋ถ„์„

์‹ค์ œ ์šด์˜ ์ค‘์ธ DAO๋“ค์„ ๋ถ„์„ํ•˜๋ฉด ๋งŽ์€ ๊ฑธ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด. ๊ฐ ํ”„๋กœ์ ํŠธ์˜ GitHub ์ €์žฅ์†Œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์‹ค์ „ ์ฝ”๋“œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์ง€! ๐Ÿ”

๐Ÿ† ๋ฒค์น˜๋งˆํ‚นํ•  ๋งŒํ•œ ํ”„๋กœ์ ํŠธ๋“ค

โ€ข Compound: ๊ฐ€์žฅ ์„ฑ๊ณต์ ์ธ DeFi DAO ์ค‘ ํ•˜๋‚˜. ๊ฑฐ๋ฒ„๋„Œ์Šค ํ† ํฐ COMP๋กœ ํ”„๋กœํ† ์ฝœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํˆฌํ‘œ๋กœ ๊ฒฐ์ •ํ•ด.

โ€ข Uniswap: UNI ํ† ํฐ ํ™€๋”๋“ค์ด ํ”„๋กœํ† ์ฝœ ์—…๊ทธ๋ ˆ์ด๋“œ์™€ ์ˆ˜์ˆ˜๋ฃŒ ์ •์ฑ…์„ ๊ฒฐ์ •. ์œ„์ž„ ํˆฌํ‘œ ์‹œ์Šคํ…œ์ด ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์–ด.

โ€ข Snapshot: ์˜คํ”„์ฒด์ธ ํˆฌํ‘œ ํ”Œ๋žซํผ. ๊ฐ€์Šค๋น„ ์—†์ด ํˆฌํ‘œํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋งŽ์€ DAO๋“ค์ด ์‚ฌ์šฉํ•ด. IPFS ๊ธฐ๋ฐ˜์ด๋ผ ์™„์ „ ํƒˆ์ค‘์•™ํ™”์•ผ.

โ€ข Aragon: DAO ์ƒ์„ฑ ํ”Œ๋žซํผ. ์ฝ”๋“œ ์—†์ด๋„ DAO๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋…ธ์ฝ”๋“œ ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•ด.

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

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ๋ผ๋ผ๋ฒจ๋กœ DAO ํˆฌํ‘œ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•˜๋Š” ์ „์ฒด ๊ณผ์ •์„ ์‚ดํŽด๋ดค์–ด! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜…

์šฐ๋ฆฌ๊ฐ€ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด:

โœ… DAO์˜ ๊ฐœ๋…๊ณผ ์™œ ์ค‘์š”ํ•œ์ง€
โœ… ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ (์˜จ์ฒด์ธ + ์˜คํ”„์ฒด์ธ)
โœ… ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ (๋ชจ๋ธ, ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค)
โœ… ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑ ๋ฐ ๋ณด์•ˆ
โœ… Vue.js ํ”„๋ก ํŠธ์—”๋“œ์™€ Web3 ์—ฐ๋™
โœ… ์Šค์ผ€์ค„๋Ÿฌ์™€ ํ๋กœ ์ž๋™ํ™”
โœ… ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ๊ณผ ์ตœ์ ํ™”
โœ… ๋ฐฐํฌ ๋ฐ ์šด์˜ ์ „๋žต

์ด ๋ชจ๋“  ๊ฑธ ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ์‹œ๊ฐ„์ด ์ข€ ๊ฑธ๋ฆฌ๊ฒ ์ง€๋งŒ, ํ•œ ๋‹จ๊ณ„์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ง„ํ–‰ํ•˜๋ฉด ์ถฉ๋ถ„ํžˆ ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช

ํŠนํžˆ ๊ธฐ์–ตํ•ด์•ผ ํ•  ํ•ต์‹ฌ ํฌ์ธํŠธ๋Š”:

1. ๋ณด์•ˆ์ด ์ตœ์šฐ์„ ์ด์•ผ. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ํ•œ ๋ฒˆ ๋ฐฐํฌํ•˜๋ฉด ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ˆ๊นŒ ๋ฐฐํฌ ์ „์— ์ฒ ์ €ํ•œ ๊ฐ์‚ฌ๊ฐ€ ํ•„์ˆ˜์•ผ. ๐Ÿ”’

2. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด. ์•„๋ฌด๋ฆฌ ๊ธฐ์ˆ ์ ์œผ๋กœ ์™„๋ฒฝํ•ด๋„ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์šฐ๋ฉด ์•„๋ฌด๋„ ์•ˆ ์จ. ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ, ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…, ๊ฒฐ๊ณผ ํ™•์ธ๊นŒ์ง€ ๋ชจ๋“  ๊ณผ์ •์ด ์ง๊ด€์ ์ด์–ด์•ผ ํ•ด. ๐ŸŽจ

3. ์ ์ง„์  ํƒˆ์ค‘์•™ํ™”๋ฅผ ์ถ”๊ตฌํ•˜์ž. ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„์ „ํ•œ ํƒˆ์ค‘์•™ํ™”๋ฅผ ๋ชฉํ‘œ๋กœ ํ•˜๋ฉด ๋ณต์žก๋„๊ฐ€ ๋„ˆ๋ฌด ๋†’์•„์ ธ. ์ดˆ๊ธฐ์—๋Š” ์ค‘์•™ํ™”๋œ ์š”์†Œ๋ฅผ ์ผ๋ถ€ ๋‚จ๊ฒจ๋‘๊ณ , ์‹œ์Šคํ…œ์ด ์•ˆ์ •ํ™”๋˜๋ฉด ์ ์  ๋” ํƒˆ์ค‘์•™ํ™”ํ•˜๋Š” ๊ฒŒ ํ˜„์‹ค์ ์ด์•ผ. ๐Ÿ“ˆ

4. ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ํ•ต์‹ฌ์ด์•ผ. DAO๋Š” ๊ฒฐ๊ตญ ์‚ฌ๋žŒ๋“ค์˜ ์ฐธ์—ฌ๋กœ ์ž‘๋™ํ•˜๋Š” ์‹œ์Šคํ…œ์ด๊ฑฐ๋“ . ํ™œ๋ฐœํ•œ ํ† ๋ก ๊ณผ ํˆฌ๋ช…ํ•œ ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ •์ด DAO์˜ ์„ฑ๊ณต์„ ์ขŒ์šฐํ•ด. ๐Ÿ‘ฅ

๋ธ”๋ก์ฒด์ธ๊ณผ Web3๋Š” ์ •๋ง ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•˜๋Š” ๋ถ„์•ผ์•ผ. ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ๋„ ๋ช‡ ๋‹ฌ ํ›„๋ฉด ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์ด ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์–ด. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ํ•ญ์ƒ ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ์ฃผ์‹œํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์†Œํ†ตํ•˜๋ฉด์„œ ๊ณ„์† ๋ฐฐ์›Œ๋‚˜๊ฐ€๋Š” ๊ฒŒ ์ค‘์š”ํ•ด! ๐Ÿš€

๋งˆ์ง€๋ง‰์œผ๋กœ, ์ด ๊ธ€์ด ์—ฌ๋Ÿฌ๋ถ„์˜ DAO ํ”„๋กœ์ ํŠธ์— ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“ ์ง€ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด๋ด. ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๋Š” ์ •๋ง ์นœ์ ˆํ•˜๊ณ  ๋„์›€์„ ์ž˜ ์ฃผ๊ฑฐ๋“ ! ๐Ÿ˜Š

๊ทธ๋Ÿผ ์—ฌ๋Ÿฌ๋ถ„์˜ ๋ฉ‹์ง„ DAO ํ”„๋กœ์ ํŠธ๋ฅผ ์‘์›ํ• ๊ฒŒ! ํ™”์ดํŒ…! ๐ŸŽ‰

๐ŸŽ‰ Happy Coding! Build the Future of Decentralized Governance ๐Ÿ—ณ๏ธ ๐Ÿ” โšก ๐Ÿš€ ๐Ÿ’Ž
๋Œ“๊ธ€ ์ž‘์„ฑ

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

๋Œ“๊ธ€ 0