paint-brush
๐ŸŒˆ๐Ÿฆ„ Bacalhau๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‚˜๋งŒ์˜ AI ์ƒ์„ฑ ์˜ˆ์ˆ  NFT DApp ๊ตฌ์ถ•โ€‚~์— ์˜ํ•ด@developerally
2,427 ํŒ๋…๊ฐ’
2,427 ํŒ๋…๊ฐ’

๐ŸŒˆ๐Ÿฆ„ Bacalhau๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‚˜๋งŒ์˜ AI ์ƒ์„ฑ ์˜ˆ์ˆ  NFT DApp ๊ตฌ์ถ•

~์— ์˜ํ•ด DeveloperAlly29m2023/02/08
Read on Terminal Reader
Read this story w/o Javascript

๋„ˆ๋ฌด ์˜ค๋ž˜; ์ฝ๋‹ค

FVM Hyperspace Testnet์—์„œ AI ์ƒ์„ฑ ์•„ํŠธ NFT๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์ž์‹ ๋งŒ์˜ Text-to Image ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DApp์„ ๊ตฌ์ถ•, ์‹คํ–‰ ๋ฐ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์™„์ „ํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ์ด ๋ธ”๋กœ๊ทธ์—์„œ๋Š” Tensorflow๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜คํ”ˆ ์†Œ์Šค Python ๊ธฐ๋ฐ˜ ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ์ €๋Š” ์˜๋„์ ์œผ๋กœ ์ด ์Šคํƒ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์˜คํ”ˆ ์†Œ์Šค ๋ฐ ๋ถ„์‚ฐ ๊ธฐ์ˆ ์„ ์ตœ๋Œ€ํ•œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

People Mentioned

Mention Thumbnail
featured image - ๐ŸŒˆ๐Ÿฆ„ Bacalhau๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‚˜๋งŒ์˜ AI ์ƒ์„ฑ ์˜ˆ์ˆ  NFT DApp ๊ตฌ์ถ•
DeveloperAlly HackerNoon profile picture
0-item

FVM Hyperspace Testnet์—์„œ AI ์ƒ์„ฑ ์•„ํŠธ NFT๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์ž์‹ ๋งŒ์˜ Text-to Image ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DApp์„ ๊ตฌ์ถ•, ์‹คํ–‰ ๋ฐ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์™„๋ฒฝํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค!


๋ชฉ์ฐจ

  • ๐Ÿ‘ฉโ€๐Ÿ’ป ์šฐ๋ฆฌ๊ฐ€ ํ•  ์ผ์€...
  • ๐Ÿ› ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ(๋‹ค์†Œ)
  • ๐Ÿฅž DApp ๊ธฐ์ˆ  ์Šคํƒ
  • ๐Ÿ—๏ธ Python ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
  • โš’๏ธ Solidity NFT ์Šคํฌ๋ฆฝํŠธ ๊ตฌ์ถ• ๋ฐ ๋ฐฐํฌ
  • ๐ŸŽฌ ํ”„๋ŸฐํŠธ ์—”๋“œ ์ƒํ˜ธ ์ž‘์šฉ ๊ตฌ์ถ•
    • ์™„์ „ํ•œ ํ๋ฆ„
    • ๋ฐ”์นผ๋žด์šฐ ์ƒํ˜ธ์ž‘์šฉ
    • NFT.์Šคํ† ๋ฆฌ์ง€
    • ๊ณ„์•ฝ ์ƒํ˜ธ์ž‘์šฉ
  • ๐ŸŒŸ ์ตœ์ข… ์ƒ๊ฐ: AI ๋ฐ ๋ธ”๋ก์ฒด์ธ์˜ ๊ฐ€๋Šฅ์„ฑ
  • ๐Ÿ  ๋ฐ”์นผ๋žด์šฐ ๋กœ๋“œ๋งต
  • โœ๏ธ ๊ณ„์† ์—ฐ๋ฝํ•˜์„ธ์š”!


๐Ÿฆ„ ๋น ๋ฅธ ๋งํฌ:


๐Ÿ‘ฉโ€๐Ÿ’ป ์šฐ๋ฆฌ๊ฐ€ ํ•  ์ผ์€...

์ด ๋ธ”๋กœ๊ทธ์—์„œ๋Š” ๋‹ค์Œ ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค.


  1. Tensorflow๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜คํ”ˆ์†Œ์Šค Python ๊ธฐ๋ฐ˜ ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค(๊ด€์‹ฌ์ด ์—†๋Š” ๊ฒฝ์šฐ Bacalhau HTTP ์—”๋“œํฌ์ธํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Œ).

  2. Bacalhau(๊ฐœ๋ฐฉํ˜• P2P ์˜คํ”„์ฒด์ธ ์ปดํ“จํŒ… ํ”Œ๋žซํผ)์—์„œ ์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”.

  3. Solidity์—์„œ NFT ๊ณ„์•ฝ ์ƒ์„ฑ(Open Zeppelin ERC721 ๊ณ„์•ฝ ๊ธฐ๋ฐ˜)

  4. Hardhat์„ ์‚ฌ์šฉํ•˜์—ฌ FVM(Filecoin Virtual Machine) ํ•˜์ดํผ์ŠคํŽ˜์ด์Šค ํ…Œ์ŠคํŠธ๋„ท์— NFT ๊ณ„์•ฝ ๋ฐฐํฌ

  5. ํ”„๋ŸฐํŠธ ์—”๋“œ ์ƒํ˜ธ ์ž‘์šฉ - Bacalhau ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์Šคํฌ๋ฆฝํŠธ ๋ฐ React์—์„œ NFT ๊ณ„์•ฝ๊ณผ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

  6. NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ NFT.Storage์— ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•

  7. Fleek์— ํ”„๋ŸฐํŠธ์—”๋“œ DApp์„ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•


์ €๋Š” ์˜๋„์ ์œผ๋กœ ์ด ์Šคํƒ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์˜คํ”ˆ ์†Œ์Šค ๋ฐ ๋ถ„์‚ฐ ๊ธฐ์ˆ ์„ ์ตœ๋Œ€ํ•œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.


์ด ๋ธ”๋กœ๊ทธ๋Š” ๊ฝค ๊ธธ์–ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. (๋ชจ๋“  ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์ดˆ๋ณด์ž ์นœํ™”์ ์ด๊ณ  ํฌ๊ด„์ ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค!) - ํ‘œ์—์„œ ์œ ์šฉํ•œ ๋ถ€๋ถ„์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ์…”๋„ ๋ฉ๋‹ˆ๋‹ค. ๋‚ด์šฉ <3

๐Ÿ› ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ(๋‹ค์†Œ)

๐Ÿฅž DApp ๊ธฐ์ˆ  ์Šคํƒ

(์•Œ๊ฒ ์Šต๋‹ˆ๋‹ค - ํŒฌ์ผ€์ดํฌ ๋”๋ฏธ์ž…๋‹ˆ๋‹ค #๋ฏธ์•ˆํ•ด์š”์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค)


์˜คํ”ˆ ์†Œ์Šค ๋ฐ Web3์˜ ๊ฐ€์น˜๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ :)



  • ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ [Solidity, Open Zeppelin]
    • Solidity ๋Š” Ethereum(EVM) ํ˜ธํ™˜ ๋ธ”๋ก์ฒด์ธ์„ ์œ„ํ•œ OO ์Šค๋งˆํŠธ ๊ณ„์•ฝ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์ž…๋‹ˆ๋‹ค.
    • Open Zeppelin์€ ์ผ๋ฐ˜์ ์ธ ์Šค๋งˆํŠธ ๊ณ„์•ฝ ๊ตฌ์„ฑ ์š”์†Œ ๋ฐ ๊ณ„์•ฝ์˜ ๋ณด์•ˆ ๊ฐ์‚ฌ ๊ตฌํ˜„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ์Šค๋งˆํŠธ ๊ณ„์•ฝ IDE [Hardhat]
    • Hardhat ์€ Ethereum ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ํŽธ์ง‘, ์ปดํŒŒ์ผ, ๋””๋ฒ„๊น… ๋ฐ ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์ž…๋‹ˆ๋‹ค.
  • ๋ธ”๋ก์ฒด์ธ ํ…Œ์ŠคํŠธ๋„ท [ํŒŒ์ผ์ฝ”์ธ ๊ฐ€์ƒ๋จธ์‹  ํ•˜์ดํผ์ŠคํŽ˜์ด์Šค]
    • FVM Hyperspace ๋Š” Filecoin ๋ธ”๋ก์ฒด์ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•๋œ EVM ํ˜ธํ™˜ ํ…Œ์ŠคํŠธ๋„ท์ž…๋‹ˆ๋‹ค.
  • NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ [NFT.Storage]
    • NFT.Storage๋Š” NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋ณ€์ ์ด๊ณ  ์ง€์†์ ์œผ๋กœ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด IPFS ๋ฐ Filecoin ์œ„์— ๊ตฌ์ถ•๋œ ๊ณต๊ณต์žฌ์ด๋ฉฐ NFT ๋ฐ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ SDK๋ฅผ ์œ„ํ•œ ๋ฌด๋ฃŒ ๋ถ„์‚ฐํ˜• ์ €์žฅ์†Œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ํ”„๋ก ํŠธ์—”๋“œ [NextJS / React + NPM]
    • ์šฐ๋ฆฌ ๋ชจ๋‘๋Š” ์ด๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค... ๊ทธ๋ ‡์ฃ ? :ํ”ผ
  • ํด๋ผ์ด์–ธํŠธ์˜ ์Šค๋งˆํŠธ ๊ณ„์•ฝ ์ƒํ˜ธ ์ž‘์šฉ [Metamask, Ethers, Chainstack RPC Node]
    • ๊ณต๊ฐœ RPC ๋…ธ๋“œ ์‚ฌ์šฉ - ๋ธ”๋ก์ฒด์ธ ๊ณ„์•ฝ๊ณผ ์ฝ๊ธฐ ์ „์šฉ ์ƒํ˜ธ ์ž‘์šฉ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • Metamask ๊ณต๊ธ‰์ž(๋˜๋Š” EIP-1193 ์—์„œ ์ง€์ •ํ•œ Ethereum API๋ฅผ ๋ธŒ๋ผ์šฐ์ €์— ์ฃผ์ž…ํ•˜๋Š” ์œ ์‚ฌํ•œ ์ง€๊ฐ‘)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ธ”๋ก์ฒด์ธ ๊ณ„์•ฝ์— ๋Œ€ํ•œ ์“ฐ๊ธฐ ํ˜ธ์ถœ์„ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.
    • Ethers js๋Š” EVM ํ˜ธํ™˜ ์Šค๋งˆํŠธ ๊ณ„์•ฝ๊ณผ ์ƒํ˜ธ ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.
  • AI Text-To-Image ์•ˆ์ • ํ™•์‚ฐ ์Šคํฌ๋ฆฝํŠธ [Python, Tensorflow]
    • TensorFlow ๋Š” ์‚ฌ์ „ ํ•™์Šต๋œ ๋ชจ๋ธ๊ณผ ๊ธฐํƒ€ ๋ฐ์ดํ„ฐ ๋ฐ ML ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์˜คํ”ˆ ์†Œ์Šค ๊ธฐ๊ณ„ ํ•™์Šต ํ”Œ๋žซํผ์ด์ž ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.
  • AI ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ถ„์‚ฐํ˜• ์˜คํ”„์ฒด์ธ ์ปดํ“จํŒ… [Bacalhau]
    • Bacalhau ๋Š” ๊ณต๊ฐœ์ ์ด๊ณ  ํˆฌ๋ช…ํ•˜๋ฉฐ ์„ ํƒ์ ์œผ๋กœ ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•œ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ์œ„ํ•œ ํ”Œ๋žซํผ์„ ์ œ๊ณตํ•˜๋Š” P2P ๊ฐœ๋ฐฉํ˜• ๊ณ„์‚ฐ ๋„คํŠธ์›Œํฌ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋ถ„์‚ฐ๋œ ์˜คํ”„์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.
  • ๋ถ„์‚ฐํ˜• DApp ๋ฐฐํฌ [Fleek]
    • Fleek์€ IPFS ๋ฐ Filecoin์— ์›น์‚ฌ์ดํŠธ ๋ฐฐํฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Vercel ๋˜๋Š” Netlify์˜ web3 ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋ถ„์‚ฐํ˜• ์•ฑ์ด ์žˆ๊ณ  ์ด๋ฅผ web2์— ๋ฐฐํฌํ•œ๋‹ค๊ณ  ๋งํ•  ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค! :๋””

๐Ÿ—๏ธ Python ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ


๐Ÿ’กTLDR ํŒ ๐Ÿ’ก

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ์ด๋ฏธ CLI ๋ฐ HTTP ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด Bacalhau๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ด ๋ถ€๋ถ„์„ ๊ฑด๋„ˆ๋›ฐ์–ด๋„ ๋ฉ๋‹ˆ๋‹ค.


์•ˆ์ • ํ™•์‚ฐ์— ๋Œ€ํ•œ ๋น ๋ฅธ ์†Œ๊ฐœ


Stable Diffusion์€ ํ˜„์žฌ ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ตœ๊ณ ์˜ ๊ธฐ๊ณ„ ํ•™์Šต ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค(&๋Š” Dall-E๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค). ์ด๋Š” ์ผ์ข…์˜ ๋”ฅ ๋Ÿฌ๋‹์ž…๋‹ˆ๋‹ค. ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์ž์ฒด์ ์œผ๋กœ ํ•™์Šตํ•˜๋Š” ๋จธ์‹  ๋Ÿฌ๋‹์˜ ํ•˜์œ„ ์ง‘ํ•ฉ์ž…๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋Š” ํ…์ŠคํŠธ ์ž…๋ ฅ์„ ์ด๋ฏธ์ง€ ์ถœ๋ ฅ์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.


์ด ์˜ˆ์—์„œ๋Š” ๋ณ€ํ™˜๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…์ŠคํŠธ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ™•์‚ฐ ํ™•๋ฅ  ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


ํ•˜์ง€๋งŒ ๊ฑฑ์ •ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ์ด๋ฅผ ์œ„ํ•ด ๊ธฐ๊ณ„ ํ•™์Šต ๋ชจ๋ธ์„ ๊ต์œกํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. (ํ•˜์ง€๋งŒ, ๊ทธ๊ฒŒ ๋‹น์‹ ์˜ ์ผ์ด๋ผ๋ฉด ์ „์ ์œผ๋กœ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!)


๋Œ€์‹  ML ๊ฐ€์ค‘์น˜๊ฐ€ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— Python ์Šคํฌ๋ฆฝํŠธ์—์„œ Google์˜ TensorFlow ์˜คํ”ˆ ์†Œ์Šค ๊ธฐ๊ณ„ ํ•™์Šต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์‚ฌ์ „ ํ›ˆ๋ จ๋œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


๋” ์ •ํ™•ํ•˜๊ฒŒ๋Š” ์›๋ž˜ ML ๋ชจ๋ธ์˜ ์ตœ์ ํ™”๋œ Keras/TensorFlow ๊ตฌํ˜„ ํฌํฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


ํŒŒ์ด์ฌ ์Šคํฌ๋ฆฝํŠธ


๐Ÿฆ„ Bacalhau ๋ฌธ์„œ ์™€ ์ด @BacalhauProject YouTube ๋™์˜์ƒ ์—์„œ ์ด ํ…์ŠคํŠธ-์ด๋ฏธ์ง€ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ณ  Dockeriseํ•˜๊ณ  Bacalhau์—์„œ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ „์ฒด ์—ฐ์Šต์„ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.๐Ÿฆ„ ๋˜ํ•œ ์ด Google Collabs Notebook ์—์„œ ์‹คํ–‰ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.


์ „์ฒด Python ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค!


 import argparse from stable_diffusion_tf.stable_diffusion import Text2Image from PIL import Image import os parser = argparse.ArgumentParser(description="Stable Diffusion") parser.add_argument("--h",dest="height", type=int,help="height of the image",default=512) parser.add_argument("--w",dest="width", type=int,help="width of the image",default=512) parser.add_argument("--p",dest="prompt", type=str,help="Description of the image you want to generate",default="cat") parser.add_argument("--n",dest="numSteps", type=int,help="Number of Steps",default=50) parser.add_argument("--u",dest="unconditionalGuidanceScale", type=float,help="Number of Steps",default=7.5) parser.add_argument("--t",dest="temperature", type=int,help="Number of Steps",default=1) parser.add_argument("--b",dest="batchSize", type=int,help="Number of Images",default=1) parser.add_argument("--o",dest="output", type=str,help="Output Folder where to store the Image",default="./") args=parser.parse_args() height=args.height width=args.width prompt=args.prompt numSteps=args.numSteps unconditionalGuidanceScale=args.unconditionalGuidanceScale temperature=args.temperature batchSize=args.batchSize output=args.output generator = Text2Image( img_height=height, img_width=width, jit_compile=False, # You can try True as well (different performance profile) ) img = generator.generate( prompt, num_steps=numSteps, unconditional_guidance_scale=unconditionalGuidanceScale, temperature=temperature, batch_size=batchSize, ) for i in range(0,batchSize): pil_img = Image.fromarray(img[i]) image = pil_img.save(f"{output}/image{i}.png")


์œ„์˜ ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹จ์ˆœํžˆ ํ…์ŠคํŠธ ํ”„๋กฌํ”„ํŠธ ์ž…๋ ฅ ์ธ์ˆ˜์™€ ๊ธฐํƒ€ ์„ ํƒ์  ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜จ ๋‹ค์Œ ๋ถ„๊ธฐ๋œ TensorFlow ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ถœ๋ ฅ ํŒŒ์ผ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.


์—ฌ๊ธฐ์—์„œ ์ˆ˜ํ–‰๋˜๋Š” ๋ชจ๋“  ์–ด๋ ค์šด ์ž‘์—…์€ ์•„๋ž˜ ์„น์…˜์—์„œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ๊ฐ€ ๊ธฐ๊ณ„ ํ•™์Šต ๋ชจ๋ธ์ด ๋งˆ๋ฒ•์„ ๋ฐœํœ˜ํ•˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. ๐Ÿช„


 generator = Text2Image( img_height=height, img_width=width, jit_compile=False, ) img = generator.generate( prompt, num_steps=numSteps, unconditional_guidance_scale=unconditionalGuidanceScale, temperature=temperature, batch_size=batchSize, )


์ข‹์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ํ”„๋กฌํ”„ํŠธ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์Œ... ์ด GPU ํ•„์ˆ˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ์œ„์น˜๋Š”..... ๐Ÿค”๐Ÿค”


๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์ด ๋ณธ์งˆ์ ์œผ๋กœ ์ž˜ ํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ์ด ์žˆ๋‹ค๋ฉด ๋ฐ”๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋ฌด์‹ ๋ขฐ ๋ฐ ๊ฒ€์—ด ์ €ํ•ญ๊ณผ ๊ฐ™์€ ๋‹ค๋ฅธ ๊ฐ•๋ ฅํ•œ ์†์„ฑ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์„ ํ†ตํ•œ ์ปดํ“จํŒ… ๋น„์šฉ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.


์ž‘์€ ์˜ˆ์ œ๋ฅผ ์œ„ํ•ด ๋กœ์ปฌ ์ปดํ“จํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์‹ค ๋‚˜๋Š” ์ด ํŠน์ • ์˜ˆ์ œ๋ฅผ ๋‚ด Mac M1์—์„œ ์ž‘๋™ํ•˜๋„๋ก ๊ด€๋ฆฌํ–ˆ์ง€๋งŒ(๋งค์šฐ ๋ถˆํ–‰ํ•จ) ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋ฐ ๋งค์šฐ ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค(ํƒ๊ตฌ ๊ฒŒ์ž„์„ ํ•˜๋Š” ์‚ฌ๋žŒ์ด ์žˆ์Šต๋‹ˆ๊นŒ?). ๋”ฐ๋ผ์„œ ๋” ํฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด ๋” ๋งŽ์€ ๊ฐ€์Šค๊ฐ€ ํ•„์š”ํ•˜๊ฒŒ ๋˜๋ฉฐ(๋ง์žฅ๋‚œ ์˜๋„) ์ง‘ ์ฃผ๋ณ€์— ์ „์šฉ ์„œ๋ฒ„๊ฐ€ ์—†์œผ๋ฉด ๊ฐ€์ƒ ๋จธ์‹ ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์šฐ๋“œ ์ปดํ“จํŒ… ํ”Œ๋žซํผ.


์ค‘์•™ ์ง‘์ค‘ํ™”๋˜์–ด ์žˆ์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ณ„์‚ฐ ๊ธฐ๊ณ„๋กœ๋ถ€ํ„ฐ ๋ฉ€๋ฆฌ ๋–จ์–ด์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋น„ํšจ์œจ์ ์ด๋ฉฐ ๋น„์šฉ์ด ๋งŽ์ด ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด GPU ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฌด๋ฃŒ ๊ณ„์ธต ํด๋ผ์šฐ๋“œ ์ปดํ“จํŒ… ์„œ๋น„์Šค๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ๊ณ (๋ˆ„๊ตฐ๊ฐ€ ์•”ํ˜ธํ™”ํ ์ฑ„๊ตด ๊ธˆ์ง€๋ผ๊ณ  ๋งํ–ˆ๋‚˜์š”..?) ํ•œ ๋‹ฌ์— US$400๊ฐ€ ๋„˜๋Š” ๊ณ„ํš์ด ๋‚˜์™”์Šต๋‹ˆ๋‹ค(๊ณ ๋งˆ์›Œ์š”).



๋ฐ”์นผ๋ผ์šฐ!


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


Bacalhau๋Š” IPFS, Filecoin ๋ฐ Web3์— ๋‚ด์žฌ๋œ ๋ถ„์‚ฐ ๊ฐ€์น˜๋ฅผ ๋ณด๋‹ค ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ํฌ๊ธฐํ•˜์ง€ ์•Š๊ณ  ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์˜คํ”„์ฒด์ธ ๊ณ„์‚ฐ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์˜ ๋ฏธ๋ž˜๋ฅผ ๋ฏผ์ฃผํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ฃผ๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


Bacalhau ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ IPFS(๋ฐ ๊ณง Filecoin)์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด Docker ์ปจํ…Œ์ด๋„ˆ ๋˜๋Š” ์›น ์–ด์…ˆ๋ธ”๋ฆฌ ์ด๋ฏธ์ง€๋ฅผ ์ž‘์—…์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๊ฐœ์ ์ด๊ณ  ํˆฌ๋ช…ํ•˜๋ฉฐ ์„ ํƒ์ ์œผ๋กœ ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•œ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ์œ„ํ•œ ํ”Œ๋žซํผ์„ ์ œ๊ณตํ•˜๋Š” P2P ๊ฐœ๋ฐฉํ˜• ๊ณ„์‚ฐ ๋„คํŠธ์›Œํฌ์ž…๋‹ˆ๋‹ค. US$400 ์ด์ƒ์ด ์•„๋‹Œ GPU ์ž‘์—…๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค!

์†Œ๊ฐœ | ๋ฐ”์นผ๋žด์šฐ ๋ฌธ์„œ

Bacalhau์—์„œ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰


์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด Bacalhau์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก Dockerise๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์—ฌ๊ธฐ ํŠœํ† ๋ฆฌ์–ผ์„ ๋”ฐ๋ผ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋‹จ ํ•œ ์ค„์˜ ์ฝ”๋“œ๋กœ Bacalhau CLI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๋‹ค๋ฅธ ํ•œ ์ค„๋กœ Bacalhau๋ฅผ ์„ค์น˜ํ•œ ํ›„).

 bacalhau docker run --gpu 1 ghcr.io/bacalhau-project/examples/stable-diffusion-gpu:0.0.1 -- python main.py --o ./outputs --p "Rainbow Unicorn" 



ํ•˜์ง€๋งŒ ์ด ์˜ˆ์—์„œ๋Š” ํ†ตํ•ฉ ์„น์…˜์—์„œ ๋ณด์—ฌ๋“œ๋ฆด ์ด ๊ณ ์ •๋œ ์•ˆ์ •์ ์ธ ํ™•์‚ฐ ์Šคํฌ๋ฆฝํŠธ์— ์—ฐ๊ฒฐํ•˜๋Š” HTTP ์—”๋“œํฌ์ธํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ๋Š” ์ด๊ฒƒ์ด ์›น3 ์นœํ™”์ ์ธ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ๋ฐฉ๋ฒ•์ด๋ผ๋Š” ์ ์— ์ฃผ๋ชฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ด ํ•˜๋‚˜์˜ ์ž‘์€ ๋ชจ๋ธ์—๋งŒ ๊ตญํ•œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜๋„ NFT ์Šคํฌ๋ฆฝํŠธ๋กœ ๋„˜์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค! :)

โš’๏ธ Solidity NFT ์Šคํฌ๋ฆฝํŠธ ๊ตฌ์ถ• ๋ฐ ๋ฐฐํฌ

์Šค๋งˆํŠธ ๊ณ„์•ฝ

NFT ์Šค๋งˆํŠธ ๊ณ„์•ฝ์€ Open Zeppelin์˜ ERC721 ๊ตฌํ˜„์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์ง€๋งŒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ‘œ์ค€ ํ™•์žฅ์ด ํฌํ•จ๋œ ERC721URIStorage ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค(๋”ฐ๋ผ์„œ NFT.Storage์— ์ €์žฅํ•  IPFS ์ฃผ์†Œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ณ„์•ฝ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค). .


์ด ๊ธฐ๋ณธ ๊ณ„์•ฝ์€ mint() ๋ฐ transfer()์™€ ๊ฐ™์€ ๊ธฐ๋Šฅ์ด ์ด๋ฏธ ๊ตฌํ˜„๋œ NFT ๊ณ„์•ฝ์˜ ์ผ๋ฐ˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.


๋˜ํ•œ ์ƒˆ๋กœ์šด NFT๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ๋งˆ๋‹ค ์ฒด์ธ์—์„œ ๋ฐฉ์ถœ๋  ์ด๋ฒคํŠธ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ”„๋ŸฐํŠธ ์—”๋“œ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ getter ํ•จ์ˆ˜๋„ ์ถ”๊ฐ€ํ–ˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” DApp์—์„œ ์˜จ์ฒด์ธ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ’ก ์ด ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋ฆฌ๋ฏน์Šค์—์„œ ์‚ฌ์šฉํ•ด ๋ณด๊ณ  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ™•์ธํ•˜์„ธ์š”! ๐Ÿ’ก


BacalhauFRC721.sol


 // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "@hardhat/console.sol"; contract BacalhauFRC721 is ERC721URIStorage { /** @notice Counter keeps track of the token ID number for each unique NFT minted in the NFT collection */ using Counters for Counters.Counter; Counters.Counter private _tokenIds; /** @notice This struct stores information about each NFT minted */ struct bacalhauFRC721NFT { address owner; string tokenURI; uint256 tokenId; } /** @notice Keeping an array for each of the NFT's minted on this contract allows me to get information on them all with a read-only front end call */ bacalhauFRC721NFT[] public nftCollection; /** @notice The mapping allows me to find NFT's owned by a particular wallet address. I'm only handling the case where an NFT is minted to an owner in this contract - but you'd need to handle others in a mainnet contract like sending to other wallets */ mapping(address => bacalhauFRC721NFT[]) public nftCollectionByOwner; /** @notice This event will be triggered (emitted) each time a new NFT is minted - which I will watch for on my front end in order to load new information that comes in about the collection as it happens */ event NewBacalhauFRC721NFTMinted( address indexed sender, uint256 indexed tokenId, string tokenURI ); /** @notice Creates the NFT Collection Contract with a Name and Symbol */ constructor() ERC721("Bacalhau NFTs", "BAC") { console.log("Hello Fil-ders! Now creating Bacalhau FRC721 NFT contract!"); } /** @notice The main function which will mint each NFT. The ipfsURI is a link to the ipfs content identifier hash of the NFT metadata stored on NFT.Storage. This data minimally includes name, description and the image in a JSON. */ function mintBacalhauNFT(address owner, string memory ipfsURI) public returns (uint256) { // get the tokenID for this new NFT uint256 newItemId = _tokenIds.current(); // Format info for saving to our array bacalhauFRC721NFT memory newNFT = bacalhauFRC721NFT({ owner: msg.sender, tokenURI: ipfsURI, tokenId: newItemId }); //mint the NFT to the chain _mint(owner, newItemId); //Set the NFT Metadata for this NFT _setTokenURI(newItemId, ipfsURI); _tokenIds.increment(); //Add it to our collection array & owner mapping nftCollection.push(newNFT); nftCollectionByOwner[owner].push(newNFT); // Emit an event on-chain to say we've minted an NFT emit NewBacalhauFRC721NFTMinted( msg.sender, newItemId, ipfsURI ); return newItemId; } /** * @notice helper function to display NFTs for frontends */ function getNFTCollection() public view returns (bacalhauFRC721NFT[] memory) { return nftCollection; } /** * @notice helper function to fetch NFT's by owner */ function getNFTCollectionByOwner(address owner) public view returns (bacalhauFRC721NFT[] memory){ return nftCollectionByOwner[owner]; }


์š”๊ตฌ์‚ฌํ•ญ

์ €๋Š” ์ด ๊ณ„์•ฝ์„ Filecoin Virtual Machine Hyperspace Testnet ์— ๋ฐฐํฌํ•  ์˜ˆ์ •์ด์ง€๋งŒ ์ด ๊ณ„์•ฝ์„ Polygon, BSC, Optimism, Arbitrum, Avalanche ๋“ฑ์„ ํฌํ•จํ•œ ๋ชจ๋“  EVM ํ˜ธํ™˜ ์ฒด์ธ์— ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฉ€ํ‹ฐ ์ฒด์ธ NFT๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ”„๋ŸฐํŠธ ์—”๋“œ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค(ํžŒํŠธ: ์ด ์ €์žฅ์†Œ )!


Hyperspace Testnet์— ๋ฐฐํฌํ•˜๋ ค๋ฉด ๋‹ค์Œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

  1. Metamask Wallet์„ Hyperspace Testnet์— ์„ค์ • ๋ฐ ์—ฐ๊ฒฐ
  2. ์ˆ˜๋„๊ผญ์ง€( Yoga ๋˜๋Š” Zondax )์—์„œ ํ…Œ์ŠคํŠธ tFIL ์ž๊ธˆ์„ ํ™•๋ณดํ•˜์„ธ์š”.


Hardhat๊ณผ ํ•จ๊ป˜ ์Šค๋งˆํŠธ ๊ณ„์•ฝ ๋ฐฐํฌ

์ €๋Š” hardhat์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด ๊ณ„์•ฝ์„ Hyperspace ํ…Œ์ŠคํŠธ๋„ท์— ๋ฐฐํฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ›ธ ์ดˆ๊ณต๊ฐ„ RPC ๋ฐ BlockExplorer ์˜ต์…˜:

๊ณต๊ฐœ RPC ๋์ 

๋ธ”๋ก์ต์Šคํ”Œ๋กœ๋Ÿฌ์˜

https://filecoin-hyperspace.chainstacklabs.com/rpc/v0

https://beryx.zondax.ch/

https://hyperspace.filfox.info/rpc/v0

https://fvm.starboard.ventures/contracts/

https://rpc.ankr.com/filecoin_testnet

https://explorer.glf.io/?network=hyperspacenet

์˜คํ”ˆ API : beryx.zondax.ch

https://hyperspace.filfox.info/en


๊ตฌ์„ฑ ์„ค์ •์„ ์œ„ํ•ด ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ณต๊ฐœ RPC ์—”๋“œํฌ์ธํŠธ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


hardhat.config.ts


 import '@nomicfoundation/hardhat-toolbox'; import { config as dotenvConfig } from 'dotenv'; import { HardhatUserConfig } from 'hardhat/config'; import { resolve } from 'path'; //Import our customised tasks // import './pages/api/hardhat/tasks'; const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || './.env'; dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) }); // Ensure that we have all the environment variables we need. const walletPrivateKey: string | undefined = process.env.WALLET_PRIVATE_KEY; if (!walletPrivateKey) { throw new Error('Please set your Wallet private key in a .env file'); } const config: HardhatUserConfig = { solidity: '0.8.17', defaultNetwork: 'filecoinHyperspace', networks: { hardhat: {}, filecoinHyperspace: { url: 'https://api.hyperspace.node.glif.io/rpc/v1', chainId: 3141, accounts: [process.env.WALLET_PRIVATE_KEY ?? 'undefined'], }, // bleeding edge often-reset FVM testnet filecoinWallaby: { url: 'https://wallaby.node.glif.io/rpc/v0', chainId: 31415, accounts: [process.env.WALLET_PRIVATE_KEY ?? 'undefined'], //explorer: https://wallaby.filscan.io/ and starboard }, }, // I am using the path mapping so I can keep my hardhat deployment within the /pages folder of my DApp and therefore access the contract ABI for use on my frontend paths: { root: './pages/api/hardhat', tests: './pages/api/hardhat/tests', //who names a directory in the singular?!!! Grammarly would not be happy cache: './pages/api/hardhat/cache', }, }; export default config;

๊ทธ๋ฆฌ๊ณ  ์Šค๋งˆํŠธ ๊ณ„์•ฝ์„ ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์„œ๋ช…์ž(์†Œ์œ ์ž)๋กœ ์ง€๊ฐ‘ ์ฃผ์†Œ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค์ •ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ž‘์„ฑํ•˜๋Š” ์‹œ์ ์— FEVM์—์„œ ์—ฌ์ „ํžˆ ์ž‘๋™ ์ค‘์ธ ๋ช‡ ๊ฐ€์ง€ ๋งคํ•‘ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ญ”๊ฐ€ ์ด์ƒํ•œ ํ–‰๋™.


deploy/deployBacalhauFRC721.ts


 import hre from 'hardhat'; import type { BacalhauFRC721 } from '../typechain-types/contracts/BacalhauFRC721'; import type { BacalhauFRC721__factory } from '../typechain-types/factories/contracts/BacalhauFRC721__factory'; async function main() { console.log('Bacalhau721 deploying....'); // !!!needed as hardhat's default does not map correctly to the FEVM const owner = new hre.ethers.Wallet( process.env.WALLET_PRIVATE_KEY || 'undefined', hre.ethers.provider ); const bacalhauFRC721Factory: BacalhauFRC721__factory = < BacalhauFRC721__factory > await hre.ethers.getContractFactory('BacalhauFRC721', owner); const bacalhauFRC721: BacalhauFRC721 = <BacalhauFRC721>( await bacalhauFRC721Factory.deploy() ); await bacalhauFRC721.deployed(); console.log('bacalhauFRC721 deployed to ', bacalhauFRC721.address); // optionally log to a file here } main().catch((error) => { console.error(error); process.exitCode = 1; });

๋ฐฐํฌํ•˜๋ ค๋ฉด ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ„ฐ๋ฏธ๋„์—์„œ ์œ„ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. (์ฃผ์˜: ๊ตฌ์„ฑ์—์„œ ๊ธฐ๋ณธ ๋„คํŠธ์›Œํฌ๋ฅผ filecoinHyperspace๋กœ ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์— ํ‘œ์‹œ๋˜์–ด ์žˆ์ง€๋งŒ ๋„คํŠธ์›Œํฌ์— ๋Œ€ํ•œ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ „๋‹ฌํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค.)

> cd ./pages/hardhat/deploy/


 npx hardhat run ./deployBacalhauFRC721.ts --network filecoinHyperspace


์ถ•ํ•˜ํ•˜๋‹ค! ๋ฐฉ๊ธˆ Filecoin ์ดˆ๊ณต๊ฐ„ ํ…Œ์ŠคํŠธ๋„ท์— NFT ๊ณ„์•ฝ์„ ๋ฐฐํฌํ–ˆ์Šต๋‹ˆ๋‹ค!

๐ŸŽฌ ํ”„๋ŸฐํŠธ ์—”๋“œ ์ƒํ˜ธ ์ž‘์šฉ ๊ตฌ์ถ•

์šฐ์™€~ ์˜ˆ์œ ๋ถ€๋ถ„์ด๊ตฐ์š”... ๊ทธ๋ฆฌ๊ณ  ์—ฌ๊ธฐ ๋ชจ๋‘๋ฅผ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์ฃผ๋Š” ์ ‘์ฐฉ์ œ๋„ ์žˆ์–ด์š” :)


ํ”„๋ŸฐํŠธ ์—”๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•ด NextJS์™€ Typescript๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์†”์งํžˆ ๋งํ•ด์„œ ์ €๋Š” NextJS์˜ SSR(์„œ๋ฒ„ ์ธก ๋ Œ๋”๋ง) ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ์œผ๋ฉฐ ํŽ˜์ด์ง€ ๋ผ์šฐํŒ…๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค(๋‹จ์ผ ํŽ˜์ด์ง€ Dapp์ด๊ธฐ ๋•Œ๋ฌธ์—). ๋ฐ”๋‹๋ผ React ์„ค์ •(๋˜๋Š” ๋ฌผ๋ก  ์„ ํƒํ•œ ํ”„๋ ˆ์ž„์›Œํฌ!)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


Typescript์— ๊ด€ํ•ด์„œ๋Š”... ์Œ, ์ €๋Š” ์ด๊ฒƒ์„ ์•ฝ๊ฐ„ ๊ธ‰ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๊ณ  ์ด๊ฒƒ์ด Typescript์˜ ์•„์ฃผ ์ข‹์€ ์˜ˆ๋Š” ์•„๋‹ˆ๋ผ๋Š” ์ ์„ ์ธ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ vars๋Š” ํ–‰๋ณตํ•ด ๋ณด์ž…๋‹ˆ๋‹ค... ;)



Anyhoo - ์ด ์„น์…˜์˜ ์ฃผ์š” ์š”์ ์€ ํ”„๋ŸฐํŠธ ์—”๋“œ ์ฝ”๋”ฉ ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์Šค๋งˆํŠธ ๊ณ„์•ฝ์ธ Bacalhau(์•ˆ์ •์ ์ธ ํ™•์‚ฐ ML ๋ชจ๋ธ ์‚ฌ์šฉ) ๋ฐ ๋ฌผ๋ก  NFT.Storage์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. NotOnIPFSNotYourNFT.

์™„์ „ํ•œ ํ๋ฆ„

[todo: ์ˆœ์„œ๋„ ๋‹ค์ด์–ด๊ทธ๋žจ ์ž‘์„ฑ]

  • ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ ํ•„๋“œ์— ํ…์ŠคํŠธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค ->
  • ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ -> Bacalhau Job์„ ํ˜ธ์ถœํ•˜์—ฌ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
  • Bacalhau ์ž‘์—… ์™„๋ฃŒ -> ํ˜•์‹์ด NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON ๊ฐœ์ฒด๋กœ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž๊ฐ€ Mint NFT ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค -> NFT.Storage๊ฐ€ NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ๋˜๊ณ  ํด๋”์— ๋Œ€ํ•œ IPFS CID์™€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. -> ์Šค๋งˆํŠธ ๊ณ„์•ฝ์˜ Mint NFT ๊ธฐ๋Šฅ์ด ์ด IPFS_URI๋กœ ํ˜ธ์ถœ๋˜์–ด ์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋กœ NFT๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • !! [FEVM ๋ฌธ์ œ] -> ์—ฌ๊ธฐ์„œ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์ด ๊ฒฐ๊ณผ์˜ TX(ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ)๊ฐ€ ๋ฐ˜ํ™˜๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ์ง€๋งŒ ํ˜„์žฌ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋Œ€์‹  ๊ณ„์•ฝ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๊ฒƒ์ด ์™„๋ฃŒ๋˜๋Š” ์‹œ์ ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ์™„๋ฃŒ! -> ์ด์ œ ๋ชจ๋“  ๋””์Šคํ”Œ๋ ˆ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ณ  ์‚ฌ์šฉ์ž ์ƒํƒœ์— ๋Œ€ํ•œ ๋ฐœํ–‰ ์„ฑ๊ณต ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์ข‹์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์ฝ”๋“œ์—์„œ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

๋ฐ”์นผ๋žด์šฐ ์ƒํ˜ธ์ž‘์šฉ

Bacalhau์šฉ ํ”„๋ŸฐํŠธ ์—”๋“œ API ์—”๋“œํฌ์ธํŠธ ์ƒ์„ฑ์€ ์—”์ง€๋‹ˆ์–ด Luke Marsden ์ด ์ž‘์„ฑํ•œ ์ด ํ”„๋กœ์ ํŠธ ๋ณด๊ณ ์„œ ์— ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


API๋Š” ํ˜„์žฌ ์ด ๋ธ”๋กœ๊ทธ์— ๋ฌธ์„œํ™”๋œ ์•ˆ์ •์ ์ธ ํ™•์‚ฐ ์Šคํฌ๋ฆฝํŠธ ์—๋งŒ ์ง์ ‘์ ์œผ๋กœ ์ ์šฉ๋˜์ง€๋งŒ ํŒ€์—์„œ๋Š” HTTP์—์„œ ์˜ˆ์ œ์™€ ์ž์ฒด ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ์ด๋ฅผ ๋ณด๋‹ค ์ผ๋ฐ˜์ ์ธ API๋กœ ํ™•์žฅํ•˜๋Š” ๊ณผ์ •์— ์žˆ์Šต๋‹ˆ๋‹ค. REST API. ์—ฌ๊ธฐ ๋˜๋Š” FilecoinProject Slack์˜ #bacalhau ์ฑ„๋„์—์„œ ์ด ๋‚ด์šฉ์„ ๊ณ„์† ์ง€์ผœ๋ณด์„ธ์š”.


>run/test in terminal


 curl -XPOST -d '{"prompt": "rainbow unicorn"}' 'http://dashboard.bacalhau.org:1000/api/v1/stablediffusion';


>react / typescript code


 import { CID } from 'multiformats/cid'; export const callBacalhauJob = async (promptInput: string) => { //Bacalahau HTTP Stable Diffusion Endpoint const url = 'http://dashboard.bacalhau.org:1000/api/v1/stablediffusion'; const headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; const data = { prompt: promptInput, //The user text prompt! }; /* FETCH FROM BACALHAU ENDPOINT */ const cid = await fetch(url, { method: 'POST', body: JSON.stringify(data), headers: headers, }) .then(async (res) => { let body = await res.json(); if (body.cid) { /* Bacalhau returns a V0 CID which we want to convert to a V1 CID for easier usage with http gateways (ie. displaying the image on web), so I'm using the IPFS multiformats package to convert it here */ return CID.parse(body.cid).toV1().toString(); } }) .catch((err) => { console.log('error in bac job', err); }); return cid; };


์ด ํ•จ์ˆ˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํด๋” ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„ IPFS CID(์ฝ˜ํ…์ธ  ์‹๋ณ„์ž)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ด๋ฏธ์ง€๋Š” /outputs/image0.png ์•„๋ž˜์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ’ก ์—ฌ๊ธฐ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ง์ ‘ ํ™•์ธํ•ด๋ณด์„ธ์š” ! ๐Ÿ’ก




์•„ ๋ฌด์ง€๊ฐœ ์œ ๋‹ˆ์ฝ˜... ๋งˆ์Œ์— ์•ˆ ๋“œ๋Š” ๊ฒŒ ๋ญ์ฃ !

NFT.์Šคํ† ๋ฆฌ์ง€

NFT.Storage๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋˜๋Š” HTTP SDK๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ IPFS ๋ฐ Filecoin์— ์˜๊ตฌ์ ์œผ๋กœ ์‰ฝ๊ฒŒ ์ €์žฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ณต๊ณต์žฌ(๋ฌด๋ฃŒ)์ž…๋‹ˆ๋‹ค.


NFT ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” Open Zeppelin ๋ฌธ์„œ์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜จ ์•„๋ž˜ ์˜ˆ์™€ ์œ ์‚ฌํ•œ JSON ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.



NFT๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ฒด์ธ์— ์ €์žฅํ•˜์ง€ ์•Š๋Š” ํ•œ(๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ์˜ ๊ฒฝ์šฐ ์—„์ฒญ๋‚˜๊ฒŒ ๋น„์šฉ์ด ๋งŽ์ด ๋“ค ์ˆ˜ ์žˆ์Œ) ํ† ํฐ์˜ '๋Œ€์ฒด ๋ถˆ๊ฐ€๋Šฅ์„ฑ'์„ ์ค€์ˆ˜ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ €์žฅ์†Œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ง€์†์ ์ด๊ณ  ์•ˆ์ •์ ์ด๋ฉฐ ๋ถˆ๋ณ€ํ•ฉ๋‹ˆ๋‹ค.


NFT์— ์œ„์˜ ์˜ˆ์™€ ๊ฐ™์€ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ฃผ์†Œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ํŒ๋งค ํ›„ ์ด ์œ„์น˜ ๊ฒฝ๋กœ๋ฅผ ์ „ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๊ตฌ๋งคํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ NFT๊ฐ€ ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฒƒ์œผ๋กœ ๋ฐ”๋€Œ๊ฑฐ๋‚˜ ์ด ๊ฒฝ์šฐ ๋ฌธ์ž ๊ทธ๋Œ€๋กœ ์–‘ํƒ„์ž๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. NFT ์ œ์ž‘์ž๊ฐ€ ๋Ÿฌ๊ทธ ์‚ฌ์ง„์˜ ์•„ํŠธ ์ด๋ฏธ์ง€๋ฅผ ์ „ํ™˜ํ•œ ๊ณณ์€ ์•„๋ž˜์ž…๋‹ˆ๋‹ค.



Open Zeppelin์—์„œ๋„ ๊ฒฝ๊ณ ํ•˜๋Š” ๋‚ด์šฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค!



NFT.Storage๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์€ IPFS์— ๊ณ ์ •๋  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ง€์†์„ฑ์„ ์œ„ํ•ด Filecoin์— ์ €์žฅ๋˜๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ IPFS ํŒŒ์ผ CID( ์ฝ˜ํ…์ธ  - ์œ„์น˜๊ฐ€ ์•„๋‹˜ - ID ์‹๋ณ„์ž)๋ฅผ ์–ป๋Š”๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. NFT.Storage๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ด์— ๋Œ€ํ•œ API ํ‚ค (.env ํŒŒ์ผ์— ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด)๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.


.env example


 NEXT_PUBLIC_NFT_STORAGE_API_KEY=xxx


๋˜ํ•œ FVM์—๋Š” (์•„์ง!) NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON์„ ์ƒ์„ฑํ–ˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค... ์šฐ๋ฆฌ๋Š” NFT๊ฐ€ ์ฑ„ํƒ๋  ๋•Œ ์—ฌ์ „ํžˆ ํ‘œ์ค€์„ ์ค€์ˆ˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. .


 import { NFTStorage } from 'nft.storage'; //connect to NFT.Storage Client const NFTStorageClient = new NFTStorage({ token: process.env.NEXT_PUBLIC_NFT_STORAGE_API_KEY, }); const createNFTMetadata = async ( promptInput: string, imageIPFSOrigin: string, //the ipfs path eg. ipfs://[CID] imageHTTPURL: string //an ipfs address fetchable through http for the front end to use (ie. including an ipfs http gateway on it like https://[CID].ipfs.nftstorage.link) ) => { console.log('Creating NFT Metadata...'); let nftJSON; // let's get the image data Blob from the IPFS CID that was returned from Bacalhau earlier... await getImageBlob(status, setStatus, imageHTTPURL).then( async (imageData) => { // Now let's create a unique CID for that image data - since we don't really want the rest of the data returned from the Bacalhau job.. await NFTStorageClient.storeBlob(imageData) .then((imageIPFS) => { console.log(imageIPFS); //Here's the JSON construction - only name, description and image are required fields- but I also want to save some other properties like the ipfs link and perhaps you have other properties that give your NFT's rarity to add as well nftJSON = { name: 'Bacalhau Hyperspace NFTs 2023', description: promptInput, image: imageIPFSOrigin, properties: { prompt: promptInput, type: 'stable-diffusion-image', origins: { ipfs: `ipfs://${imageIPFS}`, bacalhauipfs: imageIPFSOrigin, }, innovation: 100, content: { 'text/markdown': promptInput, }, }, }; }) .catch((err) => console.log('error creating blob cid', err)); } ); return nftJSON; };


์ด์ œ ์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ NFT.Storage์— ์ €์žฅํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!


 await NFTStorageClient.store(nftJson) .then((metadata) => { // DONE! - do something with this returned metadata! console.log('NFT Data pinned to IPFS & stored on Filecoin!'); console.log('Metadata URI: ', metadata.url); // once saved we can use it to mint the NFT // mintNFT(metadata); }) .catch((err) => { console.log('error uploading to nft.storage'); });


Woot - Bacalhau์˜ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๊ณ  NFT.Storage๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋ณ€์ ์ด๊ณ  ์ง€์†์ ์œผ๋กœ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ NFT๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!


๐Ÿ’ก ๋น ๋ฅธ ํŒ ๐Ÿ’กNFT.Storage๋Š” ๋˜ํ•œ storeCar ๋ฐ storeDirectory์™€ ๊ฐ™์€ ๋‹ค์–‘ํ•œ API ํ˜ธ์ถœ ๊ณผ CID์˜ IPFS ๊ณ ์ • ๋ฐ Filecoin ์ €์žฅ ๊ฑฐ๋ž˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” status() ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. -> ์ด๋Š” ๋งค์šฐ ๋ฉ‹์ง„ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. NFT ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ FEVM DApp(๋˜๋Š” FEVM์ด ๋ฉ”์ธ๋„ท ๋ฆด๋ฆฌ์Šค์— ๋„๋‹ฌํ•˜๋ฉด FEVM์— NFT ๊ตฌํ˜„).

๊ณ„์•ฝ ์ƒํ˜ธ์ž‘์šฉ

์—ฌ๊ธฐ์—๋Š” 3๊ฐ€์ง€ ์œ ํ˜•์˜ ์ƒํ˜ธ ์ž‘์šฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค. (๊ทธ๋ฆฌ๊ณ  ๋ช‡ ๊ฐ€์ง€ FEVM ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฒ ํƒ€ ๊ธฐ์ˆ ์—๋Š” ํ•ญ์ƒ ๊ธฐ๋ฐœํ•œ ๋ฒ„๊ทธ ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค!)


  • ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ์ฒด์ธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ธฐ ์œ„ํ•œ ์ฝ๊ธฐ ์ „์šฉ ํ˜ธ์ถœ

  • ์„œ๋ช…ํ•˜๊ณ  ๊ฐ€์Šค๋ฅผ ์ง€๋ถˆํ•˜๊ธฐ ์œ„ํ•ด ์ง€๊ฐ‘์ด ํ•„์š”ํ•œ ํ˜ธ์ถœ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. NFT ๋ฐœํ–‰๊ณผ ๊ฐ™์ด ์ฒด์ธ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ธฐ๋Šฅ!

  • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ - ๊ณ„์•ฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค.


์ด๋Ÿฌํ•œ ๋ชจ๋“  ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด Ethereum API์šฉ ๊ฒฝ๋Ÿ‰ ๋ž˜ํผ์ธ ethers.js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ณ„์•ฝ์— ์—ฐ๊ฒฐํ•˜๊ณ  ํ˜ธ์ถœ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.


๊ณต๊ฐœ RPC๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ๊ธฐ ๋ชจ๋“œ๋กœ ๊ณ„์•ฝ์— ์—ฐ๊ฒฐ:


 //The compiled contract found in pages/api/hardhat/artifacts/contracts import BacalhauCompiledContract from '@Contracts/BacalhauFRC721.sol/BacalhauFRC721.json'; //On-chain address of the contract const contractAddressHyperspace = '0x773d8856dd7F78857490e5Eea65111D8d466A646'; //A public RPC Endpoint (see table from contract section) const rpc = 'https://api.hyperspace.node.glif.io/rpc/v1'; const provider = new ethers.providers.JsonRpcProvider(rpc); const connectedReadBacalhauContract = new ethers.Contract( contractAddressHyperspace, BacalhauCompiledContract.abi, provider );


๊ณ„์•ฝ์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ฝ๊ธฐ ์ „์šฉ(๊ฐ€์ ธ์˜ค๊ธฐ) ์ด๋ฒคํŠธ์ด๋ฏ€๋กœ ๊ณต๊ฐœ RPC๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜จ์ฒด์ธ์—์„œ ์ด๋ฒคํŠธ ๋ฐฉ์ถœ์„ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


 //use the read-only connected Bacalhau Contract connectedReadBacalhauContract.on( // Listen for the specific event we made in our contract 'NewBacalhauFRC721NFTMinted', (sender: string, tokenId: number, tokenURI: string) => { //DO STUFF WHEN AN EVENT COMES IN // eg. re-fetch NFT's, store in state and change page status } );


์“ฐ๊ธฐ ๋ชจ๋“œ์—์„œ ๊ณ„์•ฝ์— ์—ฐ๊ฒฐ - ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ฑฐ๋ž˜์— ์„œ๋ช…ํ•˜๊ณ  ๊ฐ€์Šค ๋น„์šฉ์„ ์ง€๋ถˆํ•  ์ˆ˜ ์žˆ๋„๋ก Ethereum ๊ฐ์ฒด๊ฐ€ ์ง€๊ฐ‘์— ์˜ํ•ด ์›น ๋ธŒ๋ผ์šฐ์ €์— ์ฃผ์ž…๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์šฐ๋ฆฌ๊ฐ€ window.ethereum์„ ํ™•์ธํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. ๋ฌผ์ฒด.


 //Typescript needs to know window is an object with potentially and ethereum value. There might be a better way to do this? Open to tips! declare let window: any; //The compiled contract found in pages/api/hardhat/artifacts/contracts import BacalhauCompiledContract from '@Contracts/BacalhauFRC721.sol/BacalhauFRC721.json'; //On-chain address of the contract const contractAddressHyperspace = '0x773d8856dd7F78857490e5Eea65111D8d466A646'; //check for the ethereum object if (!window.ethereum) { //ask user to install a wallet or connect //abort this } // else there's a wallet provider else { // same function - different provider - this one has a signer - the user's connected wallet address const provider = new ethers.providers.Web3Provider(window.ethereum); const contract = new ethers.Contract( contractAddressHyperspace, BacalhauCompiledContract.abi, provider ); const signer = provider.getSigner(); const connectedWriteBacalhauContract = contract.connect(signer); }

์“ฐ๊ธฐ ์—ฐ๊ฒฐ ๊ณ„์•ฝ์„ ์‚ฌ์šฉํ•˜์—ฌ mint ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.


๋จผ์ €, ์‚ฌ์šฉ์ž์˜ ์ง€๊ฐ‘ ์ฃผ์†Œ๊ฐ€ ์žˆ๊ณ  FVM Hyperspace ์ฒด์ธ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. chainId๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•, ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ Hyperspace ๋„คํŠธ์›Œํฌ๋ฅผ Metamask/์ง€๊ฐ‘์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ• ๋“ฑ ์—ฌ๋Ÿฌ๋ถ„์ด ์›ํ•  ์ˆ˜ ์žˆ๋Š” ๋ช‡ ๊ฐ€์ง€ ์œ ์šฉํ•œ ์ง€๊ฐ‘ ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. Ethereum ๊ฐœ์ฒด๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ethers.js๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ง€๊ฐ‘๊ณผ ์ƒํ˜ธ ์ž‘์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


 declare let window: any; const fetchWalletAccounts = async () => { console.log('Fetching wallet accounts...'); await window.ethereum //use ethers? .request({ method: 'eth_requestAccounts' }) .then((accounts: string[]) => { return accounts; }) .catch((error: any) => { if (error.code === 4001) { // EIP-1193 userRejectedRequest error console.log('Please connect to MetaMask.'); } else { console.error(error); } }); }; const fetchChainId = async () => { console.log('Fetching chainId...'); await window.ethereum .request({ method: 'eth_chainId' }) .then((chainId: string[]) => { return chainId; }) .catch((error: any) => { if (error.code === 4001) { // EIP-1193 userRejectedRequest error console.log('Please connect to MetaMask.'); } else { console.error(error); } }); }; //!! This function checks for a wallet connection WITHOUT being intrusive to to the user or opening their wallet export const checkForWalletConnection = async () => { if (window.ethereum) { console.log('Checking for Wallet Connection...'); await window.ethereum .request({ method: 'eth_accounts' }) .then(async (accounts: String[]) => { console.log('Connected to wallet...'); // Found a user wallet return true; }) .catch((err: Error) => { console.log('Error fetching wallet', err); return false; }); } else { //Handle no wallet connection return false; } }; //Subscribe to changes on a user's wallet export const setWalletListeners = () => { console.log('Setting up wallet event listeners...'); if (window.ethereum) { // subscribe to provider events compatible with EIP-1193 standard. window.ethereum.on('accountsChanged', (accounts: any) => { //logic to check if disconnected accounts[] is empty if (accounts.length < 1) { //handle the locked wallet case } if (userWallet.accounts[0] !== accounts[0]) { //user has changed address } }); // Subscribe to chainId change window.ethereum.on('chainChanged', () => { // handle changed chain case }); } else { //handle the no wallet case } }; export const changeWalletChain = async (newChainId: string) => { console.log('Changing wallet chain...'); const provider = window.ethereum; try { await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: newChainId }], //newChainId }); } catch (error: any) { alert(error.message); } }; //AddHyperspaceChain export const addHyperspaceNetwork = async () => { console.log('Adding the Hyperspace Network to Wallet...'); if (window.ethereum) { window.ethereum .request({ method: 'wallet_addEthereumChain', params: [ { chainId: '0xc45', rpcUrls: [ 'https://hyperspace.filfox.info/rpc/v0', 'https://filecoin-hyperspace.chainstacklabs.com/rpc/v0', ], chainName: 'Filecoin Hyperspace', nativeCurrency: { name: 'tFIL', symbol: 'tFIL', decimals: 18, }, blockExplorerUrls: [ 'https://fvm.starboard.ventures/contracts/', 'https://hyperspace.filscan.io/', 'https://beryx.zondax.chfor', ], }, ], }) .then((res: XMLHttpRequestResponseType) => { console.log('added hyperspace successfully', res); }) .catch((err: ErrorEvent) => { console.log('Error adding hyperspace network', err); }); } };


์“ฐ๊ธฐ ๋ชจ๋“œ์—์„œ ๊ณ„์•ฝ ๋ฐœํ–‰ ๊ธฐ๋Šฅ์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค....


 // Pass in the metadata return from saving to NFT.Storage const mintNFT = async (metadata: any) => { await connectedWriteBacalhauContract // The name of our function in our smart contract .mintBacalhauNFT( userWallet.accounts[0], //users account to use metadata.url //test ipfs address ) .then(async (data: any) => { console.log('CALLED CONTRACT MINT FUNCTION', data); await data .wait() .then(async (tx: any) => { console.log('tx', tx); //CURRENTLY NOT RETURNING TX - (I use event triggering to know when this function is complete) let tokenId = tx.events[1].args.tokenId.toString(); console.log('tokenId args', tokenId); setStatus({ ...INITIAL_TRANSACTION_STATE, success: successMintingNFTmsg(data), }); }) .catch((err: any) => { console.log('ERROR', err); setStatus({ ...status, loading: '', error: errorMsg(err.message, 'Error minting NFT'), }); }); }) .catch((err: any) => { console.log('ERROR1', err); setStatus({ ...status, loading: '', error: errorMsg( err && err.message ? err.message : null, 'Error minting NFT' ), }); }); }


์šฐ์šฐ - NFT ๋ฐœํ–‰!! ์œ ๋‹ˆ์ฝ˜ ๋Œ„์Šค ๋ชจ๋“œ ์‹œ๊ฐ„!

๐ŸŒŸ ์ตœ์ข… ์ƒ๊ฐ: AI ๋ฐ ๋ธ”๋ก์ฒด์ธ์˜ ๊ฐ€๋Šฅ์„ฑ

Bacalhau๋Š” ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋ฐ˜๋ณต์ ์ด๊ณ  ๊ฒฐ์ •์ ์ธ ์ฒ˜๋ฆฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

  • ETL ํ”„๋กœ์„ธ์Šค

  • ๋จธ์‹ ๋Ÿฌ๋‹ ๋ฐ AI

  • IOT ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ

  • ๋‹ค์Œ์„ ํฌํ•จํ•œ ์ผ๊ด„ ์ฒ˜๋ฆฌ

    • ๊ธˆ์œต ๋ฐ ์‹œ์žฅ ๋ฐ์ดํ„ฐ
  • ๋น„๋””์˜ค ๋ฐ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ - ๊ด‘๊ณ  ์†Œ์žฌ์— ์ ํ•ฉ


Bacalhau ๋ฌธ์„œ ์—๋Š” ์œ„์˜ ์ผ๋ถ€๋ฅผ ๋‹ฌ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ ์˜ˆ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Bacalhau๊ฐ€ FEVM ์Šค๋งˆํŠธ ๊ณ„์•ฝ์—์„œ Bacalhau๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•œ ํ†ตํ•ฉ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ๋ฐ”์œ ๋™์•ˆ Bacalhau x FVM ํ˜‘์—…์— ๋Œ€ํ•œ ๋ช‡ ๊ฐ€์ง€ ์ƒ๊ฐ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.


  • ํ–ฅํ›„ Filecoin ๋ฐ์ดํ„ฐ์˜ ์˜จ๋ณด๋”ฉ ๋ฐ ์˜คํ”„๋ณด๋”ฉ์„ ๋„์™€์ฃผ์„ธ์š”.
  • ๊ฑฐ๋ž˜ ๋ฐ ์Šคํ† ๋ฆฌ์ง€ ์ œ๊ณต์—…์ฒด์— ๋Œ€ํ•ด ์˜จ์ฒด์ธ์—์„œ ๊ฒ€์ƒ‰๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ Filecoin์— ๋Œ€ํ•œ ํ‰ํŒ๊ณผ ์„œ๋น„์Šค ํ’ˆ์งˆ ๊ณ„์ธต์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค๋‹ˆ๋‹ค.
  • Bacalhau๋Š” ์‹œ์žฅ ๋ฐ ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๊ณ„์‚ฐ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Bacalhau๋Š” DAO ๋ฐ DataDAO์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์— ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Bacalhau๋Š” ๋น„๋””์˜ค ๋ฐ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์™€ ๊ฐ™์€ ์ฐฝ์˜์ ์ธ ์ž‘์—…์„ ์œ„ํ•œ ์ž๋™ํ™”๋ฅผ ๊ฐ•ํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Bacalhau๋Š” VR ๋ฐ AR์„ ํฌํ•จํ•œ ๊ฒŒ์ž„ ๋ฐ ๋ฉ”ํƒ€๋ฒ„์Šค ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Bacalhau, IOT ๋ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค
  • AI ๋ฐ ML ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

๐Ÿ  ๋ฐ”์นผ๋žด์šฐ ๋กœ๋“œ๋งต


์šฐ๋ฆฌ๋Š” ํ˜„์žฌ ์Šค๋งˆํŠธ ๊ณ„์•ฝ์—์„œ ์ง์ ‘ Bacalhau๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ๊ตฌ์ถ•ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค!!!! ์ด ํ”„๋กœ์ ํŠธ๋Š” Project Frog / Project Lilypad๋ผ๊ณ  ๋ถˆ๋ฆฌ๋ฉฐ FEVM ์Šค๋งˆํŠธ ๊ณ„์•ฝ์—์„œ Bacalhau ์ž‘์—…์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํ†ตํ•ฉ ๊ณ„์ธต์ด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


๋‰ด์Šค๋ ˆํ„ฐ์— ๊ฐ€์ž…ํ•˜๊ฑฐ๋‚˜ ์•„๋ž˜ ์†Œ์…œ ๋ฏธ๋””์–ด์— ๊ฐ€์ž…ํ•˜์—ฌ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๊ณ„์† ์ง€์ผœ๋ณด์„ธ์š”.

โœ๏ธ ๊ณ„์† ์—ฐ๋ฝํ•˜์„ธ์š”!

๋๊นŒ์ง€ ์ฝ์œผ์…จ๋‹ค๋ฉด ์ถ•ํ•˜๋“œ๋ฆฝ๋‹ˆ๋‹ค!!!


์ด๊ฒƒ์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด ์ข‹์•„์š”, ๋Œ“๊ธ€, ํŒ”๋กœ์šฐ ๋˜๋Š” ๊ณต์œ ํ•ด ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค! <3


๋ฐ”์นผ๋ผ์šฐ์™€ ๊ณ„์† ์—ฐ๋ฝํ•˜์„ธ์š”!



โ™ฅ๏ธ DeveloperAlly ์™€ ํ•จ๊ป˜

์ด ๊ธฐ์‚ฌ๊ฐ€ ๊ฐ€์น˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์‹œ๋‚˜์š”?

ํ›„์›์ž๊ฐ€ ๋˜์–ด Alison Haire๋ฅผ ์ง€์›ํ•˜์„ธ์š”. ์–ด๋–ค ๊ธˆ์•ก์ด๋“  ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!


ํ•ด์‹œ๋…ธ๋“œ ํ›„์›์ž์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณด์„ธ์š”.


์—ฌ๊ธฐ์—๋„ ๊ฒŒ์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.