์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ ํŒŒ์ด์ฌ๊ณผ SQLite๋กœ NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ: ๋ธ”๋ก์ฒด์ธ ์‹œ๋Œ€์˜ ์‹ค์ „ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„

๐ŸŽจ ํŒŒ์ด์ฌ๊ณผ SQLite๋กœ NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ: ๋ธ”๋ก์ฒด์ธ ์‹œ๋Œ€์˜ ์‹ค์ „ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„

Web3 ์‹œ๋Œ€, NFT ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ์•Œ๋ ค๋“œ๋ฆฝ๋‹ˆ๋‹ค! ๐Ÿ’Ž

๐Ÿš€ NFT ์‹œ์žฅ๊ณผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์˜ ์ค‘์š”์„ฑ

์•ˆ๋…•! ์š”์ฆ˜ NFT ์‹œ์žฅ ์ •๋ง ํ•ซํ•˜์ง€? ๐Ÿ”ฅ ๋””์ง€ํ„ธ ์•„ํŠธ๋ถ€ํ„ฐ ๊ฒŒ์ž„ ์•„์ดํ…œ, ์‹ฌ์ง€์–ด ๋ถ€๋™์‚ฐ๊นŒ์ง€ NFT๋กœ ๊ฑฐ๋ž˜๋˜๋Š” ์‹œ๋Œ€๊ฐ€ ์™”์–ด. ๊ทธ๋Ÿฐ๋ฐ ๋ง์ด์•ผ, NFT ํ”„๋กœ์ ํŠธ๋ฅผ ์šด์˜ํ•˜๊ฑฐ๋‚˜ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒŒ ๋ญ”์ง€ ์•Œ์•„? ๋ฐ”๋กœ ํŒ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฑฐ์•ผ!

๋ธ”๋ก์ฒด์ธ์—๋Š” ๊ฑฐ๋ž˜ ๊ธฐ๋ก์ด ๋‚จ์ง€๋งŒ, ์‹ค์ œ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค๋ฅผ ์šด์˜ํ•˜๋ ค๋ฉด ๋” ๋งŽ์€ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•ด. ๋ˆ„๊ฐ€ ์–ผ๋งˆ์— ์ƒ€๋Š”์ง€, ์–ด๋–ค NFT๊ฐ€ ์ธ๊ธฐ ์žˆ๋Š”์ง€, ์ˆ˜์ˆ˜๋ฃŒ๋Š” ์–ผ๋งˆ๋‚˜ ๋ฐœ์ƒํ–ˆ๋Š”์ง€... ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋“ค์„ ํšจ์œจ์ ์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ฑฐ๋“ . ๐ŸŽฏ

๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ ํŒŒ์ด์ฌ๊ณผ SQLite๋ฅผ ํ™œ์šฉํ•ด์„œ NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ. ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ํ•˜๋‚˜์”ฉ ๋”ฐ๋ผ์˜ค๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์‰ฌ์›Œ! ์žฌ๋Šฅ๋„ท์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๋Š” ๋ถ„๋“ค๋„ ์ด๋Ÿฐ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋…ธํ•˜์šฐ๋ฅผ ๋งŽ์ด ์ฐพ๋”๋ผ๊ณ . ๐Ÿ˜Š

NFT Data Token ID Owner Price SQLite Database Analysis Reports Statistics Insights NFT Sales Data Management System

๐Ÿ’ก ์™œ SQLite๋ฅผ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ?

NFT ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ๋•Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ ํƒ์ด ์ •๋ง ์ค‘์š”ํ•ด. MySQL, PostgreSQL, MongoDB ๋“ฑ ์—ฌ๋Ÿฌ ์„ ํƒ์ง€๊ฐ€ ์žˆ์ง€๋งŒ, ๋‚˜๋Š” SQLite๋ฅผ ์ถ”์ฒœํ•ด. ์™œ๋ƒ๊ณ ? ๐Ÿค”

๐ŸŽฏ SQLite์˜ ๊ฐ•๋ ฅํ•œ ์žฅ์ ๋“ค

1. ์„ค์น˜๊ฐ€ ํ•„์š” ์—†์–ด! ๐ŸŽ‰
SQLite๋Š” ์„œ๋ฒ„๋ฆฌ์Šค(Serverless) ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์•ผ. ๋ณ„๋„์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋ฒ„๋ฅผ ์„ค์น˜ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์–ด. ํŒŒ์ด์ฌ์— ๊ธฐ๋ณธ์œผ๋กœ ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด์„œ import๋งŒ ํ•˜๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€. ์ดˆ๋ณด์ž๋“ค์—๊ฒŒ ์ •๋ง ์นœํ™”์ ์ด์•ผ!

2. ๊ฐ€๋ณ๊ณ  ๋น ๋ฅด๋‹ค! โšก
ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ๊ด€๋ฆฌ๋ผ. ์ž‘์€ ํ”„๋กœ์ ํŠธ๋‚˜ ์ค‘๊ฐ„ ๊ทœ๋ชจ์˜ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์—๋Š” ์ถฉ๋ถ„ํ•œ ์„ฑ๋Šฅ์„ ์ œ๊ณตํ•ด. ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์˜ ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ๋„ ๋ฌธ์ œ์—†์ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.

3. ์ด์‹์„ฑ์ด ๋›ฐ์–ด๋‚˜! ๐Ÿ“ฆ
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ ํ•˜๋‚˜๋งŒ ๋ณต์‚ฌํ•˜๋ฉด ๋‹ค๋ฅธ ํ™˜๊ฒฝ์œผ๋กœ ์‰ฝ๊ฒŒ ์ด๋™ํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ณ  ํ”„๋กœ๋•์…˜์œผ๋กœ ์˜ฎ๊ธฐ๊ธฐ๋„ ๊ฐ„ํŽธํ•˜์ง€.

4. ํŠธ๋žœ์žญ์…˜์„ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›! ๐Ÿ”’
ACID(์›์ž์„ฑ, ์ผ๊ด€์„ฑ, ๊ณ ๋ฆฝ์„ฑ, ์ง€์†์„ฑ) ์†์„ฑ์„ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•ด์„œ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์ด ๋ณด์žฅ๋ผ. NFT ๊ฑฐ๋ž˜์ฒ˜๋Ÿผ ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ํ•„์ˆ˜์ ์ธ ๊ธฐ๋Šฅ์ด์•ผ.

๐Ÿ’ก Tip: ๋ฌผ๋ก  SQLite๋„ ํ•œ๊ณ„๋Š” ์žˆ์–ด. ๋™์‹œ์— ์ˆ˜์ฒœ ๋ช…์ด ์ ‘์†ํ•˜๋Š” ๋Œ€๊ทœ๋ชจ ์„œ๋น„์Šค๋ผ๋ฉด PostgreSQL์ด๋‚˜ MySQL์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด. ํ•˜์ง€๋งŒ ์‹œ์ž‘ ๋‹จ๊ณ„๋‚˜ ์ค‘์†Œ ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—๋Š” SQLite๊ฐ€ ์ตœ๊ณ ์˜ ์„ ํƒ์ด์•ผ!

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

๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋”ฉํ•˜๊ธฐ ์ „์— ์‹œ์Šคํ…œ ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ์„ค๊ณ„ํ•ด๋ณด์ž. ์ข‹์€ ์„ค๊ณ„๋Š” ๋‚˜์ค‘์— ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜! ๐Ÿ“

๐Ÿ“Š ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ

NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ด๋Ÿฐ ํ…Œ์ด๋ธ”๋“ค์ด ํ•„์š”ํ•ด:

1. NFT ์ปฌ๋ ‰์…˜ ํ…Œ์ด๋ธ” (collections)
NFT ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ๋ณธ ์ •๋ณด๋ฅผ ์ €์žฅํ•ด. ์ปฌ๋ ‰์…˜ ์ด๋ฆ„, ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ, ์ƒ์„ฑ์ผ, ์ด ๋ฐœํ–‰๋Ÿ‰ ๋“ฑ์„ ๊ด€๋ฆฌํ•˜์ง€.

2. NFT ์•„์ดํ…œ ํ…Œ์ด๋ธ” (nft_items)
๊ฐœ๋ณ„ NFT์˜ ์ƒ์„ธ ์ •๋ณด์•ผ. ํ† ํฐ ID, ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ URI, ํ˜„์žฌ ์†Œ์œ ์ž, ํฌ๊ท€๋„ ๋“ฑ์„ ์ €์žฅํ•ด.

3. ํŒ๋งค ๊ฑฐ๋ž˜ ํ…Œ์ด๋ธ” (sales_transactions)
์‹ค์ œ ํŒ๋งค ๊ธฐ๋ก์„ ์ €์žฅํ•˜๋Š” ํ•ต์‹ฌ ํ…Œ์ด๋ธ”์ด์•ผ. ํŒ๋งค์ž, ๊ตฌ๋งค์ž, ๊ฐ€๊ฒฉ, ๊ฑฐ๋ž˜ ์‹œ๊ฐ„, ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ๋“ฑ์„ ๊ธฐ๋กํ•ด.

4. ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ” (users)
NFT๋ฅผ ๊ฑฐ๋ž˜ํ•˜๋Š” ์‚ฌ์šฉ์ž๋“ค์˜ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•ด. ์ง€๊ฐ‘ ์ฃผ์†Œ, ๋‹‰๋„ค์ž„, ๊ฐ€์ž…์ผ ๋“ฑ์„ ์ €์žฅํ•˜์ง€.

5. ๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ ํ…Œ์ด๋ธ” (price_history)
NFT์˜ ๊ฐ€๊ฒฉ ๋ณ€๋™์„ ์ถ”์ ํ•ด. ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๊ฐ€๊ฒฉ ๋ณ€ํ™”๋ฅผ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ”— ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„ ์„ค์ •

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด ํ…Œ์ด๋ธ” ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๊ฑฐ์•ผ. ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์—์„œ๋Š” ์ด๋Ÿฐ ๊ด€๊ณ„๋“ค์ด ์žˆ์–ด:

๐Ÿ”ธ ์ปฌ๋ ‰์…˜ โ†” NFT ์•„์ดํ…œ: 1:N ๊ด€๊ณ„ (ํ•˜๋‚˜์˜ ์ปฌ๋ ‰์…˜์— ์—ฌ๋Ÿฌ NFT๊ฐ€ ์†ํ•จ)
๐Ÿ”ธ NFT ์•„์ดํ…œ โ†” ํŒ๋งค ๊ฑฐ๋ž˜: 1:N ๊ด€๊ณ„ (ํ•˜๋‚˜์˜ NFT๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ๊ฑฐ๋ž˜๋  ์ˆ˜ ์žˆ์Œ)
๐Ÿ”ธ ์‚ฌ์šฉ์ž โ†” ํŒ๋งค ๊ฑฐ๋ž˜: 1:N ๊ด€๊ณ„ (ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๊ฑฐ๋ž˜๋ฅผ ํ•  ์ˆ˜ ์žˆ์Œ)
๐Ÿ”ธ NFT ์•„์ดํ…œ โ†” ๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ: 1:N ๊ด€๊ณ„ (ํ•˜๋‚˜์˜ NFT์— ์—ฌ๋Ÿฌ ๊ฐ€๊ฒฉ ๊ธฐ๋ก)

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ…ํ•˜๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์ค€๋น„ํ•ด๋ณด์ž. ๐Ÿ˜Š

Step 1: ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

ํŒŒ์ด์ฌ 3.7 ์ด์ƒ ๋ฒ„์ „์ด ์„ค์น˜๋˜์–ด ์žˆ๋‹ค๋ฉด SQLite๋Š” ์ด๋ฏธ ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด. ์ถ”๊ฐ€๋กœ ๋ช‡ ๊ฐ€์ง€ ์œ ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ์„ค์น˜ํ•˜๋ฉด ๋ผ:

pip install web3
pip install python-dotenv
pip install pandas
pip install matplotlib

๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์—ญํ• :
โ€ข web3: ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข python-dotenv: ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ (API ํ‚ค, ํ”„๋ผ์ด๋น— ํ‚ค ๋“ฑ)
โ€ข pandas: ๋ฐ์ดํ„ฐ ๋ถ„์„ ๋ฐ ์ฒ˜๋ฆฌ
โ€ข matplotlib: ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”

Step 2: ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

๊น”๋”ํ•œ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋Š” ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜. ์ด๋ ‡๊ฒŒ ํด๋”๋ฅผ ๊ตฌ์„ฑํ•ด๋ณด์ž:

nft_sales_manager/
โ”‚
โ”œโ”€โ”€ database/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ db_manager.py
โ”‚   โ””โ”€โ”€ models.py
โ”‚
โ”œโ”€โ”€ services/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ nft_service.py
โ”‚   โ””โ”€โ”€ sales_service.py
โ”‚
โ”œโ”€โ”€ utils/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ””โ”€โ”€ helpers.py
โ”‚
โ”œโ”€โ”€ config/
โ”‚   โ””โ”€โ”€ settings.py
โ”‚
โ”œโ”€โ”€ main.py
โ”œโ”€โ”€ requirements.txt
โ””โ”€โ”€ .env

๐Ÿ’ป ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ ๊ตฌํ˜„ํ•˜๊ธฐ

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

๐Ÿ“ database/db_manager.py - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งค๋‹ˆ์ € ํด๋ž˜์Šค

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ๊ณผ ๊ธฐ๋ณธ ์ž‘์—…์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import sqlite3
from datetime import datetime
import logging

class DatabaseManager:
    def __init__(self, db_path='nft_sales.db'):
        """
        ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™”
        
        Args:
            db_path: SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ ๊ฒฝ๋กœ
        """
        self.db_path = db_path
        self.connection = None
        self.cursor = None
        self.logger = logging.getLogger(__name__)
        
    def connect(self):
        """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ"""
        try:
            self.connection = sqlite3.connect(self.db_path)
            self.connection.row_factory = sqlite3.Row  # ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ๋กœ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
            self.cursor = self.connection.cursor()
            self.logger.info(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต: {self.db_path}")
            return True
        except sqlite3.Error as e:
            self.logger.error(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ: {e}")
            return False
    
    def disconnect(self):
        """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ข…๋ฃŒ"""
        if self.connection:
            self.connection.close()
            self.logger.info("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ข…๋ฃŒ")
    
    def create_tables(self):
        """๋ชจ๋“  ํ…Œ์ด๋ธ” ์ƒ์„ฑ"""
        try:
            # ์ปฌ๋ ‰์…˜ ํ…Œ์ด๋ธ”
            self.cursor.execute('''
                CREATE TABLE IF NOT EXISTS collections (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL,
                    contract_address TEXT UNIQUE NOT NULL,
                    description TEXT,
                    creator_address TEXT,
                    total_supply INTEGER,
                    floor_price REAL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            
            # NFT ์•„์ดํ…œ ํ…Œ์ด๋ธ”
            self.cursor.execute('''
                CREATE TABLE IF NOT EXISTS nft_items (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    collection_id INTEGER NOT NULL,
                    token_id TEXT NOT NULL,
                    name TEXT,
                    description TEXT,
                    metadata_uri TEXT,
                    image_uri TEXT,
                    current_owner TEXT,
                    rarity_score REAL,
                    attributes TEXT,  -- JSON ํ˜•ํƒœ๋กœ ์ €์žฅ
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (collection_id) REFERENCES collections(id),
                    UNIQUE(collection_id, token_id)
                )
            ''')
            
            # ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”
            self.cursor.execute('''
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    wallet_address TEXT UNIQUE NOT NULL,
                    username TEXT,
                    email TEXT,
                    profile_image TEXT,
                    total_purchases INTEGER DEFAULT 0,
                    total_sales INTEGER DEFAULT 0,
                    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            
            # ํŒ๋งค ๊ฑฐ๋ž˜ ํ…Œ์ด๋ธ”
            self.cursor.execute('''
                CREATE TABLE IF NOT EXISTS sales_transactions (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nft_item_id INTEGER NOT NULL,
                    seller_id INTEGER NOT NULL,
                    buyer_id INTEGER NOT NULL,
                    price REAL NOT NULL,
                    currency TEXT DEFAULT 'ETH',
                    platform_fee REAL DEFAULT 0,
                    royalty_fee REAL DEFAULT 0,
                    transaction_hash TEXT UNIQUE,
                    block_number INTEGER,
                    sale_type TEXT,  -- 'direct', 'auction', 'offer'
                    status TEXT DEFAULT 'completed',
                    sold_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (nft_item_id) REFERENCES nft_items(id),
                    FOREIGN KEY (seller_id) REFERENCES users(id),
                    FOREIGN KEY (buyer_id) REFERENCES users(id)
                )
            ''')
            
            # ๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ ํ…Œ์ด๋ธ”
            self.cursor.execute('''
                CREATE TABLE IF NOT EXISTS price_history (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nft_item_id INTEGER NOT NULL,
                    price REAL NOT NULL,
                    currency TEXT DEFAULT 'ETH',
                    event_type TEXT,  -- 'sale', 'listing', 'offer'
                    recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (nft_item_id) REFERENCES nft_items(id)
                )
            ''')
            
            # ์ธ๋ฑ์Šค ์ƒ์„ฑ (๊ฒ€์ƒ‰ ์„ฑ๋Šฅ ํ–ฅ์ƒ)
            self.cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_nft_collection 
                ON nft_items(collection_id)
            ''')
            
            self.cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_sales_nft 
                ON sales_transactions(nft_item_id)
            ''')
            
            self.cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_sales_date 
                ON sales_transactions(sold_at)
            ''')
            
            self.connection.commit()
            self.logger.info("๋ชจ๋“  ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ")
            return True
            
        except sqlite3.Error as e:
            self.logger.error(f"ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹คํŒจ: {e}")
            return False
    
    def execute_query(self, query, params=None):
        """์ฟผ๋ฆฌ ์‹คํ–‰ (SELECT)"""
        try:
            if params:
                self.cursor.execute(query, params)
            else:
                self.cursor.execute(query)
            return self.cursor.fetchall()
        except sqlite3.Error as e:
            self.logger.error(f"์ฟผ๋ฆฌ ์‹คํ–‰ ์‹คํŒจ: {e}")
            return None
    
    def execute_update(self, query, params=None):
        """์ฟผ๋ฆฌ ์‹คํ–‰ (INSERT, UPDATE, DELETE)"""
        try:
            if params:
                self.cursor.execute(query, params)
            else:
                self.cursor.execute(query)
            self.connection.commit()
            return self.cursor.lastrowid
        except sqlite3.Error as e:
            self.logger.error(f"์—…๋ฐ์ดํŠธ ์‹คํŒจ: {e}")
            self.connection.rollback()
            return None

์ด ํด๋ž˜์Šค๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ชจ๋“  ๊ธฐ๋ณธ ์ž‘์—…์„ ๊ด€๋ฆฌํ•ด. ์—ฐ๊ฒฐ, ํ…Œ์ด๋ธ” ์ƒ์„ฑ, ์ฟผ๋ฆฌ ์‹คํ–‰ ๋“ฑ์„ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€! ๐ŸŽฏ

โœ… ์„ค๊ณ„ ํฌ์ธํŠธ: row_factory๋ฅผ sqlite3.Row๋กœ ์„ค์ •ํ•˜๋ฉด ๊ฒฐ๊ณผ๋ฅผ ๋”•์…”๋„ˆ๋ฆฌ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์ด ํ›จ์”ฌ ์ข‹์•„์ ธ. ๊ทธ๋ฆฌ๊ณ  ์ธ๋ฑ์Šค๋ฅผ ์ ์ ˆํžˆ ์ƒ์„ฑํ•˜๋ฉด ๊ฒ€์ƒ‰ ์†๋„๊ฐ€ ์—„์ฒญ ๋นจ๋ผ์ง„๋‹ค๋Š” ์ ๋„ ๊ธฐ์–ตํ•ด!

๐ŸŽจ NFT ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๊ตฌํ˜„ํ•˜๊ธฐ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ธฐ๋ฐ˜์ด ์™„์„ฑ๋์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์ด ๋ถ€๋ถ„์ด ์ง„์งœ ์žฌ๋ฏธ์žˆ์–ด. ๐Ÿš€

๐Ÿ–ผ๏ธ services/nft_service.py - NFT ๊ด€๋ฆฌ ์„œ๋น„์Šค

import json
from datetime import datetime
from typing import Optional, List, Dict

class NFTService:
    def __init__(self, db_manager):
        """
        NFT ์„œ๋น„์Šค ์ดˆ๊ธฐํ™”
        
        Args:
            db_manager: DatabaseManager ์ธ์Šคํ„ด์Šค
        """
        self.db = db_manager
    
    def create_collection(self, name: str, contract_address: str, 
                         description: str = None, creator_address: str = None,
                         total_supply: int = None) -> Optional[int]:
        """
        ์ƒˆ๋กœ์šด NFT ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ
        
        Args:
            name: ์ปฌ๋ ‰์…˜ ์ด๋ฆ„
            contract_address: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ
            description: ์ปฌ๋ ‰์…˜ ์„ค๋ช…
            creator_address: ์ œ์ž‘์ž ์ง€๊ฐ‘ ์ฃผ์†Œ
            total_supply: ์ด ๋ฐœํ–‰๋Ÿ‰
            
        Returns:
            ์ƒ์„ฑ๋œ ์ปฌ๋ ‰์…˜ ID ๋˜๋Š” None
        """
        query = '''
            INSERT INTO collections 
            (name, contract_address, description, creator_address, total_supply)
            VALUES (?, ?, ?, ?, ?)
        '''
        params = (name, contract_address, description, creator_address, total_supply)
        
        collection_id = self.db.execute_update(query, params)
        
        if collection_id:
            print(f"โœ… ์ปฌ๋ ‰์…˜ '{name}' ์ƒ์„ฑ ์™„๋ฃŒ! (ID: {collection_id})")
        else:
            print(f"โŒ ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ ์‹คํŒจ")
            
        return collection_id
    
    def add_nft_item(self, collection_id: int, token_id: str, 
                     name: str = None, description: str = None,
                     metadata_uri: str = None, image_uri: str = None,
                     current_owner: str = None, rarity_score: float = None,
                     attributes: Dict = None) -> Optional[int]:
        """
        NFT ์•„์ดํ…œ ์ถ”๊ฐ€
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            token_id: ํ† ํฐ ID
            name: NFT ์ด๋ฆ„
            description: NFT ์„ค๋ช…
            metadata_uri: ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ URI
            image_uri: ์ด๋ฏธ์ง€ URI
            current_owner: ํ˜„์žฌ ์†Œ์œ ์ž ์ง€๊ฐ‘ ์ฃผ์†Œ
            rarity_score: ํฌ๊ท€๋„ ์ ์ˆ˜
            attributes: ์†์„ฑ ์ •๋ณด (๋”•์…”๋„ˆ๋ฆฌ)
            
        Returns:
            ์ƒ์„ฑ๋œ NFT ์•„์ดํ…œ ID ๋˜๋Š” None
        """
        # ์†์„ฑ์„ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
        attributes_json = json.dumps(attributes) if attributes else None
        
        query = '''
            INSERT INTO nft_items 
            (collection_id, token_id, name, description, metadata_uri, 
             image_uri, current_owner, rarity_score, attributes)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        '''
        params = (collection_id, token_id, name, description, metadata_uri,
                 image_uri, current_owner, rarity_score, attributes_json)
        
        nft_id = self.db.execute_update(query, params)
        
        if nft_id:
            print(f"โœ… NFT '{name}' (Token ID: {token_id}) ์ถ”๊ฐ€ ์™„๋ฃŒ!")
        else:
            print(f"โŒ NFT ์ถ”๊ฐ€ ์‹คํŒจ")
            
        return nft_id
    
    def get_collection_by_address(self, contract_address: str) -> Optional[Dict]:
        """
        ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๋กœ ์ปฌ๋ ‰์…˜ ์กฐํšŒ
        
        Args:
            contract_address: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ
            
        Returns:
            ์ปฌ๋ ‰์…˜ ์ •๋ณด ๋”•์…”๋„ˆ๋ฆฌ ๋˜๋Š” None
        """
        query = '''
            SELECT * FROM collections 
            WHERE contract_address = ?
        '''
        results = self.db.execute_query(query, (contract_address,))
        
        if results:
            return dict(results[0])
        return None
    
    def get_nft_by_token(self, collection_id: int, token_id: str) -> Optional[Dict]:
        """
        ํ† ํฐ ID๋กœ NFT ์กฐํšŒ
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            token_id: ํ† ํฐ ID
            
        Returns:
            NFT ์ •๋ณด ๋”•์…”๋„ˆ๋ฆฌ ๋˜๋Š” None
        """
        query = '''
            SELECT n.*, c.name as collection_name, c.contract_address
            FROM nft_items n
            JOIN collections c ON n.collection_id = c.id
            WHERE n.collection_id = ? AND n.token_id = ?
        '''
        results = self.db.execute_query(query, (collection_id, token_id))
        
        if results:
            nft_data = dict(results[0])
            # JSON ๋ฌธ์ž์—ด์„ ๋”•์…”๋„ˆ๋ฆฌ๋กœ ๋ณ€ํ™˜
            if nft_data.get('attributes'):
                nft_data['attributes'] = json.loads(nft_data['attributes'])
            return nft_data
        return None
    
    def update_nft_owner(self, nft_id: int, new_owner: str) -> bool:
        """
        NFT ์†Œ์œ ์ž ์—…๋ฐ์ดํŠธ
        
        Args:
            nft_id: NFT ์•„์ดํ…œ ID
            new_owner: ์ƒˆ๋กœ์šด ์†Œ์œ ์ž ์ง€๊ฐ‘ ์ฃผ์†Œ
            
        Returns:
            ์„ฑ๊ณต ์—ฌ๋ถ€
        """
        query = '''
            UPDATE nft_items 
            SET current_owner = ?, updated_at = CURRENT_TIMESTAMP
            WHERE id = ?
        '''
        result = self.db.execute_update(query, (new_owner, nft_id))
        
        if result is not None:
            print(f"โœ… NFT ์†Œ์œ ์ž ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ!")
            return True
        else:
            print(f"โŒ NFT ์†Œ์œ ์ž ์—…๋ฐ์ดํŠธ ์‹คํŒจ")
            return False
    
    def get_collection_stats(self, collection_id: int) -> Dict:
        """
        ์ปฌ๋ ‰์…˜ ํ†ต๊ณ„ ์กฐํšŒ
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            
        Returns:
            ํ†ต๊ณ„ ์ •๋ณด ๋”•์…”๋„ˆ๋ฆฌ
        """
        # ์ด NFT ์ˆ˜
        query_total = '''
            SELECT COUNT(*) as total_nfts FROM nft_items 
            WHERE collection_id = ?
        '''
        total_result = self.db.execute_query(query_total, (collection_id,))
        total_nfts = total_result[0]['total_nfts'] if total_result else 0
        
        # ๊ณ ์œ  ์†Œ์œ ์ž ์ˆ˜
        query_owners = '''
            SELECT COUNT(DISTINCT current_owner) as unique_owners 
            FROM nft_items 
            WHERE collection_id = ? AND current_owner IS NOT NULL
        '''
        owners_result = self.db.execute_query(query_owners, (collection_id,))
        unique_owners = owners_result[0]['unique_owners'] if owners_result else 0
        
        # ํ‰๊ท  ํฌ๊ท€๋„ ์ ์ˆ˜
        query_rarity = '''
            SELECT AVG(rarity_score) as avg_rarity 
            FROM nft_items 
            WHERE collection_id = ? AND rarity_score IS NOT NULL
        '''
        rarity_result = self.db.execute_query(query_rarity, (collection_id,))
        avg_rarity = rarity_result[0]['avg_rarity'] if rarity_result else 0
        
        return {
            'total_nfts': total_nfts,
            'unique_owners': unique_owners,
            'avg_rarity_score': round(avg_rarity, 2) if avg_rarity else 0
        }
    
    def search_nfts(self, collection_id: int = None, owner: str = None,
                   min_rarity: float = None, max_rarity: float = None,
                   limit: int = 100) -> List[Dict]:
        """
        NFT ๊ฒ€์ƒ‰
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID (์„ ํƒ)
            owner: ์†Œ์œ ์ž ์ฃผ์†Œ (์„ ํƒ)
            min_rarity: ์ตœ์†Œ ํฌ๊ท€๋„ (์„ ํƒ)
            max_rarity: ์ตœ๋Œ€ ํฌ๊ท€๋„ (์„ ํƒ)
            limit: ๊ฒฐ๊ณผ ์ œํ•œ
            
        Returns:
            NFT ๋ฆฌ์ŠคํŠธ
        """
        query = 'SELECT * FROM nft_items WHERE 1=1'
        params = []
        
        if collection_id:
            query += ' AND collection_id = ?'
            params.append(collection_id)
        
        if owner:
            query += ' AND current_owner = ?'
            params.append(owner)
        
        if min_rarity is not None:
            query += ' AND rarity_score >= ?'
            params.append(min_rarity)
        
        if max_rarity is not None:
            query += ' AND rarity_score <= ?'
            params.append(max_rarity)
        
        query += f' LIMIT {limit}'
        
        results = self.db.execute_query(query, tuple(params))
        
        nfts = []
        if results:
            for row in results:
                nft_data = dict(row)
                if nft_data.get('attributes'):
                    nft_data['attributes'] = json.loads(nft_data['attributes'])
                nfts.append(nft_data)
        
        return nfts

NFT ์„œ๋น„์Šค ํด๋ž˜์Šค๋Š” NFT์™€ ์ปฌ๋ ‰์…˜ ๊ด€๋ จ๋œ ๋ชจ๋“  ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•ด. ์ƒ์„ฑ, ์กฐํšŒ, ์—…๋ฐ์ดํŠธ, ๊ฒ€์ƒ‰๊นŒ์ง€ ๋‹ค ๊ฐ€๋Šฅํ•˜์ง€! ๐Ÿ˜Ž

๐Ÿ’ก Pro Tip: ์†์„ฑ(attributes)์„ JSON ํ˜•ํƒœ๋กœ ์ €์žฅํ•˜๋ฉด ์œ ์—ฐํ•˜๊ฒŒ ๋‹ค์–‘ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. NFT๋งˆ๋‹ค ์†์„ฑ์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ ์ด ๋ฐฉ์‹์ด ์ •๋ง ์œ ์šฉํ•ด!

๐Ÿ’ฐ ํŒ๋งค ๊ฑฐ๋ž˜ ์„œ๋น„์Šค ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„, ํŒ๋งค ๊ฑฐ๋ž˜๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์ด๊ฒŒ ๋ฐ”๋กœ ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ์ด์•ผ. ๐Ÿ’Ž

๐Ÿ’ธ services/sales_service.py - ํŒ๋งค ๊ด€๋ฆฌ ์„œ๋น„์Šค

from datetime import datetime, timedelta
from typing import Optional, List, Dict, Tuple
import statistics

class SalesService:
    def __init__(self, db_manager):
        """
        ํŒ๋งค ์„œ๋น„์Šค ์ดˆ๊ธฐํ™”
        
        Args:
            db_manager: DatabaseManager ์ธ์Šคํ„ด์Šค
        """
        self.db = db_manager
    
    def create_or_get_user(self, wallet_address: str, 
                          username: str = None) -> Optional[int]:
        """
        ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ
        
        Args:
            wallet_address: ์ง€๊ฐ‘ ์ฃผ์†Œ
            username: ์‚ฌ์šฉ์ž ์ด๋ฆ„ (์„ ํƒ)
            
        Returns:
            ์‚ฌ์šฉ์ž ID
        """
        # ๊ธฐ์กด ์‚ฌ์šฉ์ž ํ™•์ธ
        query_check = 'SELECT id FROM users WHERE wallet_address = ?'
        result = self.db.execute_query(query_check, (wallet_address,))
        
        if result:
            return result[0]['id']
        
        # ์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
        query_insert = '''
            INSERT INTO users (wallet_address, username)
            VALUES (?, ?)
        '''
        user_id = self.db.execute_update(query_insert, (wallet_address, username))
        
        if user_id:
            print(f"โœ… ์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์™„๋ฃŒ! (ID: {user_id})")
        
        return user_id
    
    def record_sale(self, nft_item_id: int, seller_address: str,
                   buyer_address: str, price: float, currency: str = 'ETH',
                   platform_fee: float = 0, royalty_fee: float = 0,
                   transaction_hash: str = None, block_number: int = None,
                   sale_type: str = 'direct') -> Optional[int]:
        """
        ํŒ๋งค ๊ฑฐ๋ž˜ ๊ธฐ๋ก
        
        Args:
            nft_item_id: NFT ์•„์ดํ…œ ID
            seller_address: ํŒ๋งค์ž ์ง€๊ฐ‘ ์ฃผ์†Œ
            buyer_address: ๊ตฌ๋งค์ž ์ง€๊ฐ‘ ์ฃผ์†Œ
            price: ํŒ๋งค ๊ฐ€๊ฒฉ
            currency: ํ†ตํ™” (๊ธฐ๋ณธ: ETH)
            platform_fee: ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ
            royalty_fee: ๋กœ์—ดํ‹ฐ ์ˆ˜์ˆ˜๋ฃŒ
            transaction_hash: ๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ
            block_number: ๋ธ”๋ก ๋ฒˆํ˜ธ
            sale_type: ํŒ๋งค ์œ ํ˜• (direct, auction, offer)
            
        Returns:
            ๊ฑฐ๋ž˜ ID ๋˜๋Š” None
        """
        # ํŒ๋งค์ž์™€ ๊ตฌ๋งค์ž ID ๊ฐ€์ ธ์˜ค๊ธฐ
        seller_id = self.create_or_get_user(seller_address)
        buyer_id = self.create_or_get_user(buyer_address)
        
        if not seller_id or not buyer_id:
            print("โŒ ์‚ฌ์šฉ์ž ์ •๋ณด ์ฒ˜๋ฆฌ ์‹คํŒจ")
            return None
        
        # ํŒ๋งค ๊ฑฐ๋ž˜ ๊ธฐ๋ก
        query = '''
            INSERT INTO sales_transactions
            (nft_item_id, seller_id, buyer_id, price, currency,
             platform_fee, royalty_fee, transaction_hash, block_number, sale_type)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        '''
        params = (nft_item_id, seller_id, buyer_id, price, currency,
                 platform_fee, royalty_fee, transaction_hash, block_number, sale_type)
        
        sale_id = self.db.execute_update(query, params)
        
        if sale_id:
            # NFT ์†Œ์œ ์ž ์—…๋ฐ์ดํŠธ
            update_query = '''
                UPDATE nft_items 
                SET current_owner = ?, updated_at = CURRENT_TIMESTAMP
                WHERE id = ?
            '''
            self.db.execute_update(update_query, (buyer_address, nft_item_id))
            
            # ๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ก
            history_query = '''
                INSERT INTO price_history (nft_item_id, price, currency, event_type)
                VALUES (?, ?, ?, 'sale')
            '''
            self.db.execute_update(history_query, (nft_item_id, price, currency))
            
            # ์‚ฌ์šฉ์ž ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
            self._update_user_stats(seller_id, buyer_id)
            
            print(f"โœ… ํŒ๋งค ๊ฑฐ๋ž˜ ๊ธฐ๋ก ์™„๋ฃŒ! (ID: {sale_id})")
            print(f"   ๊ฐ€๊ฒฉ: {price} {currency}")
            print(f"   ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ: {platform_fee} {currency}")
            print(f"   ๋กœ์—ดํ‹ฐ: {royalty_fee} {currency}")
        else:
            print("โŒ ํŒ๋งค ๊ฑฐ๋ž˜ ๊ธฐ๋ก ์‹คํŒจ")
        
        return sale_id
    
    def _update_user_stats(self, seller_id: int, buyer_id: int):
        """์‚ฌ์šฉ์ž ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ (๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ)"""
        # ํŒ๋งค์ž ํ†ต๊ณ„
        query_seller = '''
            UPDATE users 
            SET total_sales = total_sales + 1
            WHERE id = ?
        '''
        self.db.execute_update(query_seller, (seller_id,))
        
        # ๊ตฌ๋งค์ž ํ†ต๊ณ„
        query_buyer = '''
            UPDATE users 
            SET total_purchases = total_purchases + 1
            WHERE id = ?
        '''
        self.db.execute_update(query_buyer, (buyer_id,))
    
    def get_nft_sales_history(self, nft_item_id: int) -> List[Dict]:
        """
        NFT ํŒ๋งค ์ด๋ ฅ ์กฐํšŒ
        
        Args:
            nft_item_id: NFT ์•„์ดํ…œ ID
            
        Returns:
            ํŒ๋งค ์ด๋ ฅ ๋ฆฌ์ŠคํŠธ
        """
        query = '''
            SELECT 
                st.*,
                seller.wallet_address as seller_address,
                buyer.wallet_address as buyer_address,
                n.name as nft_name,
                n.token_id
            FROM sales_transactions st
            JOIN users seller ON st.seller_id = seller.id
            JOIN users buyer ON st.buyer_id = buyer.id
            JOIN nft_items n ON st.nft_item_id = n.id
            WHERE st.nft_item_id = ?
            ORDER BY st.sold_at DESC
        '''
        results = self.db.execute_query(query, (nft_item_id,))
        
        return [dict(row) for row in results] if results else []
    
    def get_user_sales(self, wallet_address: str, 
                      as_seller: bool = True) -> List[Dict]:
        """
        ์‚ฌ์šฉ์ž์˜ ํŒ๋งค/๊ตฌ๋งค ์ด๋ ฅ ์กฐํšŒ
        
        Args:
            wallet_address: ์ง€๊ฐ‘ ์ฃผ์†Œ
            as_seller: True๋ฉด ํŒ๋งค ์ด๋ ฅ, False๋ฉด ๊ตฌ๋งค ์ด๋ ฅ
            
        Returns:
            ๊ฑฐ๋ž˜ ์ด๋ ฅ ๋ฆฌ์ŠคํŠธ
        """
        role = 'seller' if as_seller else 'buyer'
        
        query = f'''
            SELECT 
                st.*,
                n.name as nft_name,
                n.token_id,
                n.image_uri,
                c.name as collection_name,
                other_user.wallet_address as counterparty_address
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            JOIN collections c ON n.collection_id = c.id
            JOIN users u ON st.{role}_id = u.id
            JOIN users other_user ON st.{'buyer' if as_seller else 'seller'}_id = other_user.id
            WHERE u.wallet_address = ?
            ORDER BY st.sold_at DESC
        '''
        results = self.db.execute_query(query, (wallet_address,))
        
        return [dict(row) for row in results] if results else []
    
    def get_collection_sales_stats(self, collection_id: int, 
                                   days: int = 30) -> Dict:
        """
        ์ปฌ๋ ‰์…˜ ํŒ๋งค ํ†ต๊ณ„
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            days: ์กฐํšŒ ๊ธฐ๊ฐ„ (์ผ)
            
        Returns:
            ํ†ต๊ณ„ ์ •๋ณด ๋”•์…”๋„ˆ๋ฆฌ
        """
        start_date = datetime.now() - timedelta(days=days)
        
        query = '''
            SELECT 
                COUNT(*) as total_sales,
                SUM(st.price) as total_volume,
                AVG(st.price) as avg_price,
                MIN(st.price) as floor_price,
                MAX(st.price) as highest_sale
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            WHERE n.collection_id = ? 
            AND st.sold_at >= ?
            AND st.status = 'completed'
        '''
        result = self.db.execute_query(query, (collection_id, start_date))
        
        if result and result[0]['total_sales']:
            stats = dict(result[0])
            
            # ๊ฐ€๊ฒฉ ๋ณ€๋™ ๊ณ„์‚ฐ
            price_trend = self._calculate_price_trend(collection_id, days)
            stats['price_trend'] = price_trend
            
            # ๊ณ ์œ  ๊ฑฐ๋ž˜์ž ์ˆ˜
            unique_traders = self._get_unique_traders(collection_id, days)
            stats['unique_traders'] = unique_traders
            
            return stats
        
        return {
            'total_sales': 0,
            'total_volume': 0,
            'avg_price': 0,
            'floor_price': 0,
            'highest_sale': 0,
            'price_trend': 0,
            'unique_traders': 0
        }
    
    def _calculate_price_trend(self, collection_id: int, days: int) -> float:
        """๊ฐ€๊ฒฉ ์ถ”์„ธ ๊ณ„์‚ฐ (๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ)"""
        query = '''
            SELECT st.price, st.sold_at
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            WHERE n.collection_id = ?
            AND st.sold_at >= datetime('now', '-' || ? || ' days')
            ORDER BY st.sold_at ASC
        '''
        results = self.db.execute_query(query, (collection_id, days))
        
        if not results or len(results) < 2:
            return 0
        
        # ์ฒซ ๊ฑฐ๋ž˜์™€ ๋งˆ์ง€๋ง‰ ๊ฑฐ๋ž˜ ๊ฐ€๊ฒฉ ๋น„๊ต
        first_price = results[0]['price']
        last_price = results[-1]['price']
        
        if first_price == 0:
            return 0
        
        trend = ((last_price - first_price) / first_price) * 100
        return round(trend, 2)
    
    def _get_unique_traders(self, collection_id: int, days: int) -> int:
        """๊ณ ์œ  ๊ฑฐ๋ž˜์ž ์ˆ˜ ๊ณ„์‚ฐ (๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ)"""
        query = '''
            SELECT COUNT(DISTINCT u.wallet_address) as unique_count
            FROM (
                SELECT seller_id as user_id FROM sales_transactions st
                JOIN nft_items n ON st.nft_item_id = n.id
                WHERE n.collection_id = ?
                AND st.sold_at >= datetime('now', '-' || ? || ' days')
                UNION
                SELECT buyer_id as user_id FROM sales_transactions st
                JOIN nft_items n ON st.nft_item_id = n.id
                WHERE n.collection_id = ?
                AND st.sold_at >= datetime('now', '-' || ? || ' days')
            ) traders
            JOIN users u ON traders.user_id = u.id
        '''
        result = self.db.execute_query(query, (collection_id, days, collection_id, days))
        
        return result[0]['unique_count'] if result else 0
    
    def get_top_sales(self, limit: int = 10, days: int = 7) -> List[Dict]:
        """
        ์ตœ๊ณ  ํŒ๋งค ๊ฑฐ๋ž˜ ์กฐํšŒ
        
        Args:
            limit: ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜
            days: ์กฐํšŒ ๊ธฐ๊ฐ„ (์ผ)
            
        Returns:
            ์ตœ๊ณ  ํŒ๋งค ๋ฆฌ์ŠคํŠธ
        """
        start_date = datetime.now() - timedelta(days=days)
        
        query = '''
            SELECT 
                st.*,
                n.name as nft_name,
                n.token_id,
                n.image_uri,
                c.name as collection_name,
                seller.wallet_address as seller_address,
                buyer.wallet_address as buyer_address
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            JOIN collections c ON n.collection_id = c.id
            JOIN users seller ON st.seller_id = seller.id
            JOIN users buyer ON st.buyer_id = buyer.id
            WHERE st.sold_at >= ?
            AND st.status = 'completed'
            ORDER BY st.price DESC
            LIMIT ?
        '''
        results = self.db.execute_query(query, (start_date, limit))
        
        return [dict(row) for row in results] if results else []
    
    def get_price_distribution(self, collection_id: int) -> Dict:
        """
        ๊ฐ€๊ฒฉ ๋ถ„ํฌ ๋ถ„์„
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            
        Returns:
            ๊ฐ€๊ฒฉ ๋ถ„ํฌ ์ •๋ณด
        """
        query = '''
            SELECT price
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            WHERE n.collection_id = ?
            AND st.status = 'completed'
        '''
        results = self.db.execute_query(query, (collection_id,))
        
        if not results:
            return {}
        
        prices = [row['price'] for row in results]
        
        return {
            'min': min(prices),
            'max': max(prices),
            'mean': round(statistics.mean(prices), 4),
            'median': round(statistics.median(prices), 4),
            'stdev': round(statistics.stdev(prices), 4) if len(prices) > 1 else 0,
            'total_samples': len(prices)
        }

ํŒ๋งค ์„œ๋น„์Šค๋Š” ๊ฑฐ๋ž˜ ๊ธฐ๋ก๋ถ€ํ„ฐ ํ†ต๊ณ„ ๋ถ„์„๊นŒ์ง€ ๋ชจ๋“  ๊ฑธ ์ฒ˜๋ฆฌํ•ด. ํŠนํžˆ ๊ฐ€๊ฒฉ ์ถ”์„ธ๋‚˜ ๋ถ„ํฌ ๋ถ„์„ ๊ฐ™์€ ๊ธฐ๋Šฅ์€ NFT ํˆฌ์ž์ž๋“ค์—๊ฒŒ ์ •๋ง ์œ ์šฉํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜์ง€! ๐Ÿ“Š

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ ์˜ˆ์ œ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ์•Œ์•„๋ณด์ž! ๐Ÿš€

๐Ÿ“ฑ main.py - ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

import logging
from database.db_manager import DatabaseManager
from services.nft_service import NFTService
from services.sales_service import SalesService

# ๋กœ๊น… ์„ค์ •
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

def initialize_system():
    """์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™”"""
    print("๐Ÿš€ NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ์‹œ์ž‘!\n")
    
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งค๋‹ˆ์ € ์ƒ์„ฑ
    db = DatabaseManager('nft_sales.db')
    
    # ์—ฐ๊ฒฐ ๋ฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ
    if db.connect():
        db.create_tables()
        return db
    return None

def demo_collection_creation(nft_service):
    """์ปฌ๋ ‰์…˜ ์ƒ์„ฑ ๋ฐ๋ชจ"""
    print("\n" + "="*50)
    print("๐Ÿ“ฆ ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ ๋ฐ๋ชจ")
    print("="*50 + "\n")
    
    # CryptoPunks ์Šคํƒ€์ผ ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ
    collection_id = nft_service.create_collection(
        name="CyberPunks",
        contract_address="0x1234567890abcdef1234567890abcdef12345678",
        description="10,000๊ฐœ์˜ ๋…ํŠนํ•œ ์‚ฌ์ด๋ฒ„ํŽ‘ํฌ ์บ๋ฆญํ„ฐ NFT",
        creator_address="0xCreator123456789",
        total_supply=10000
    )
    
    return collection_id

def demo_nft_creation(nft_service, collection_id):
    """NFT ์ƒ์„ฑ ๋ฐ๋ชจ"""
    print("\n" + "="*50)
    print("๐ŸŽจ NFT ์•„์ดํ…œ ์ƒ์„ฑ ๋ฐ๋ชจ")
    print("="*50 + "\n")
    
    # ์—ฌ๋Ÿฌ NFT ์ƒ์„ฑ
    nfts = [
        {
            "token_id": "1",
            "name": "CyberPunk #1",
            "description": "ํฌ๊ท€ํ•œ ๋ ˆ์ด์ € ๋ˆˆ์„ ๊ฐ€์ง„ ์‚ฌ์ด๋ฒ„ํŽ‘ํฌ",
            "metadata_uri": "ipfs://QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/1",
            "image_uri": "ipfs://QmYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/1.png",
            "current_owner": "0xOwner111111111",
            "rarity_score": 95.5,
            "attributes": {
                "background": "Purple",
                "eyes": "Laser",
                "hair": "Mohawk",
                "accessories": "Cybernetic Implant"
            }
        },
        {
            "token_id": "2",
            "name": "CyberPunk #2",
            "description": "ํด๋ž˜์‹ ์„ ๊ธ€๋ผ์Šค๋ฅผ ์ฐฉ์šฉํ•œ ์‚ฌ์ด๋ฒ„ํŽ‘ํฌ",
            "metadata_uri": "ipfs://QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/2",
            "image_uri": "ipfs://QmYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/2.png",
            "current_owner": "0xOwner222222222",
            "rarity_score": 78.3,
            "attributes": {
                "background": "Blue",
                "eyes": "Sunglasses",
                "hair": "Bald",
                "accessories": "Earring"
            }
        },
        {
            "token_id": "3",
            "name": "CyberPunk #3",
            "description": "๋„ค์˜จ ๋งˆ์Šคํฌ๋ฅผ ์ฐฉ์šฉํ•œ ์‹ ๋น„๋กœ์šด ์‚ฌ์ด๋ฒ„ํŽ‘ํฌ",
            "metadata_uri": "ipfs://QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/3",
            "image_uri": "ipfs://QmYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/3.png",
            "current_owner": "0xOwner333333333",
            "rarity_score": 88.7,
            "attributes": {
                "background": "Black",
                "eyes": "Neon Mask",
                "hair": "Long",
                "accessories": "Chain"
            }
        }
    ]
    
    nft_ids = []
    for nft_data in nfts:
        nft_id = nft_service.add_nft_item(
            collection_id=collection_id,
            **nft_data
        )
        nft_ids.append(nft_id)
    
    return nft_ids

def demo_sales_recording(sales_service, nft_ids):
    """ํŒ๋งค ๊ธฐ๋ก ๋ฐ๋ชจ"""
    print("\n" + "="*50)
    print("๐Ÿ’ฐ ํŒ๋งค ๊ฑฐ๋ž˜ ๊ธฐ๋ก ๋ฐ๋ชจ")
    print("="*50 + "\n")
    
    # ์ฒซ ๋ฒˆ์งธ NFT ํŒ๋งค
    sale1 = sales_service.record_sale(
        nft_item_id=nft_ids[0],
        seller_address="0xOwner111111111",
        buyer_address="0xBuyer111111111",
        price=5.5,
        currency="ETH",
        platform_fee=0.275,  # 5% ํ”Œ๋žซํผ ์ˆ˜์ˆ˜๋ฃŒ
        royalty_fee=0.55,    # 10% ๋กœ์—ดํ‹ฐ
        transaction_hash="0xTxHash1111111111111111111111111111111111111111111111111111111111",
        block_number=15000000,
        sale_type="direct"
    )
    
    # ๋‘ ๋ฒˆ์งธ NFT ํŒ๋งค
    sale2 = sales_service.record_sale(
        nft_item_id=nft_ids[1],
        seller_address="0xOwner222222222",
        buyer_address="0xBuyer222222222",
        price=3.2,
        currency="ETH",
        platform_fee=0.16,
        royalty_fee=0.32,
        transaction_hash="0xTxHash2222222222222222222222222222222222222222222222222222222222",
        block_number=15000100,
        sale_type="auction"
    )
    
    # ์„ธ ๋ฒˆ์งธ NFT ํŒ๋งค
    sale3 = sales_service.record_sale(
        nft_item_id=nft_ids[2],
        seller_address="0xOwner333333333",
        buyer_address="0xBuyer333333333",
        price=7.8,
        currency="ETH",
        platform_fee=0.39,
        royalty_fee=0.78,
        transaction_hash="0xTxHash3333333333333333333333333333333333333333333333333333333333",
        block_number=15000200,
        sale_type="direct"
    )
    
    return [sale1, sale2, sale3]

def demo_statistics(nft_service, sales_service, collection_id, nft_ids):
    """ํ†ต๊ณ„ ์กฐํšŒ ๋ฐ๋ชจ"""
    print("\n" + "="*50)
    print("๐Ÿ“Š ํ†ต๊ณ„ ๋ถ„์„ ๋ฐ๋ชจ")
    print("="*50 + "\n")
    
    # ์ปฌ๋ ‰์…˜ ํ†ต๊ณ„
    print("๐Ÿ“ฆ ์ปฌ๋ ‰์…˜ ํ†ต๊ณ„:")
    collection_stats = nft_service.get_collection_stats(collection_id)
    print(f"   ์ด NFT ์ˆ˜: {collection_stats['total_nfts']}")
    print(f"   ๊ณ ์œ  ์†Œ์œ ์ž: {collection_stats['unique_owners']}")
    print(f"   ํ‰๊ท  ํฌ๊ท€๋„: {collection_stats['avg_rarity_score']}")
    
    # ํŒ๋งค ํ†ต๊ณ„
    print("\n๐Ÿ’ฐ ํŒ๋งค ํ†ต๊ณ„ (์ตœ๊ทผ 30์ผ):")
    sales_stats = sales_service.get_collection_sales_stats(collection_id, days=30)
    print(f"   ์ด ๊ฑฐ๋ž˜ ์ˆ˜: {sales_stats['total_sales']}")
    print(f"   ์ด ๊ฑฐ๋ž˜๋Ÿ‰: {sales_stats['total_volume']:.2f} ETH")
    print(f"   ํ‰๊ท  ๊ฐ€๊ฒฉ: {sales_stats['avg_price']:.2f} ETH")
    print(f"   ๋ฐ”๋‹ฅ ๊ฐ€๊ฒฉ: {sales_stats['floor_price']:.2f} ETH")
    print(f"   ์ตœ๊ณ  ํŒ๋งค๊ฐ€: {sales_stats['highest_sale']:.2f} ETH")
    print(f"   ๊ฐ€๊ฒฉ ์ถ”์„ธ: {sales_stats['price_trend']:+.2f}%")
    print(f"   ๊ณ ์œ  ๊ฑฐ๋ž˜์ž: {sales_stats['unique_traders']}")
    
    # NFT ํŒ๋งค ์ด๋ ฅ
    print(f"\n๐Ÿ“œ NFT #{nft_ids[0]} ํŒ๋งค ์ด๋ ฅ:")
    history = sales_service.get_nft_sales_history(nft_ids[0])
    for sale in history:
        print(f"   {sale['sold_at']}: {sale['price']} {sale['currency']}")
        print(f"   ํŒ๋งค์ž: {sale['seller_address'][:10]}...")
        print(f"   ๊ตฌ๋งค์ž: {sale['buyer_address'][:10]}...")
        print()
    
    # ์ตœ๊ณ  ํŒ๋งค ๊ฑฐ๋ž˜
    print("๐Ÿ† ์ตœ๊ณ  ํŒ๋งค ๊ฑฐ๋ž˜ (์ตœ๊ทผ 7์ผ):")
    top_sales = sales_service.get_top_sales(limit=5, days=7)
    for i, sale in enumerate(top_sales, 1):
        print(f"   {i}. {sale['nft_name']}: {sale['price']} {sale['currency']}")
    
    # ๊ฐ€๊ฒฉ ๋ถ„ํฌ
    print("\n๐Ÿ“ˆ ๊ฐ€๊ฒฉ ๋ถ„ํฌ ๋ถ„์„:")
    price_dist = sales_service.get_price_distribution(collection_id)
    if price_dist:
        print(f"   ์ตœ์†Œ: {price_dist['min']:.2f} ETH")
        print(f"   ์ตœ๋Œ€: {price_dist['max']:.2f} ETH")
        print(f"   ํ‰๊ท : {price_dist['mean']:.2f} ETH")
        print(f"   ์ค‘์•™๊ฐ’: {price_dist['median']:.2f} ETH")
        print(f"   ํ‘œ์ค€ํŽธ์ฐจ: {price_dist['stdev']:.2f} ETH")
        print(f"   ์ƒ˜ํ”Œ ์ˆ˜: {price_dist['total_samples']}")

def demo_search(nft_service, collection_id):
    """๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๋ฐ๋ชจ"""
    print("\n" + "="*50)
    print("๐Ÿ” NFT ๊ฒ€์ƒ‰ ๋ฐ๋ชจ")
    print("="*50 + "\n")
    
    # ํฌ๊ท€๋„ ๊ธฐ์ค€ ๊ฒ€์ƒ‰
    print("๋†’์€ ํฌ๊ท€๋„ NFT ๊ฒ€์ƒ‰ (85์  ์ด์ƒ):")
    rare_nfts = nft_service.search_nfts(
        collection_id=collection_id,
        min_rarity=85.0
    )
    
    for nft in rare_nfts:
        print(f"   {nft['name']}: ํฌ๊ท€๋„ {nft['rarity_score']}")
        print(f"   ์†Œ์œ ์ž: {nft['current_owner'][:10]}...")
        print()

def main():
    """๋ฉ”์ธ ํ•จ์ˆ˜"""
    # ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™”
    db = initialize_system()
    if not db:
        print("โŒ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์‹คํŒจ")
        return
    
    # ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    nft_service = NFTService(db)
    sales_service = SalesService(db)
    
    try:
        # ๋ฐ๋ชจ ์‹คํ–‰
        collection_id = demo_collection_creation(nft_service)
        nft_ids = demo_nft_creation(nft_service, collection_id)
        sale_ids = demo_sales_recording(sales_service, nft_ids)
        demo_statistics(nft_service, sales_service, collection_id, nft_ids)
        demo_search(nft_service, collection_id)
        
        print("\n" + "="*50)
        print("โœ… ๋ชจ๋“  ๋ฐ๋ชจ ์™„๋ฃŒ!")
        print("="*50)
        
    except Exception as e:
        print(f"\nโŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
        logging.error(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
    
    finally:
        # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ข…๋ฃŒ
        db.disconnect()
        print("\n๐Ÿ‘‹ ์‹œ์Šคํ…œ ์ข…๋ฃŒ")

if __name__ == "__main__":
    main()

์ด ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ์•ผ. ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ๋ถ€ํ„ฐ NFT ์ถ”๊ฐ€, ํŒ๋งค ๊ธฐ๋ก, ํ†ต๊ณ„ ๋ถ„์„๊นŒ์ง€ ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€! ๐ŸŽฏ

โœ… ์‹คํ–‰ ๋ฐฉ๋ฒ•: ํ„ฐ๋ฏธ๋„์—์„œ `python main.py`๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋ชจ๋“  ๋ฐ๋ชจ๊ฐ€ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰๋ผ. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์›น API๋‚˜ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

๐Ÿ“Š ๋ฐ์ดํ„ฐ ๋ถ„์„ ๋ฐ ์‹œ๊ฐํ™”

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

๐Ÿ“‰ utils/analytics.py - ๋ถ„์„ ์œ ํ‹ธ๋ฆฌํ‹ฐ

import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import List, Dict
import json

class NFTAnalytics:
    def __init__(self, db_manager):
        """
        NFT ๋ถ„์„ ๋„๊ตฌ ์ดˆ๊ธฐํ™”
        
        Args:
            db_manager: DatabaseManager ์ธ์Šคํ„ด์Šค
        """
        self.db = db_manager
    
    def get_sales_dataframe(self, collection_id: int = None, 
                           days: int = 30) -> pd.DataFrame:
        """
        ํŒ๋งค ๋ฐ์ดํ„ฐ๋ฅผ Pandas DataFrame์œผ๋กœ ๋ณ€ํ™˜
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID (์„ ํƒ)
            days: ์กฐํšŒ ๊ธฐ๊ฐ„
            
        Returns:
            DataFrame
        """
        start_date = datetime.now() - timedelta(days=days)
        
        query = '''
            SELECT 
                st.id,
                st.price,
                st.currency,
                st.platform_fee,
                st.royalty_fee,
                st.sale_type,
                st.sold_at,
                n.name as nft_name,
                n.token_id,
                n.rarity_score,
                c.name as collection_name
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            JOIN collections c ON n.collection_id = c.id
            WHERE st.sold_at >= ?
        '''
        
        params = [start_date]
        
        if collection_id:
            query += ' AND n.collection_id = ?'
            params.append(collection_id)
        
        query += ' ORDER BY st.sold_at ASC'
        
        results = self.db.execute_query(query, tuple(params))
        
        if not results:
            return pd.DataFrame()
        
        # DataFrame ์ƒ์„ฑ
        df = pd.DataFrame([dict(row) for row in results])
        df['sold_at'] = pd.to_datetime(df['sold_at'])
        
        return df
    
    def calculate_daily_volume(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        ์ผ๋ณ„ ๊ฑฐ๋ž˜๋Ÿ‰ ๊ณ„์‚ฐ
        
        Args:
            df: ํŒ๋งค ๋ฐ์ดํ„ฐ DataFrame
            
        Returns:
            ์ผ๋ณ„ ํ†ต๊ณ„ DataFrame
        """
        if df.empty:
            return pd.DataFrame()
        
        df['date'] = df['sold_at'].dt.date
        
        daily_stats = df.groupby('date').agg({
            'price': ['sum', 'mean', 'count'],
            'platform_fee': 'sum',
            'royalty_fee': 'sum'
        }).reset_index()
        
        daily_stats.columns = ['date', 'total_volume', 'avg_price', 
                              'sales_count', 'total_platform_fee', 'total_royalty']
        
        return daily_stats
    
    def analyze_rarity_price_correlation(self, collection_id: int) -> Dict:
        """
        ํฌ๊ท€๋„์™€ ๊ฐ€๊ฒฉ์˜ ์ƒ๊ด€๊ด€๊ณ„ ๋ถ„์„
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            
        Returns:
            ๋ถ„์„ ๊ฒฐ๊ณผ ๋”•์…”๋„ˆ๋ฆฌ
        """
        query = '''
            SELECT 
                n.rarity_score,
                AVG(st.price) as avg_price,
                COUNT(*) as sales_count
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            WHERE n.collection_id = ?
            AND n.rarity_score IS NOT NULL
            GROUP BY n.rarity_score
            ORDER BY n.rarity_score
        '''
        
        results = self.db.execute_query(query, (collection_id,))
        
        if not results:
            return {}
        
        df = pd.DataFrame([dict(row) for row in results])
        
        # ์ƒ๊ด€๊ณ„์ˆ˜ ๊ณ„์‚ฐ
        correlation = df['rarity_score'].corr(df['avg_price'])
        
        return {
            'correlation': round(correlation, 4),
            'data': df.to_dict('records'),
            'interpretation': self._interpret_correlation(correlation)
        }
    
    def _interpret_correlation(self, corr: float) -> str:
        """์ƒ๊ด€๊ณ„์ˆ˜ ํ•ด์„"""
        if corr > 0.7:
            return "๊ฐ•ํ•œ ์–‘์˜ ์ƒ๊ด€๊ด€๊ณ„ - ํฌ๊ท€๋„๊ฐ€ ๋†’์„์ˆ˜๋ก ๊ฐ€๊ฒฉ์ด ๋†’์Œ"
        elif corr > 0.3:
            return "์ค‘๊ฐ„ ์–‘์˜ ์ƒ๊ด€๊ด€๊ณ„ - ํฌ๊ท€๋„๊ฐ€ ๊ฐ€๊ฒฉ์— ์˜ํ–ฅ์„ ์คŒ"
        elif corr > -0.3:
            return "์•ฝํ•œ ์ƒ๊ด€๊ด€๊ณ„ - ํฌ๊ท€๋„์™€ ๊ฐ€๊ฒฉ์˜ ๊ด€๊ณ„๊ฐ€ ๋ถˆ๋ช…ํ™•"
        elif corr > -0.7:
            return "์ค‘๊ฐ„ ์Œ์˜ ์ƒ๊ด€๊ด€๊ณ„ - ํฌ๊ท€๋„๊ฐ€ ๋‚ฎ์„์ˆ˜๋ก ๊ฐ€๊ฒฉ์ด ๋†’์Œ"
        else:
            return "๊ฐ•ํ•œ ์Œ์˜ ์ƒ๊ด€๊ด€๊ณ„ - ํฌ๊ท€๋„์™€ ๊ฐ€๊ฒฉ์ด ๋ฐ˜๋Œ€๋กœ ์›€์ง์ž„"
    
    def get_holder_distribution(self, collection_id: int) -> Dict:
        """
        ์†Œ์œ ์ž ๋ถ„ํฌ ๋ถ„์„
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            
        Returns:
            ๋ถ„ํฌ ์ •๋ณด
        """
        query = '''
            SELECT 
                current_owner,
                COUNT(*) as nft_count
            FROM nft_items
            WHERE collection_id = ?
            AND current_owner IS NOT NULL
            GROUP BY current_owner
            ORDER BY nft_count DESC
        '''
        
        results = self.db.execute_query(query, (collection_id,))
        
        if not results:
            return {}
        
        df = pd.DataFrame([dict(row) for row in results])
        
        total_nfts = df['nft_count'].sum()
        total_holders = len(df)
        
        # ์ƒ์œ„ ์†Œ์œ ์ž ๋ถ„์„
        top_10_count = df.head(10)['nft_count'].sum()
        top_10_percentage = (top_10_count / total_nfts) * 100
        
        # ๊ณ ๋ž˜(whale) ์ •์˜: ์ „์ฒด์˜ 5% ์ด์ƒ ๋ณด์œ 
        whale_threshold = total_nfts * 0.05
        whales = df[df['nft_count'] >= whale_threshold]
        
        return {
            'total_holders': total_holders,
            'total_nfts': total_nfts,
            'avg_nfts_per_holder': round(total_nfts / total_holders, 2),
            'top_10_holders_percentage': round(top_10_percentage, 2),
            'whale_count': len(whales),
            'whale_holdings': int(whales['nft_count'].sum()),
            'distribution_data': df.head(20).to_dict('records')
        }
    
    def analyze_sale_types(self, collection_id: int) -> Dict:
        """
        ํŒ๋งค ์œ ํ˜•๋ณ„ ๋ถ„์„
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            
        Returns:
            ํŒ๋งค ์œ ํ˜• ํ†ต๊ณ„
        """
        query = '''
            SELECT 
                st.sale_type,
                COUNT(*) as count,
                AVG(st.price) as avg_price,
                SUM(st.price) as total_volume
            FROM sales_transactions st
            JOIN nft_items n ON st.nft_item_id = n.id
            WHERE n.collection_id = ?
            GROUP BY st.sale_type
        '''
        
        results = self.db.execute_query(query, (collection_id,))
        
        if not results:
            return {}
        
        return {
            'by_type': [dict(row) for row in results]
        }
    
    def get_price_momentum(self, collection_id: int, 
                          window_days: int = 7) -> Dict:
        """
        ๊ฐ€๊ฒฉ ๋ชจ๋ฉ˜ํ…€ ๋ถ„์„
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            window_days: ์ด๋™ํ‰๊ท  ์œˆ๋„์šฐ (์ผ)
            
        Returns:
            ๋ชจ๋ฉ˜ํ…€ ์ •๋ณด
        """
        df = self.get_sales_dataframe(collection_id, days=30)
        
        if df.empty:
            return {}
        
        daily_stats = self.calculate_daily_volume(df)
        
        # ์ด๋™ํ‰๊ท  ๊ณ„์‚ฐ
        daily_stats['ma_price'] = daily_stats['avg_price'].rolling(
            window=window_days, min_periods=1
        ).mean()
        
        # ํ˜„์žฌ ๊ฐ€๊ฒฉ๊ณผ ์ด๋™ํ‰๊ท  ๋น„๊ต
        latest_price = daily_stats.iloc[-1]['avg_price']
        latest_ma = daily_stats.iloc[-1]['ma_price']
        
        momentum = ((latest_price - latest_ma) / latest_ma) * 100
        
        return {
            'current_avg_price': round(latest_price, 4),
            'moving_average': round(latest_ma, 4),
            'momentum_percentage': round(momentum, 2),
            'signal': 'bullish' if momentum > 5 else 'bearish' if momentum < -5 else 'neutral',
            'daily_data': daily_stats.to_dict('records')
        }
    
    def export_report(self, collection_id: int, filename: str = None):
        """
        ๋ถ„์„ ๋ฆฌํฌํŠธ ์ƒ์„ฑ ๋ฐ ์ €์žฅ
        
        Args:
            collection_id: ์ปฌ๋ ‰์…˜ ID
            filename: ์ €์žฅํ•  ํŒŒ์ผ๋ช…
        """
        if not filename:
            filename = f"nft_report_{collection_id}_{datetime.now().strftime('%Y%m%d')}.json"
        
        report = {
            'generated_at': datetime.now().isoformat(),
            'collection_id': collection_id,
            'rarity_analysis': self.analyze_rarity_price_correlation(collection_id),
            'holder_distribution': self.get_holder_distribution(collection_id),
            'sale_types': self.analyze_sale_types(collection_id),
            'price_momentum': self.get_price_momentum(collection_id)
        }
        
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(report, f, indent=2, ensure_ascii=False)
        
        print(f"โœ… ๋ฆฌํฌํŠธ ์ €์žฅ ์™„๋ฃŒ: {filename}")

์ด ๋ถ„์„ ๋„๊ตฌ๋Š” NFT ์ปฌ๋ ‰์…˜์˜ ๋‹ค์–‘ํ•œ ์ธก๋ฉด์„ ๋ถ„์„ํ•ด์ค˜. ๊ฐ€๊ฒฉ ์ถ”์„ธ, ์†Œ์œ ์ž ๋ถ„ํฌ, ํฌ๊ท€๋„์™€ ๊ฐ€๊ฒฉ์˜ ์ƒ๊ด€๊ด€๊ณ„ ๋“ฑ์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ“Š

๐Ÿ’ก ํ™œ์šฉ ํŒ: ์ด๋Ÿฐ ๋ถ„์„ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ NFT ์ปจ์„คํŒ… ์„œ๋น„์Šค๋กœ ์ œ๊ณตํ•  ์ˆ˜๋„ ์žˆ์–ด. ํ”„๋กœ์ ํŠธ ์šด์˜์ž๋“ค์—๊ฒŒ ์ •๋ง ์œ ์šฉํ•œ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์ค„ ์ˆ˜ ์žˆ๊ฑฐ๋“ !

๐Ÿ” ๋ณด์•ˆ ๋ฐ ์ตœ์ ํ™” ๊ณ ๋ ค์‚ฌํ•ญ

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด. ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿ›ก๏ธ

๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐฉ๋ฒ•

1. SQL Injection ๋ฐฉ์ง€ ๐Ÿšซ
์šฐ๋ฆฌ๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ์—์„œ๋Š” ์ด๋ฏธ ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด. ์ด๊ฒŒ SQL Injection์„ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ด๊ณ  ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์ด์•ผ:

# โŒ ๋‚˜์œ ์˜ˆ - SQL Injection ์ทจ์•ฝ
query = f"SELECT * FROM users WHERE wallet_address = '{address}'"

# โœ… ์ข‹์€ ์˜ˆ - ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ์ฟผ๋ฆฌ
query = "SELECT * FROM users WHERE wallet_address = ?"
self.db.execute_query(query, (address,))

2. ๋ฏผ๊ฐํ•œ ์ •๋ณด ์•”ํ˜ธํ™” ๐Ÿ”
์ง€๊ฐ‘ ์ฃผ์†Œ๋‚˜ ๊ฐœ์ธ ์ •๋ณด๋Š” ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•˜๋Š” ๊ฒŒ ์ข‹์•„:

from cryptography.fernet import Fernet
import os

class SecurityManager:
    def __init__(self):
        # ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ์•”ํ˜ธํ™” ํ‚ค ๋กœ๋“œ
        key = os.getenv('ENCRYPTION_KEY')
        if not key:
            key = Fernet.generate_key()
            print(f"์ƒˆ ์•”ํ˜ธํ™” ํ‚ค ์ƒ์„ฑ: {key.decode()}")
        else:
            key = key.encode()
        
        self.cipher = Fernet(key)
    
    def encrypt_data(self, data: str) -> str:
        """๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™”"""
        return self.cipher.encrypt(data.encode()).decode()
    
    def decrypt_data(self, encrypted_data: str) -> str:
        """๋ฐ์ดํ„ฐ ๋ณตํ˜ธํ™”"""
        return self.cipher.decrypt(encrypted_data.encode()).decode()

3. ์ ‘๊ทผ ์ œ์–ด ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ ๐Ÿ‘ฅ
์‚ฌ์šฉ์ž๋ณ„๋กœ ๋‹ค๋ฅธ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•ด์„œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์„ ์ œํ•œํ•ด์•ผ ํ•ด:

class AccessControl:
    ROLES = {
        'admin': ['read', 'write', 'delete', 'manage_users'],
        'user': ['read', 'write'],
        'viewer': ['read']
    }
    
    def check_permission(self, user_role: str, action: str) -> bool:
        """๊ถŒํ•œ ํ™•์ธ"""
        return action in self.ROLES.get(user_role, [])

4. ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ๊ฒ€์ฆ โœ…
๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๊ฐ€ ์‹ค์ œ๋กœ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด:

from web3 import Web3

class BlockchainVerifier:
    def __init__(self, provider_url):
        self.w3 = Web3(Web3.HTTPProvider(provider_url))
    
    def verify_transaction(self, tx_hash: str) -> bool:
        """ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ"""
        try:
            tx = self.w3.eth.get_transaction(tx_hash)
            receipt = self.w3.eth.get_transaction_receipt(tx_hash)
            
            # ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธ
            return receipt['status'] == 1
        except Exception as e:
            print(f"ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ ์‹คํŒจ: {e}")
            return False

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

1. ์ธ๋ฑ์Šค ์ตœ์ ํ™” ๐Ÿ“‡
์ž์ฃผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ปฌ๋Ÿผ์—๋Š” ๋ฐ˜๋“œ์‹œ ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•ด. ์šฐ๋ฆฌ๊ฐ€ ์ด๋ฏธ ๋ช‡ ๊ฐœ ๋งŒ๋“ค์—ˆ์ง€๋งŒ, ๋” ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด:

# ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX idx_sales_collection_date 
ON sales_transactions(nft_item_id, sold_at);

# ๋ถ€๋ถ„ ์ธ๋ฑ์Šค (์กฐ๊ฑด๋ถ€ ์ธ๋ฑ์Šค)
CREATE INDEX idx_active_sales 
ON sales_transactions(nft_item_id) 
WHERE status = 'completed';

2. ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๐ŸŽฏ
๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋„๋ก ์ฟผ๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•ด์•ผ ํ•ด:

# โŒ ๋น„ํšจ์œจ์  - ๋ชจ๋“  ์ปฌ๋Ÿผ ๊ฐ€์ ธ์˜ค๊ธฐ
SELECT * FROM nft_items WHERE collection_id = ?

# โœ… ํšจ์œจ์  - ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์„ ํƒ
SELECT id, name, token_id, current_owner 
FROM nft_items 
WHERE collection_id = ?

3. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๐Ÿ“ฆ
์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ ˆ์ฝ”๋“œ๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ:

def batch_insert_nfts(self, nfts_data: List[Dict]) -> List[int]:
    """NFT ๋ฐฐ์น˜ ์‚ฝ์ž…"""
    query = '''
        INSERT INTO nft_items 
        (collection_id, token_id, name, description, current_owner)
        VALUES (?, ?, ?, ?, ?)
    '''
    
    # executemany ์‚ฌ์šฉ
    params_list = [
        (nft['collection_id'], nft['token_id'], nft['name'],
         nft['description'], nft['current_owner'])
        for nft in nfts_data
    ]
    
    try:
        self.cursor.executemany(query, params_list)
        self.connection.commit()
        print(f"โœ… {len(nfts_data)}๊ฐœ NFT ๋ฐฐ์น˜ ์‚ฝ์ž… ์™„๋ฃŒ!")
        return True
    except sqlite3.Error as e:
        print(f"โŒ ๋ฐฐ์น˜ ์‚ฝ์ž… ์‹คํŒจ: {e}")
        self.connection.rollback()
        return False

4. ์บ์‹ฑ ์ „๋žต ๐Ÿ’พ
์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹œ์— ์ €์žฅํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€ํ•˜๋ฅผ ์ค„์—ฌ:

from functools import lru_cache
from datetime import datetime, timedelta

class CachedNFTService:
    def __init__(self, nft_service):
        self.service = nft_service
        self.cache_expiry = {}
    
    @lru_cache(maxsize=128)
    def get_collection_cached(self, contract_address: str):
        """์ปฌ๋ ‰์…˜ ์ •๋ณด ์บ์‹ฑ (5๋ถ„)"""
        cache_key = f"collection_{contract_address}"
        
        # ์บ์‹œ ๋งŒ๋ฃŒ ํ™•์ธ
        if cache_key in self.cache_expiry:
            if datetime.now() < self.cache_expiry[cache_key]:
                return self.service.get_collection_by_address(contract_address)
        
        # ์บ์‹œ ๊ฐฑ์‹ 
        result = self.service.get_collection_by_address(contract_address)
        self.cache_expiry[cache_key] = datetime.now() + timedelta(minutes=5)
        
        return result

5. ์—ฐ๊ฒฐ ํ’€๋ง ๐ŸŠ
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์–ด:

import sqlite3
from contextlib import contextmanager

class ConnectionPool:
    def __init__(self, db_path, pool_size=5):
        self.db_path = db_path
        self.pool = [sqlite3.connect(db_path) for _ in range(pool_size)]
        self.available = self.pool.copy()
    
    @contextmanager
    def get_connection(self):
        """์—ฐ๊ฒฐ ํ’€์—์„œ ์—ฐ๊ฒฐ ๊ฐ€์ ธ์˜ค๊ธฐ"""
        if not self.available:
            # ํ’€์ด ๋น„์–ด์žˆ์œผ๋ฉด ์ƒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ
            conn = sqlite3.connect(self.db_path)
        else:
            conn = self.available.pop()
        
        try:
            yield conn
        finally:
            # ์—ฐ๊ฒฐ ๋ฐ˜ํ™˜
            if len(self.available) < len(self.pool):
                self.available.append(conn)
            else:
                conn.close()
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: SQLite๋Š” ๋™์‹œ ์“ฐ๊ธฐ ์ž‘์—…์— ์ œํ•œ์ด ์žˆ์–ด. ์ดˆ๋‹น ์ˆ˜๋ฐฑ ๊ฑด ์ด์ƒ์˜ ์“ฐ๊ธฐ ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค๋ฉด PostgreSQL์ด๋‚˜ MySQL๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด!

๐Ÿš€ ์‹ค์ „ ๋ฐฐํฌ ๊ฐ€์ด๋“œ

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

๐Ÿ“ฆ ๋ฐฐํฌ ์ค€๋น„ํ•˜๊ธฐ

1. ํ™˜๊ฒฝ ์„ค์ • ํŒŒ์ผ (.env)

# .env ํŒŒ์ผ
DATABASE_PATH=./data/nft_sales.db
ENCRYPTION_KEY=your-encryption-key-here
WEB3_PROVIDER_URL=https://mainnet.infura.io/v3/YOUR-PROJECT-ID
LOG_LEVEL=INFO
MAX_CONNECTIONS=10
CACHE_TTL=300

2. ์„ค์ • ๊ด€๋ฆฌ (config/settings.py)

import os
from dotenv import load_dotenv

load_dotenv()

class Settings:
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
    DATABASE_PATH = os.getenv('DATABASE_PATH', 'nft_sales.db')
    
    # ๋ณด์•ˆ ์„ค์ •
    ENCRYPTION_KEY = os.getenv('ENCRYPTION_KEY')
    
    # ๋ธ”๋ก์ฒด์ธ ์„ค์ •
    WEB3_PROVIDER_URL = os.getenv('WEB3_PROVIDER_URL')
    
    # ๋กœ๊น… ์„ค์ •
    LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
    
    # ์„ฑ๋Šฅ ์„ค์ •
    MAX_CONNECTIONS = int(os.getenv('MAX_CONNECTIONS', 10))
    CACHE_TTL = int(os.getenv('CACHE_TTL', 300))
    
    @classmethod
    def validate(cls):
        """์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ"""
        required = ['ENCRYPTION_KEY', 'WEB3_PROVIDER_URL']
        missing = [key for key in required if not getattr(cls, key)]
        
        if missing:
            raise ValueError(f"ํ•„์ˆ˜ ์„ค์ • ๋ˆ„๋ฝ: {', '.join(missing)}")

settings = Settings()
settings.validate()

3. ๋กœ๊น… ์„ค์ •

import logging
from logging.handlers import RotatingFileHandler
import os

def setup_logging():
    """๋กœ๊น… ์„ค์ •"""
    log_dir = 'logs'
    if not os.path.exists(log_dir):
        os.makedirs(log_dir)
    
    # ๋กœ๊ฑฐ ์ƒ์„ฑ
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    # ํŒŒ์ผ ํ•ธ๋“ค๋Ÿฌ (10MB ํฌ๊ธฐ, 5๊ฐœ ๋ฐฑ์—…)
    file_handler = RotatingFileHandler(
        f'{log_dir}/nft_system.log',
        maxBytes=10*1024*1024,
        backupCount=5
    )
    file_handler.setLevel(logging.INFO)
    
    # ์ฝ˜์†” ํ•ธ๋“ค๋Ÿฌ
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG)
    
    # ํฌ๋งท ์„ค์ •
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    
    # ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    
    return logger

4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… ์Šคํฌ๋ฆฝํŠธ

import shutil
from datetime import datetime
import os

def backup_database(db_path: str, backup_dir: str = 'backups'):
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—…"""
    if not os.path.exists(backup_dir):
        os.makedirs(backup_dir)
    
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_path = f"{backup_dir}/nft_sales_backup_{timestamp}.db"
    
    try:
        shutil.copy2(db_path, backup_path)
        print(f"โœ… ๋ฐฑ์—… ์™„๋ฃŒ: {backup_path}")
        
        # ์˜ค๋ž˜๋œ ๋ฐฑ์—… ์‚ญ์ œ (7์ผ ์ด์ƒ)
        cleanup_old_backups(backup_dir, days=7)
        
        return backup_path
    except Exception as e:
        print(f"โŒ ๋ฐฑ์—… ์‹คํŒจ: {e}")
        return None

def cleanup_old_backups(backup_dir: str, days: int = 7):
    """์˜ค๋ž˜๋œ ๋ฐฑ์—… ํŒŒ์ผ ์‚ญ์ œ"""
    cutoff_time = datetime.now().timestamp() - (days * 24 * 60 * 60)
    
    for filename in os.listdir(backup_dir):
        filepath = os.path.join(backup_dir, filename)
        if os.path.isfile(filepath):
            if os.path.getmtime(filepath) < cutoff_time:
                os.remove(filepath)
                print(f"๐Ÿ—‘๏ธ ์˜ค๋ž˜๋œ ๋ฐฑ์—… ์‚ญ์ œ: {filename}")

๐Ÿณ Docker๋กœ ๋ฐฐํฌํ•˜๊ธฐ

Docker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋–ค ํ™˜๊ฒฝ์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

Dockerfile

FROM python:3.9-slim

WORKDIR /app

# ์‹œ์Šคํ…œ ํŒจํ‚ค์ง€ ์—…๋ฐ์ดํŠธ
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Python ํŒจํ‚ค์ง€ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
RUN mkdir -p /app/data /app/logs /app/backups

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
ENV PYTHONUNBUFFERED=1

# ํฌํŠธ ๋…ธ์ถœ (API ์„œ๋ฒ„์šฉ)
EXPOSE 8000

# ์‹คํ–‰ ๋ช…๋ น
CMD ["python", "main.py"]

docker-compose.yml

version: '3.8'

services:
  nft-system:
    build: .
    container_name: nft-sales-manager
    volumes:
      - ./data:/app/data
      - ./logs:/app/logs
      - ./backups:/app/backups
    environment:
      - DATABASE_PATH=/app/data/nft_sales.db
      - LOG_LEVEL=INFO
    restart: unless-stopped
    networks:
      - nft-network

networks:
  nft-network:
    driver: bridge

์‹คํ–‰ ๋ฐฉ๋ฒ•:

# Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ
docker-compose build

# ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
docker-compose up -d

# ๋กœ๊ทธ ํ™•์ธ
docker-compose logs -f

# ์ปจํ…Œ์ด๋„ˆ ์ค‘์ง€
docker-compose down
โœ… ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ: โ˜‘๏ธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ์™„๋ฃŒ
โ˜‘๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… ์Šคํฌ๋ฆฝํŠธ ์„ค์ •
โ˜‘๏ธ ๋กœ๊น… ์‹œ์Šคํ…œ ๊ตฌ์„ฑ
โ˜‘๏ธ ๋ณด์•ˆ ์„ค์ • ๊ฒ€ํ† 
โ˜‘๏ธ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ ์„ค์น˜
โ˜‘๏ธ ์—๋Ÿฌ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

๐Ÿ”„ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”

๋ธ”๋ก์ฒด์ธ์˜ ์‹ค์‹œ๊ฐ„ ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ๋ฅผ ์šฐ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๋™๊ธฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ์ด๊ฒŒ ์ง„์งœ ์‹ค์ „์—์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ. โšก

๐Ÿ”— ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ

from web3 import Web3
from web3.middleware import geth_poa_middleware
import json
import time
from typing import Callable

class NFTEventListener:
    def __init__(self, provider_url: str, contract_address: str, 
                 contract_abi: list):
        """
        NFT ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
        
        Args:
            provider_url: Web3 ํ”„๋กœ๋ฐ”์ด๋” URL
            contract_address: NFT ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ
            contract_abi: ์ปจํŠธ๋ž™ํŠธ ABI
        """
        self.w3 = Web3(Web3.HTTPProvider(provider_url))
        
        # PoA ์ฒด์ธ ์ง€์› (Polygon ๋“ฑ)
        self.w3.middleware_onion.inject(geth_poa_middleware, layer=0)
        
        self.contract = self.w3.eth.contract(
            address=Web3.toChecksumAddress(contract_address),
            abi=contract_abi
        )
        
        self.is_listening = False
    
    def listen_transfer_events(self, callback: Callable, 
                               from_block: int = 'latest'):
        """
        Transfer ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹
        
        Args:
            callback: ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ฝœ๋ฐฑ ํ•จ์ˆ˜
            from_block: ์‹œ์ž‘ ๋ธ”๋ก ๋ฒˆํ˜ธ
        """
        print(f"๐ŸŽง Transfer ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ์‹œ์ž‘... (๋ธ”๋ก: {from_block})")
        
        # ์ด๋ฒคํŠธ ํ•„ํ„ฐ ์ƒ์„ฑ
        event_filter = self.contract.events.Transfer.create_filter(
            fromBlock=from_block
        )
        
        self.is_listening = True
        
        while self.is_listening:
            try:
                # ์ƒˆ ์ด๋ฒคํŠธ ํ™•์ธ
                for event in event_filter.get_new_entries():
                    self._process_transfer_event(event, callback)
                
                # 1์ดˆ ๋Œ€๊ธฐ
                time.sleep(1)
                
            except Exception as e:
                print(f"โŒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ์˜ค๋ฅ˜: {e}")
                time.sleep(5)  # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ 5์ดˆ ๋Œ€๊ธฐ
    
    def _process_transfer_event(self, event, callback):
        """Transfer ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ"""
        try:
            # ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
            event_data = {
                'from_address': event['args']['from'],
                'to_address': event['args']['to'],
                'token_id': str(event['args']['tokenId']),
                'transaction_hash': event['transactionHash'].hex(),
                'block_number': event['blockNumber'],
                'timestamp': self._get_block_timestamp(event['blockNumber'])
            }
            
            print(f"๐Ÿ“ฆ Transfer ์ด๋ฒคํŠธ ๊ฐ์ง€:")
            print(f"   Token ID: {event_data['token_id']}")
            print(f"   From: {event_data['from_address'][:10]}...")
            print(f"   To: {event_data['to_address'][:10]}...")
            
            # ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์‹คํ–‰
            callback(event_data)
            
        except Exception as e:
            print(f"โŒ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ: {e}")
    
    def _get_block_timestamp(self, block_number: int) -> int:
        """๋ธ”๋ก ํƒ€์ž„์Šคํƒฌํ”„ ์กฐํšŒ"""
        try:
            block = self.w3.eth.get_block(block_number)
            return block['timestamp']
        except Exception as e:
            print(f"โŒ ๋ธ”๋ก ํƒ€์ž„์Šคํƒฌํ”„ ์กฐํšŒ ์‹คํŒจ: {e}")
            return int(time.time())
    
    def stop_listening(self):
        """๋ฆฌ์Šค๋‹ ์ค‘์ง€"""
        self.is_listening = False
        print("๐Ÿ›‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ์ค‘์ง€")

class SyncManager:
    def __init__(self, db_manager, nft_service, sales_service):
        """
        ๋™๊ธฐํ™” ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™”
        
        Args:
            db_manager: DatabaseManager ์ธ์Šคํ„ด์Šค
            nft_service: NFTService ์ธ์Šคํ„ด์Šค
            sales_service: SalesService ์ธ์Šคํ„ด์Šค
        """
        self.db = db_manager
        self.nft_service = nft_service
        self.sales_service = sales_service
    
    def handle_transfer_event(self, event_data: dict):
        """
        Transfer ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐ DB ์—…๋ฐ์ดํŠธ
        
        Args:
            event_data: ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ
        """
        try:
            # NFT ์กฐํšŒ
            nft = self.nft_service.get_nft_by_token(
                collection_id=1,  # ์‹ค์ œ๋กœ๋Š” ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๋กœ ์กฐํšŒ
                token_id=event_data['token_id']
            )
            
            if not nft:
                print(f"โš ๏ธ NFT๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: Token ID {event_data['token_id']}")
                return
            
            # ๋ฏผํŒ…(0x0์—์„œ ์ „์†ก)์ด ์•„๋‹Œ ๊ฒฝ์šฐ ํŒ๋งค๋กœ ๊ฐ„์ฃผ
            zero_address = '0x0000000000000000000000000000000000000000'
            
            if event_data['from_address'].lower() != zero_address.lower():
                # ํŠธ๋žœ์žญ์…˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
                tx_details = self._get_transaction_details(
                    event_data['transaction_hash']
                )
                
                if tx_details:
                    # ํŒ๋งค ๊ธฐ๋ก
                    self.sales_service.record_sale(
                        nft_item_id=nft['id'],
                        seller_address=event_data['from_address'],
                        buyer_address=event_data['to_address'],
                        price=tx_details['value'],
                        transaction_hash=event_data['transaction_hash'],
                        block_number=event_data['block_number']
                    )
                    
                    print(f"โœ… ํŒ๋งค ๊ธฐ๋ก ์™„๋ฃŒ!")
            else:
                # ๋ฏผํŒ…์ธ ๊ฒฝ์šฐ ์†Œ์œ ์ž๋งŒ ์—…๋ฐ์ดํŠธ
                self.nft_service.update_nft_owner(
                    nft_id=nft['id'],
                    new_owner=event_data['to_address']
                )
                print(f"โœ… ๋ฏผํŒ… ๊ธฐ๋ก ์™„๋ฃŒ!")
                
        except Exception as e:
            print(f"โŒ Transfer ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ: {e}")
    
    def _get_transaction_details(self, tx_hash: str) -> dict:
        """ํŠธ๋žœ์žญ์…˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ"""
        # ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” Web3๋ฅผ ์‚ฌ์šฉํ•ด ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ
        # ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋กœ ๊ฐ„๋‹จํžˆ ์ฒ˜๋ฆฌ
        return {
            'value': 1.5,  # ETH
            'gas_used': 150000
        }

์ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋Š” ๋ธ”๋ก์ฒด์ธ์˜ ์‹ค์‹œ๊ฐ„ ๊ฑฐ๋ž˜๋ฅผ ๊ฐ์ง€ํ•ด์„œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ธฐ๋กํ•ด์ค˜. ์™„์ „ ์ž๋™ํ™”! ๐Ÿค–

๐Ÿ’ก ์‹ค์ „ ํŒ: ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋ณ„๋„์˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ”„๋กœ์„ธ์Šค๋กœ ์‹คํ–‰ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. Celery๋‚˜ RQ ๊ฐ™์€ ์ž‘์—… ํ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์•ˆ์ •์ ์œผ๋กœ ์šด์˜ํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ“ฑ REST API ๊ตฌ์ถ•ํ•˜๊ธฐ

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

๐Ÿ”Œ Flask API ์„œ๋ฒ„

from flask import Flask, jsonify, request
from flask_cors import CORS
from database.db_manager import DatabaseManager
from services.nft_service import NFTService
from services.sales_service import SalesService
from utils.analytics import NFTAnalytics
import logging

app = Flask(__name__)
CORS(app)  # CORS ํ™œ์„ฑํ™”

# ๋กœ๊น… ์„ค์ •
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ์„œ๋น„์Šค ์ดˆ๊ธฐํ™”
db = DatabaseManager('nft_sales.db')
db.connect()
db.create_tables()

nft_service = NFTService(db)
sales_service = SalesService(db)
analytics = NFTAnalytics(db)

# ============= ์ปฌ๋ ‰์…˜ API =============

@app.route('/api/collections', methods=['POST'])
def create_collection():
    """์ปฌ๋ ‰์…˜ ์ƒ์„ฑ"""
    try:
        data = request.json
        collection_id = nft_service.create_collection(
            name=data['name'],
            contract_address=data['contract_address'],
            description=data.get('description'),
            creator_address=data.get('creator_address'),
            total_supply=data.get('total_supply')
        )
        
        return jsonify({
            'success': True,
            'collection_id': collection_id
        }), 201
        
    except Exception as e:
        logger.error(f"์ปฌ๋ ‰์…˜ ์ƒ์„ฑ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/collections/<contract_address>', methods=['GET'])
def get_collection(contract_address):
    """์ปฌ๋ ‰์…˜ ์กฐํšŒ"""
    try:
        collection = nft_service.get_collection_by_address(contract_address)
        
        if collection:
            return jsonify({
                'success': True,
                'data': collection
            })
        else:
            return jsonify({
                'success': False,
                'error': '์ปฌ๋ ‰์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
            }), 404
            
    except Exception as e:
        logger.error(f"์ปฌ๋ ‰์…˜ ์กฐํšŒ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/collections/<int:collection_id>/stats', methods=['GET'])
def get_collection_stats(collection_id):
    """์ปฌ๋ ‰์…˜ ํ†ต๊ณ„"""
    try:
        stats = nft_service.get_collection_stats(collection_id)
        
        return jsonify({
            'success': True,
            'data': stats
        })
        
    except Exception as e:
        logger.error(f"ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

# ============= NFT API =============

@app.route('/api/nfts', methods=['POST'])
def create_nft():
    """NFT ์ƒ์„ฑ"""
    try:
        data = request.json
        nft_id = nft_service.add_nft_item(
            collection_id=data['collection_id'],
            token_id=data['token_id'],
            name=data.get('name'),
            description=data.get('description'),
            metadata_uri=data.get('metadata_uri'),
            image_uri=data.get('image_uri'),
            current_owner=data.get('current_owner'),
            rarity_score=data.get('rarity_score'),
            attributes=data.get('attributes')
        )
        
        return jsonify({
            'success': True,
            'nft_id': nft_id
        }), 201
        
    except Exception as e:
        logger.error(f"NFT ์ƒ์„ฑ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/nfts/<int:collection_id>/<token_id>', methods=['GET'])
def get_nft(collection_id, token_id):
    """NFT ์กฐํšŒ"""
    try:
        nft = nft_service.get_nft_by_token(collection_id, token_id)
        
        if nft:
            return jsonify({
                'success': True,
                'data': nft
            })
        else:
            return jsonify({
                'success': False,
                'error': 'NFT๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
            }), 404
            
    except Exception as e:
        logger.error(f"NFT ์กฐํšŒ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/nfts/search', methods=['GET'])
def search_nfts():
    """NFT ๊ฒ€์ƒ‰"""
    try:
        collection_id = request.args.get('collection_id', type=int)
        owner = request.args.get('owner')
        min_rarity = request.args.get('min_rarity', type=float)
        max_rarity = request.args.get('max_rarity', type=float)
        limit = request.args.get('limit', default=100, type=int)
        
        nfts = nft_service.search_nfts(
            collection_id=collection_id,
            owner=owner,
            min_rarity=min_rarity,
            max_rarity=max_rarity,
            limit=limit
        )
        
        return jsonify({
            'success': True,
            'data': nfts,
            'count': len(nfts)
        })
        
    except Exception as e:
        logger.error(f"NFT ๊ฒ€์ƒ‰ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

# ============= ํŒ๋งค API =============

@app.route('/api/sales', methods=['POST'])
def record_sale():
    """ํŒ๋งค ๊ธฐ๋ก"""
    try:
        data = request.json
        sale_id = sales_service.record_sale(
            nft_item_id=data['nft_item_id'],
            seller_address=data['seller_address'],
            buyer_address=data['buyer_address'],
            price=data['price'],
            currency=data.get('currency', 'ETH'),
            platform_fee=data.get('platform_fee', 0),
            royalty_fee=data.get('royalty_fee', 0),
            transaction_hash=data.get('transaction_hash'),
            block_number=data.get('block_number'),
            sale_type=data.get('sale_type', 'direct')
        )
        
        return jsonify({
            'success': True,
            'sale_id': sale_id
        }), 201
        
    except Exception as e:
        logger.error(f"ํŒ๋งค ๊ธฐ๋ก ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/sales/nft/<int:nft_id>', methods=['GET'])
def get_nft_sales_history(nft_id):
    """NFT ํŒ๋งค ์ด๋ ฅ"""
    try:
        history = sales_service.get_nft_sales_history(nft_id)
        
        return jsonify({
            'success': True,
            'data': history,
            'count': len(history)
        })
        
    except Exception as e:
        logger.error(f"ํŒ๋งค ์ด๋ ฅ ์กฐํšŒ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/sales/user/<wallet_address>', methods=['GET'])
def get_user_sales(wallet_address):
    """์‚ฌ์šฉ์ž ๊ฑฐ๋ž˜ ์ด๋ ฅ"""
    try:
        as_seller = request.args.get('as_seller', default='true').lower() == 'true'
        
        sales = sales_service.get_user_sales(wallet_address, as_seller)
        
        return jsonify({
            'success': True,
            'data': sales,
            'count': len(sales)
        })
        
    except Exception as e:
        logger.error(f"์‚ฌ์šฉ์ž ๊ฑฐ๋ž˜ ์ด๋ ฅ ์กฐํšŒ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/sales/top', methods=['GET'])
def get_top_sales():
    """์ตœ๊ณ  ํŒ๋งค ๊ฑฐ๋ž˜"""
    try:
        limit = request.args.get('limit', default=10, type=int)
        days = request.args.get('days', default=7, type=int)
        
        top_sales = sales_service.get_top_sales(limit, days)
        
        return jsonify({
            'success': True,
            'data': top_sales,
            'count': len(top_sales)
        })
        
    except Exception as e:
        logger.error(f"์ตœ๊ณ  ํŒ๋งค ์กฐํšŒ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

# ============= ๋ถ„์„ API =============

@app.route('/api/analytics/collection/<int:collection_id>/sales', methods=['GET'])
def get_sales_analytics(collection_id):
    """ํŒ๋งค ๋ถ„์„"""
    try:
        days = request.args.get('days', default=30, type=int)
        
        stats = sales_service.get_collection_sales_stats(collection_id, days)
        
        return jsonify({
            'success': True,
            'data': stats
        })
        
    except Exception as e:
        logger.error(f"ํŒ๋งค ๋ถ„์„ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/analytics/collection/<int:collection_id>/holders', methods=['GET'])
def get_holder_analytics(collection_id):
    """์†Œ์œ ์ž ๋ถ„์„"""
    try:
        distribution = analytics.get_holder_distribution(collection_id)
        
        return jsonify({
            'success': True,
            'data': distribution
        })
        
    except Exception as e:
        logger.error(f"์†Œ์œ ์ž ๋ถ„์„ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

@app.route('/api/analytics/collection/<int:collection_id>/rarity', methods=['GET'])
def get_rarity_analytics(collection_id):
    """ํฌ๊ท€๋„ ๋ถ„์„"""
    try:
        analysis = analytics.analyze_rarity_price_correlation(collection_id)
        
        return jsonify({
            'success': True,
            'data': analysis
        })
        
    except Exception as e:
        logger.error(f"ํฌ๊ท€๋„ ๋ถ„์„ ์‹คํŒจ: {e}")
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

# ============= ํ—ฌ์Šค ์ฒดํฌ =============

@app.route('/api/health', methods=['GET'])
def health_check():
    """ํ—ฌ์Šค ์ฒดํฌ"""
    return jsonify({
        'status': 'healthy',
        'service': 'NFT Sales Manager API',
        'version': '1.0.0'
    })

# ============= ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ =============

@app.errorhandler(404)
def not_found(error):
    return jsonify({
        'success': False,
        'error': '์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
    }), 404

@app.errorhandler(500)
def internal_error(error):
    logger.error(f"์„œ๋ฒ„ ์˜ค๋ฅ˜: {error}")
    return jsonify({
        'success': False,
        'error': '์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'
    }), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=True)

์ด์ œ ์™„์ „ํ•œ REST API ์„œ๋ฒ„๊ฐ€ ์™„์„ฑ๋์–ด! ํ”„๋ก ํŠธ์—”๋“œ๋‚˜ ๋‹ค๋ฅธ ์„œ๋น„์Šค์—์„œ ์‰ฝ๊ฒŒ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์ง€. ๐ŸŽ‰

๐Ÿ“ก API ์‚ฌ์šฉ ์˜ˆ์ œ

import requests

# API ๋ฒ ์ด์Šค URL
BASE_URL = 'http://localhost:8000/api'

# 1. ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ
response = requests.post(f'{BASE_URL}/collections', json={
    'name': 'CoolNFTs',
    'contract_address': '0x1234567890abcdef',
    'description': '๋ฉ‹์ง„ NFT ์ปฌ๋ ‰์…˜',
    'total_supply': 10000
})
print(response.json())

# 2. NFT ๊ฒ€์ƒ‰
response = requests.get(f'{BASE_URL}/nfts/search', params={
    'collection_id': 1,
    'min_rarity': 80,
    'limit': 10
})
print(response.json())

# 3. ํŒ๋งค ํ†ต๊ณ„ ์กฐํšŒ
response = requests.get(f'{BASE_URL}/analytics/collection/1/sales', params={
    'days': 30
})
print(response.json())

# 4. ์ตœ๊ณ  ํŒ๋งค ์กฐํšŒ
response = requests.get(f'{BASE_URL}/sales/top', params={
    'limit': 5,
    'days': 7
})
print(response.json())

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

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐ŸŽ‰ ์šฐ๋ฆฌ๋Š” ํŒŒ์ด์ฌ๊ณผ SQLite๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์™„์ „ํ•œ NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์—ˆ์–ด. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„๋ถ€ํ„ฐ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”, REST API๊นŒ์ง€ ๋ชจ๋“  ๊ฑธ ๋‹ค๋ค˜์ง€!

๐Ÿ“š ํ•ต์‹ฌ ์š”์•ฝ

์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด ๊ฒƒ๋“ค:

โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„: NFT ํŒ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ €์žฅํ•˜๋Š” ํ…Œ์ด๋ธ” ๊ตฌ์กฐ
โœ… SQLite ํ™œ์šฉ: ๊ฐ€๋ณ๊ณ  ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‹œ์Šคํ…œ ๊ตฌ์ถ•
โœ… ์„œ๋น„์Šค ๋ ˆ์ด์–ด: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊น”๋”ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•
โœ… ๋ฐ์ดํ„ฐ ๋ถ„์„: Pandas๋ฅผ ํ™œ์šฉํ•œ ํ†ต๊ณ„ ๋ฐ ์ธ์‚ฌ์ดํŠธ ๋„์ถœ
โœ… ๋ณด์•ˆ ๋ฐ ์ตœ์ ํ™”: ํ”„๋กœ๋•์…˜ ๋ ˆ๋ฒจ์˜ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋…ธํ•˜์šฐ
โœ… ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”: ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ๊ธฐ๋ก
โœ… REST API: ์™ธ๋ถ€ ์„œ๋น„์Šค์™€ ์‰ฝ๊ฒŒ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค

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

์ด ์‹œ์Šคํ…œ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋” ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋ฐฉํ–ฅ๋“ค:

1. ํ”„๋ก ํŠธ์—”๋“œ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ• ๐Ÿ“Š
React๋‚˜ Vue.js๋กœ ์‹œ๊ฐ์ ์ธ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ์ง๊ด€์ ์œผ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.

2. ๋จธ์‹ ๋Ÿฌ๋‹ ๊ฐ€๊ฒฉ ์˜ˆ์ธก ๐Ÿค–
๊ณผ๊ฑฐ ํŒ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ํ•™์Šตํ•ด์„œ NFT ๊ฐ€๊ฒฉ์„ ์˜ˆ์ธกํ•˜๋Š” ๋ชจ๋ธ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.

3. ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๐Ÿ””
ํŠน์ • ์กฐ๊ฑด์ด ๋งŒ์กฑ๋˜๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด.

4. ๋‹ค์ค‘ ์ฒด์ธ ์ง€์› ๐ŸŒ
์ด๋”๋ฆฌ์›€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ Polygon, Solana ๋“ฑ ๋‹ค์–‘ํ•œ ๋ธ”๋ก์ฒด์ธ์„ ์ง€์›ํ•˜๋„๋ก ํ™•์žฅํ•  ์ˆ˜ ์žˆ์–ด.

5. GraphQL API ๐Ÿ“ก
REST API ๋Œ€์‹  GraphQL์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์œ ์—ฐํ•œ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด.

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

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

NFT์™€ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ„์† ํ•™์Šตํ•˜๊ณ  ์‹คํ—˜ํ•ด๋ณด๊ธธ ๋ฐ”๋ผ! ๐Ÿ’ช

ํŠนํžˆ ์ด๋Ÿฐ ๋ถ€๋ถ„๋“ค์„ ๋” ๊ณต๋ถ€ํ•ด๋ณด๋ฉด ์ข‹์•„:
โ€ข Web3 ๊ฐœ๋ฐœ ์‹ฌํ™” (์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์—ฐ๋™)
โ€ข ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ (PostgreSQL, Redis)
โ€ข ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ (AWS, GCP, Azure)
โ€ข ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… (Prometheus, Grafana)
โ€ข ํ…Œ์ŠคํŠธ ์ž๋™ํ™” (pytest, unittest)

๊ทธ๋ฆฌ๊ณ  ๋ฌด์—‡๋ณด๋‹ค ์ค‘์š”ํ•œ ๊ฑด, ์ง์ ‘ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋Š” ๊ฑฐ์•ผ! ์ด๋ก ๋งŒ ์•„๋Š” ๊ฒƒ๋ณด๋‹ค ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๋ถ€๋”ชํžˆ๋Š” ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๋Š” ๊ณผ์ •์—์„œ ์ง„์งœ ์‹ค๋ ฅ์ด ๋Š˜์–ด๋‚˜๊ฑฐ๋“ . ๐Ÿš€

๐ŸŽ ๋ณด๋„ˆ์Šค ํŒ: GitHub์— ์ด ํ”„๋กœ์ ํŠธ๋ฅผ ์˜ฌ๋ ค์„œ ํฌํŠธํด๋ฆฌ์˜ค๋กœ ํ™œ์šฉํ•ด๋ด! ์ž˜ ์ •๋ฆฌ๋œ README์™€ ํ•จ๊ป˜ ์˜ฌ๋ฆฌ๋ฉด ์ทจ์—…์ด๋‚˜ ํ”„๋ฆฌ๋žœ์„œ ํ™œ๋™ํ•  ๋•Œ ํฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ. ๊ทธ๋ฆฌ๊ณ  ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ณต๊ฐœํ•˜๋ฉด ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ํ˜‘์—…ํ•˜๋ฉด์„œ ๋” ๋งŽ์ด ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด!

์ž, ์ด์ œ ๋‹น์‹ ๋„ NFT ํŒ๋งค ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๋Šฅ๋ ฅ์„ ๊ฐ–์ท„์–ด! ๐ŸŽŠ ์ด ์ง€์‹์„ ๋ฐ”ํƒ•์œผ๋กœ ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ธธ ๋ฐ”๋ผ. ํ™”์ดํŒ…! ๐Ÿ’ชโœจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ์žˆ๋Š” ๋‚ด์šฉ์ด ํ•„์š”ํ•˜๋ฉด ์–ธ์ œ๋“ ์ง€ ๋ฌผ์–ด๋ด. ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€์ž! ๐Ÿค

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

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

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

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