目次
イーサリアムDAPPを構築するための前提条件は何ですか?
DAPPSにおけるスマートコントラクトの役割は何ですか?
私のdappをテストする方法は?
DAOとは何ですか?また、DAPPとどう関係していますか?
私のdappのセキュリティを確保する方法は?
メタマスクとは何ですか?また、なぜDAPP開発において重要なのですか?
私のdappを展開する方法は?
DAPP開発の課題は何ですか?
展開後にDAPPを更新するにはどうすればよいですか?
ホームページ テクノロジー周辺機器 IT業界 ビルディングイーサリアムダップ:ストーリーダオのホワイトリストとテスト

ビルディングイーサリアムダップ:ストーリーダオのホワイトリストとテスト

Feb 16, 2025 pm 12:24 PM

Building Ethereum DApps: Whitelisting & Testing a Story DAO

キーポイント

  • Story Daoは、OpenzePpelinの所有可能な契約を使用して、所有者のみが管理機能を実行できるようにし、DAPP運用のセキュリティと制御を強化します。
  • Story DAO契約には、調整可能な料金と期間パラメーターがあり、不正な変更を防ぐためのセキュリティ対策が装備されており、所有者のみが重要な設定を変更できるようにします。
  • ストーリーのホワイトリスト管理DAOは、送信者の貢献に基づいて自動および条件付きアクセスを可能にする有料機能を通じて実装されています。
  • SolitidityやJavaScriptテストを含む
  • 統合されたテスト戦略は、ストーリーDAOの機能とセキュリティを検証し、展開前に堅牢な操作を確保するために重要です。
  • ストーリーDAOの展開プロセスは、特定の移行スクリプトと構成によりトリュフによって簡素化され、開発環境から生産環境へのスムーズな移行が容易になります。
  • このチュートリアルシリーズの第3部では、イーサリアムを使用してDAPPの構築について説明しています。ここでは、トークンをEthereum Test Network Rinkebyに展開します。このセクションでは、Story Daoコードの作成を開始します。

入門記事にリストされている基準を使用して、導きます。

契約の概要

新しい契約を作成しましょう。Storydao.sol。そのフレームワークは次のとおりです。

SafeMathをインポートして安全な計算を再度行いますが、今回はZeppelinの所有可能な契約も使用します。これにより、誰かがストーリーを「所有」し、特定の管理者のみの機能を実行できます。簡単に言えば、StoryDaoが所有可能になるだけで十分です。

また、この契約では唯一の所有者修飾子を使用しています。関数修飾子は、基本的に機能の拡張機能とプラグインです。唯一の所有者モディファイアは次のようになります:
pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract StoryDao is Ownable {
    using SafeMath for uint256;

    mapping(address => bool) whitelist;
    uint256 public whitelistedNumber = 0;
    mapping(address => bool) blacklist;
    event Whitelisted(address addr, bool status);
    event Blacklisted(address addr, bool status);

    uint256 public daofee = 100; // 百分之几,即 100 为 1%
    uint256 public whitelistfee = 10000000000000000; // 以 Wei 为单位,这是 0.01 以太币

    event SubmissionCommissionChanged(uint256 newFee);
    event WhitelistFeeChanged(uint256 newFee);

    uint256 public durationDays = 21; // 故事章节持续时间(天)
    uint256 public durationSubmissions = 1000; // 故事章节持续时间(条目)

    function changedaofee(uint256 _fee) onlyOwner external {
        require(_fee <= 1000); // 限制最大费用为 10%
        daofee = _fee;
        emit SubmissionCommissionChanged(_fee);
    }

    function changewhitelistfee(uint256 _fee) onlyOwner external {
        require(_fee > 0); // 确保费用大于 0
        whitelistfee = _fee;
        emit WhitelistFeeChanged(_fee);
    }


    function changeDurationDays(uint256 _days) onlyOwner external {
        require(_days >= 1);
        durationDays = _days;
    }

    function changeDurationSubmissions(uint256 _subs) onlyOwner external {
        require(_subs > 99);
        durationSubmissions = _subs;
    }
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

関数に所有者のみが追加されると、関数の本体が_に貼り付けられ、前の部分が最初に実行されます。したがって、この修飾子を使用することにより、関数はメッセージ送信者が契約の所有者であるかどうかを自動的にチェックし、それが真である場合は通常どおり継続します。そうでない場合、それはクラッシュします。

ストーリーDAOの料金やその他のパラメーターを変更する関数に唯一の所有者修飾子を使用することにより、管理者のみがこれらの変更を行うことができるようにします。
modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

テスト

初期関数をテストしましょう。

存在しない場合は、フォルダーテストを作成します。次に、ファイルTestStoryDao.solとtestStorydao.jsを作成します。トリュフには例外をテストするためのネイティブ方法がないため、ヘルパー/expectThrow.jsも以下を使用して作成されます。

注:堅牢性テストは通常​​、低レベルの契約ベースの機能、つまりスマートコントラクトの内部構造をテストするために使用されます。 JSテストは、契約が外部から正しく相互作用できるかどうかをテストするためによく使用されます。これは、エンドユーザーが行うことです。

export default async promise => {
    try {
      await promise;
    } catch (error) {
      const invalidOpcode = error.message.search('invalid opcode') >= 0;
      const outOfGas = error.message.search('out of gas') >= 0;
      const revert = error.message.search('revert') >= 0;
      assert(
        invalidOpcode || outOfGas || revert,
        'Expected throw, got \'' + error + '\' instead',
      );
      return;
    }
    assert.fail('Expected throw not received');
  };
ログイン後にコピー
ログイン後にコピー
testStorydao.solで、次のコンテンツを置きます:

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract StoryDao is Ownable {
    using SafeMath for uint256;

    mapping(address => bool) whitelist;
    uint256 public whitelistedNumber = 0;
    mapping(address => bool) blacklist;
    event Whitelisted(address addr, bool status);
    event Blacklisted(address addr, bool status);

    uint256 public daofee = 100; // 百分之几,即 100 为 1%
    uint256 public whitelistfee = 10000000000000000; // 以 Wei 为单位,这是 0.01 以太币

    event SubmissionCommissionChanged(uint256 newFee);
    event WhitelistFeeChanged(uint256 newFee);

    uint256 public durationDays = 21; // 故事章节持续时间(天)
    uint256 public durationSubmissions = 1000; // 故事章节持续时间(条目)

    function changedaofee(uint256 _fee) onlyOwner external {
        require(_fee <= 1000); // 限制最大费用为 10%
        daofee = _fee;
        emit SubmissionCommissionChanged(_fee);
    }

    function changewhitelistfee(uint256 _fee) onlyOwner external {
        require(_fee > 0); // 确保费用大于 0
        whitelistfee = _fee;
        emit WhitelistFeeChanged(_fee);
    }


    function changeDurationDays(uint256 _days) onlyOwner external {
        require(_days >= 1);
        durationDays = _days;
    }

    function changeDurationSubmissions(uint256 _subs) onlyOwner external {
        require(_subs > 99);
        durationSubmissions = _subs;
    }
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

これにより、StoryDao契約が正しい料金と期間の数値で適切に展開されていることが確認されます。最初の行では、展開されたアドレスリストから読み取ることで展開されることが保証され、最後のセクションでは>アサーション - 宣言が真か偽かを確認します。この例では、展開された契約の初期値と数値を比較します。それが「真」であるときはいつでも、Assert.equalsセクションは「真の」と述べたイベントを発行します。これは、トリュフがテスト中に聞いていることです。

testStorydao.jsでは、次のコンテンツを置きます

テストが正常に実行されるためには、ストーリーダオを展開したいことをトリュフに伝える必要があります。それでは、以前に書いた移行とほぼ同じコンテンツを使用して、3_Deploy_StoryDao.jsを移行中に作成しましょう。
modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
この時点で、これまでに必要な依存関係と将来必要なものを含むプロジェクトフォルダーのルートにあるパッケージファイルを更新(またはそうでない場合は作成)する必要があります。 >

および.babelrcファイルを含むファイル:
export default async promise => {
    try {
      await promise;
    } catch (error) {
      const invalidOpcode = error.message.search('invalid opcode') >= 0;
      const outOfGas = error.message.search('out of gas') >= 0;
      const revert = error.message.search('revert') >= 0;
      assert(
        invalidOpcode || outOfGas || revert,
        'Expected throw, got \'' + error + '\' instead',
      );
      return;
    }
    assert.fail('Expected throw not received');
  };
ログイン後にコピー
ログイン後にコピー

また、トリュフの構成にバベルが要求される必要があるため、テストをコンパイルするときに使用する必要があることがわかります。

pragma solidity ^0.4.24;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/StoryDao.sol";

contract TestStoryDao {
    function testDeploymentIsFine() public {
        StoryDao sd = StoryDao(DeployedAddresses.StoryDao());

        uint256 daofee = 100; // 百分之几,即 100 为 1%
        uint256 whitelistfee = 10000000000000000; // 以 Wei 为单位,这是 0.01 以太币

        uint256 durationDays = 21; // 故事章节持续时间(天)
        uint256 durationSubmissions = 1000; // 故事章节持续时间(条目)

        Assert.equal(sd.daofee(), daofee, "初始 DAO 费用应为 100");
        Assert.equal(sd.whitelistfee(), whitelistfee, "初始白名单费用应为 0.01 以太币");
        Assert.equal(sd.durationDays(), durationDays, "初始天数持续时间应设置为 3 周");
        Assert.equal(sd.durationSubmissions(), durationSubmissions, "初始提交持续时间应设置为 1000 个条目");
    }
}
ログイン後にコピー

注:BabelはNodejsのアドオンであり、現在の世代のNodejsで次世代JavaScriptを使用できるようにするため、インポートやその他のコンテンツを書くことができます。これを理解していない場合は、それを無視して逐語的に貼り付けてください。インストール後、この問題に二度と対処する必要がない場合があります。

import expectThrow from './helpers/expectThrow';

const StoryDao = artifacts.require("StoryDao");

contract('StoryDao Test', async (accounts) => {
    it("should make sure environment is OK by checking that the first 3 accounts have over 20 eth", async () =>{
        assert.equal(web3.eth.getBalance(accounts[0]).toNumber() > 2e+19, true, "Account 0 has more than 20 eth");
        assert.equal(web3.eth.getBalance(accounts[1]).toNumber() > 2e+19, true, "Account 1 has more than 20 eth");
        assert.equal(web3.eth.getBalance(accounts[2]).toNumber() > 2e+19, true, "Account 2 has more than 20 eth");
    });

    it("should make the deployer the owner", async () => {
        let instance = await StoryDao.deployed();
        assert.equal(await instance.owner(), accounts[0]);
    });

    it("should let owner change fee and duration", async () => {
        let instance = await StoryDao.deployed();

        let newDaoFee = 50;
        let newWhitelistFee = 1e+10; // 1 ether
        let newDayDuration = 42;
        let newSubsDuration = 1500;

        instance.changedaofee(newDaoFee, {from: accounts[0]});
        instance.changewhitelistfee(newWhitelistFee, {from: accounts[0]});
        instance.changeDurationDays(newDayDuration, {from: accounts[0]});
        instance.changeDurationSubmissions(newSubsDuration, {from: accounts[0]});

        assert.equal(await instance.daofee(), newDaoFee);
        assert.equal(await instance.whitelistfee(), newWhitelistFee);
        assert.equal(await instance.durationDays(), newDayDuration);
        assert.equal(await instance.durationSubmissions(), newSubsDuration);
    });

    it("should forbid non-owners from changing fee and duration", async () => {
        let instance = await StoryDao.deployed();

        let newDaoFee = 50;
        let newWhitelistFee = 1e+10; // 1 ether
        let newDayDuration = 42;
        let newSubsDuration = 1500;

        await expectThrow(instance.changedaofee(newDaoFee, {from: accounts[1]}));
        await expectThrow(instance.changewhitelistfee(newWhitelistFee, {from: accounts[1]}));
        await expectThrow(instance.changeDurationDays(newDayDuration, {from: accounts[1]}));
        await expectThrow(instance.changeDurationSubmissions(newSubsDuration, {from: accounts[1]}));
    });

    it("should make sure the owner can only change fees and duration to valid values", async () =>{
        let instance = await StoryDao.deployed();

        let invalidDaoFee = 20000;
        let invalidDayDuration = 0;
        let invalidSubsDuration = 98;

        await expectThrow(instance.changedaofee(invalidDaoFee, {from: accounts[0]}));
        await expectThrow(instance.changeDurationDays(invalidDayDuration, {from: accounts[0]}));
        await expectThrow(instance.changeDurationSubmissions(invalidSubsDuration, {from: accounts[0]}));
    })
});
ログイン後にコピー

さあ、最終的にトリュフテストを実行します。出力はこれに似ている必要があります:

var Migrations = artifacts.require("./Migrations.sol");
var StoryDao = artifacts.require("./StoryDao.sol");

module.exports = function(deployer, network, accounts) {
  if (network == "development") {
    deployer.deploy(StoryDao, {from: accounts[0]});
  } else {
    deployer.deploy(StoryDao);
  }
};
ログイン後にコピー
テストの詳細については、このチュートリアルを参照してください。これは、スマートコントラクトのテストをカバーするために特別に準備しました。

このコースの次のセクションでは、逐語的に入力するとチュートリアルが長すぎるため、テストをスキップしますが、プロジェクトの最終ソースコードを参照してすべてのテストを確認してください。完了したばかりのプロセスには、テスト用の環境が設定されているため、さらにセットアップせずにテストを作成できます。

Building Ethereum DApps: Whitelisting & Testing a Story DAO ホワイトリスト

ユーザーがストーリーの構築に参加できるようにするホワイトリストメカニズムを構築しましょう。以下の関数フレームワークをStoryDao.SOLに追加します:

無名の関数関数()はフォールバック関数と呼ばれます。これは、この契約にファンドが送信されたが特定の指示はありません(つまり、他の関数は特異的に呼ばれない)と呼ばれます。これにより、人々はエーテルをDAOのみに送り、ホワイトリストに登録されているかどうかに基づいて、すぐにホワイトリストまたはトークンを購入することでStoryDaoに参加できます。 ホワイトリストの関数はホワイトリストに使用され、直接呼び出すことができますが、送信者がまだホワイトリストに登録されていない場合、フォールバック関数がエーテルを受け取った後に自動的に呼び出すことを確認します。 WhitelistAddress関数は、他の契約からも呼び出されるべきであるため公開されますが、ファンドは外部住所からこのアドレスに送信されるため、フォールバック関数は外部です。この契約を呼び出す契約は、必要な関数を直接簡単に呼び出すことができます。

最初にフォールバック関数に対処しましょう。

pragma solidity ^0.4.24;

import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract StoryDao is Ownable {
    using SafeMath for uint256;

    mapping(address => bool) whitelist;
    uint256 public whitelistedNumber = 0;
    mapping(address => bool) blacklist;
    event Whitelisted(address addr, bool status);
    event Blacklisted(address addr, bool status);

    uint256 public daofee = 100; // 百分之几,即 100 为 1%
    uint256 public whitelistfee = 10000000000000000; // 以 Wei 为单位,这是 0.01 以太币

    event SubmissionCommissionChanged(uint256 newFee);
    event WhitelistFeeChanged(uint256 newFee);

    uint256 public durationDays = 21; // 故事章节持续时间(天)
    uint256 public durationSubmissions = 1000; // 故事章节持续时间(条目)

    function changedaofee(uint256 _fee) onlyOwner external {
        require(_fee <= 1000); // 限制最大费用为 10%
        daofee = _fee;
        emit SubmissionCommissionChanged(_fee);
    }

    function changewhitelistfee(uint256 _fee) onlyOwner external {
        require(_fee > 0); // 确保费用大于 0
        whitelistfee = _fee;
        emit WhitelistFeeChanged(_fee);
    }


    function changeDurationDays(uint256 _days) onlyOwner external {
        require(_days >= 1);
        durationDays = _days;
    }

    function changeDurationSubmissions(uint256 _subs) onlyOwner external {
        require(_subs > 99);
        durationSubmissions = _subs;
    }
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
送信者がまだホワイトリストに登録されていないかどうかを確認し、ホワイトリスタドレス機能への呼び出しを委任します。まだ持っていないので、BuyTokens関数にコメントしたことに注意してください。

次に、ホワイトリストに対処しましょう。

modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
この関数は、(トランザクションから)メッセージから抽出するのではなく、引数としてアドレスとして受け入れることに注意してください。これには、誰かがDAOに参加する余裕がない場合、他の人をホワイトリストに登録できるという利点があります。

正気チェック機能から始めます。送信者は、ホワイトリストまたはブラックリスト(禁止)が必要であり、支払うのに十分な料金を送信する必要があります。これらの条件が満足のいくものである場合、住所がホワイトリストに追加され、ホワイトリストに登録されたイベントが発行され、最後に、送信されるエーテルの数がホワイトリスト料金の支払いに必要なエーテルの数よりも大きい場合、残りは購入に使用されますトークン。

注:これは安全な計算のためのセーフマス関数であるため、subの代わりに - subを使用します。

ユーザーが0.01エーテル以上のエーテルをStoryDao契約に送信する限り、自分自身または他の人がホワイトリストに登録できるようになりました。

結論 このチュートリアルでは、DAOの最初の部分を構築しましたが、まだやるべきことがたくさんあります。ご期待ください:次のセクションでは、ストーリーにコンテンツを追加することを扱います!

イーサリアムダップとホワイトリストの構築に関するFAQ

イーサリアムDAPPを構築するための前提条件は何ですか?

ホワイトリストのプロセスはDAPPでどのように機能しますか?

ホワイトリストは、アプリケーションの特定の機能または領域へのアクセスを制限するDAPPのセキュリティ対策です。 DAPPとの相互作用を可能にする承認されたアドレスのリストを作成することが含まれます。これらのアドレスから開始されたトランザクションのみが受け入れられますが、他の住所は拒否されます。これは、不正アクセスと悪意のあるアクティビティを防ぐのに役立ちます。

DAPPSにおけるスマートコントラクトの役割は何ですか?

スマート契約は自己執行契約であり、その契約条件はコードに直接記述されます。彼らは、ブロックチェーン上のビジネスロジックの実行を自動化するため、Dappsで重要な役割を果たします。展開されたら、変更したり改ざんしたりすることができないため、透明性、セキュリティ、および不変性を確保します。

私のdappをテストする方法は?

テストは、その機能とセキュリティを確保するためのDAPP開発の重要な部分です。トリュフやガナッシュなどのツールを使用してテストできます。トリュフは、イーサリアムに開発環境、テストフレームワーク、アセットパイプラインを提供しますが、Ganacheを使用すると、テスト用のプライベートイーサリアムブロックチェーンを作成できます。

DAOとは何ですか?また、DAPPとどう関係していますか?

daoは分散型の自律組織を表します。これは、透明性があり、組織メンバーによって管理され、中央政府の影響を受けないコンピュータープログラムとしてエンコードされたルールで表される組織タイプです。 DAOの金融取引とルールはブロックチェーンに保持されているため、DAPPになります。

私のdappのセキュリティを確保する方法は?

DAPPのセキュリティにはさまざまなプラクティスが含まれることを確認します。これには、安全なスマートコントラクトの作成、徹底的なテスト、セキュリティ監査の実行、ソフトウェアと依存関係を最新の状態に保つことが含まれます。また、安全なコーディングのためにベストプラクティスに従い、ブロックチェーンスペースの最新のセキュリティの脆弱性と脅威に遅れないことも重要です。

メタマスクとは何ですか?また、なぜDAPP開発において重要なのですか?

Metamaskは、ブラウザから直接Ethereumブロックチェーンと対話できるブラウザ拡張機能です。また、イーサリアムとERC-20トークンを管理するためのイーサリアムウォレットとしても機能します。ユーザーが完全なイーサリアムノードを実行せずにDAPPと対話できるようにユーザーフレンドリーなインターフェイスをユーザーに提供するため、DAPP開発において重要です。

私のdappを展開する方法は?

DAPPを開発およびテストしたら、Ethereum MainNetまたはTestNetに展開できます。これには、スマートコントラクトをコンパイルし、ブロックチェーンに展開し、DAPPをそれらの契約に接続することが含まれます。トリュフやインフラなどのツールを使用して、このプロセスを完了できます。

DAPP開発の課題は何ですか?

DAPP開発はいくつかの課題に直面しています。これには、Ethereum Networkのスケーラビリティの問題の処理、DAPPのセキュリティの確保、取引の揮発性ガス価格の管理、ユーザーフレンドリーなインターフェイスの提供が含まれます。また、急速に進化するブロックチェーンテクノロジーと規制に注意する必要があります。

展開後にDAPPを更新するにはどうすればよいですか?

ブロックチェーン上のスマートコントラクトが不変であるため、展開後のDAPPSの更新は困難な場合があります。ただし、データとロジックを異なる契約に分離したり、委任されたコールを使用して契約をアップグレードすることにより、アップグレード可能な契約を設計できます。 DAPPの設計段階でのアップグレードと変更の計画は非常に重要です。

以上がビルディングイーサリアムダップ:ストーリーダオのホワイトリストとテストの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

CNCF ARM64パイロット:インパクトと洞察 CNCF ARM64パイロット:インパクトと洞察 Apr 15, 2025 am 08:27 AM

このパイロットプログラム、CNCF(クラウドネイティブコンピューティングファンデーション)、アンペアコンピューティング、Equinix Metal、およびActuatedのコラボレーションであるCNCF GithubプロジェクトのARM64 CI/CDが合理化されます。 このイニシアチブは、セキュリティの懸念とパフォーマンスリムに対処します

AWS ECSとLambdaを備えたサーバーレス画像処理パイプライン AWS ECSとLambdaを備えたサーバーレス画像処理パイプライン Apr 18, 2025 am 08:28 AM

このチュートリアルは、AWSサービスを使用してサーバーレスイメージ処理パイプラインを構築することをガイドします。 APIゲートウェイ、Lambda関数、S3バケット、およびDynamoDBと対話するECS Fargateクラスターに展開されたnext.jsフロントエンドを作成します。 th

2025年に購読する上位21の開発者ニュースレター 2025年に購読する上位21の開発者ニュースレター Apr 24, 2025 am 08:28 AM

これらのトップ開発者ニュースレターを使用して、最新のハイテクトレンドについてお知らせください! このキュレーションされたリストは、AI愛好家からベテランのバックエンドやフロントエンド開発者まで、すべての人に何かを提供します。 お気に入りを選択し、Relを検索する時間を節約してください

See all articles