47. 아이펀 디플로이 연동 API 구현하기

이 문서에서는 아이펀 엔진으로 작성한 서버에서 아이펀 디플로이 연동 API를 구현하는데 필요한 작업을 설명합니다.

아이펀 엔진에서 아이펀 디플로이의 연동 API를 구현하는 부분은 DeployApiService 클래스입니다. 개별 API 를 구현할 때 해당 클래스의 기능을 이용하시기 바랍니다.

MANIFEST.json 에는 다음 항목이 존재 해야합니다. C++ 을 사용하시는 경우 deploy_api_service_use_mono 값이 false여야 하며, C#의 경우 true로 설정되어야 합니다.

1
2
3
"DeployApiService": {
  "deploy_api_service_use_mono": false
}

Note

디플로이 연동 API에 대하여 보다 자세한 사항은 아이펀 디플로이 문서 에서 확인하실 수 있습니다.

47.1. 미리 알아두어야 할 사항

아래의 내용을 읽어보시고, 사용하시려는 API 관련 부분을 따라하시기 바랍니다.

47.1.1. 비동기 HTTP 응답 보내기

비동기로 응답을 보내기 위해서 HTTP 응답을 지정하는 ResponseWriter 객체를 API 핸들러에 전달하게 됩니다. 이 객체는 HTTP 상태 코드 (200 OK, 400 Bad request, ... ) 값과 핸들러에 따라서 필요한 응답 값을 전달받아 응답으로 보냅니다.

예를 들어, 유저가 로그인한 상태인지 검사하는 C++ 의 IsLoggedIn() 함수와 C#의 IsUserLoggedIn() 함수는 HTTP 상태 코드와, 로그인 여부에 해당하는 bool 값을 받습니다.

struct ExampleApiHandler : public DeployApiService::DeployApiHandlerBase {
  virtual void IsLoggedIn(
      const string &id, const Ptr<BoolResponseWriter> &writer) const override {
    AccountManager::LocateAsync(id,
        [writer](const string &id, const Rpc::PeerId &node) {
          bool logged_in = !node.is_nil();  // 로그인 상태면 node 값이 nil이 아님

          // HTTP 응답을 보낸다. (200 OK, 로그인 상태에 대한 bool값)
          writer->Write(http::kOk, &logged_in);
        });
  }

};
internal class ExampleDeployHandler : UserAPI {
  // UserAPI 의 일부인 IsUserLoggedIn 함수를 구현
  public void IsUserLoggedIn(string id, BoolResponseWriter writer)
  {
    // 비동기로 로그인을 확인한 후, 응답을 보내는 예제
    AccountManager.LocateCallback cb = delegate (string uid, System.Guid peer_id) {
      // peer_id 가 00000000-... 가 아니면 로그인한 상태
      writer(StatusCode.kOk, peer_id != Guid.Empty);
    };
    AccountManager.LocateAsync(id, cb);
  }
}

이외에도 HTTP 상태 코드만 필요한 경우에 쓰는 VoidResponseWriter, JSON 응답, JSON의 벡터 (vector), string, string의 벡터 등을 응답으로 받는 각 HTTP 응답 객체들이 각 핸들러에 맞게 전달됩니다.

47.1.2. DeployApiHandlerBase 이용하기 (C++)

대부분의 디플로이 API는 DeployApiService::DeployApiHandlerBase 를 상속받은 클래스를 정의하고, 추가하려는 API에 해당하는 가상 함수를 override하는 방식으로 구현합니다. 그리고 구현한 클래스의 객체를 DeployApiService::RegisterDeployApiHandler() 함수로 전달하여 DeployApiService 에 등록합니다.

등록 후 override된 API 메서드들은 아이펀 디플로이가 API를 호출할 수 있도록 노출되므로, 이를 원하지 않으신다면 해당 함수를 구현하지 않은 상태로 유지해주세요.

class ExampleApiHandler : public DeployApiService::DeployApiHandlerBase {
  public:
    virtual void IsLoggedIn(
        const string &id, const Ptr<BoolResponseWriter> &writer) const override {
        // API 핸들러 구현
    }
};

boost::shared_ptr<ExampleApiHandler> handler;
handler.reset(new ExampleApiHandler());
DeployApiService::RegisterDeployApiHandler(handler);

올바른 API 함수를 override 했는지 명확히 하기 위해, 위 예제와 같이 override 키워드를 이용하시면 편리합니다. (이 경우, C++11 혹은 그 이후 버전을 지원하는 컴파일러 설정이 필요합니다)

Warning

가상 함수를 protected나 private으로 구현하면 핸들러가 등록되지 않습니다. public을 사용해주세요.

Note

DeployApiHandlerBase 는 C++ 의 리플렉션 기능을 이용하고 있어서 상위 클래스에 정의한 멤버 함수는 인식하지 못합니다. DeployApiHandlerBase 를 상속받는 계층은 한 단계로 한정해주시기 바랍니다.

47.1.3. DeployService 의 인터페이스 이용하기 (C#)

C#의 경우, funapi.Deploy namespace에 정의된 API 인터페이스들을 구현해주시면 됩니다. API 인터페이스는 API 용도에 따라 UserAPI, CharacterAPI 등으로 나누어져 있습니다.

이 중에서 추가하고자 하는 API 인터페이스를 상속받은 클래스를 정의하고 메서드를 구현한 후, DeployApiService.RegisterDeployApiHandler() 함수에 클래스 객체를 전달하면 아이펀 디플로이에서 해당 API를 호출할 수 있게 됩니다.

internal class ExampleDeployHandler : UserAPI {
  public void IsUserLoggedIn(string id, BoolResponseWriter writer)
  {
    // API 핸들러 구현
  }
}

ExampleDeployHandler handler = new ExampleDeployHandler();
DeployApiService.RegisterDeployApiHandler(handler);

47.2. 계정 관련 API 구현

유저 검색 조건을 반환하는 API인 GET /cs-api/v1/account/search-condition/ 를 구현하는 경우를 예로 들어보겠습니다. DeployApiService::DeployApiHandlerBase::GetUserSearchConditions 를 덮어써서 (override) 구현합니다. C# 의 경우, interface UserAPIGetUserSearchConditions 를 구현하시면 됩니다.

예를 들어, 유저 이름 (nickname)과 Facebook 아이디를 이용한 검색을 허용한다면 아래와 같이 구현합니다. 즉, 가능한 조건을 문자열의 벡터 형식으로 지정합니다.

class FooHandler : public DeployApiService::DeployApiHandlerBase {
  virtual http::StatusCode GetUserSearchConditions(
      const Ptr<StringVectorResponseWriter> &writer) const override {
    std::vector<std::string> conditions;
    conditions.push_back("nickname");
    conditions.push_back("facebook-id");
    writer->Write(fun::http::kOk, &conditions);
  }
internal class ExampleDeployHandler : UserAPI {
  public void GetUserSearchConditions(WriteStringListResponse writer)
  {
    List<String> conditions = new List<String>();
    conditions.Add("nickname");
    conditions.Add("facebook-id");
    writer(StatusCode.kOk, conditions);
  }
}

비슷하게 유저를 검색하는 GET /cs-api/v1/account/searchDeployApiHandlerBase::SearchUsers 를 덮어써서 (override) 구현합니다. 조건에 맞는 유저를 검색한 후, 각각의 유저를 하나의 dict 삼아 users 라는 이름의 JSON array 에 추가합니다. 이 API의 경우 페이징을 지원하기 때문에, 함수 인자로 넘어온 PageInfo 구조체의 값에 따라 범위를 제한해야 합니다. 만약 HTTP status code 가 OK 가 아니라면 (fun::http::kOk), 처리 과정에 오류가 발생했다고 가정하고, 해당 값을 아이펀 디플로이 쪽에 전달합니다.

C#의 경우에는 funapi.Deploy.UserAPI.SearchUsers() 를 구현하시면 됩니다.

API들은 DeployApiHandlerBase 의 다음 멤버 함수에 각각 대응합니다. 혹은 C# Deploy.UserAPI 혹은 기타 인터페이스의 메서드에 대응합니다.

GET /cs-api/v1/search-conditions/

GetUserSearchConditions() 검색 조건 이름들을 문자열의 벡터로 넣어 비동기 응답 함수 (Writer 함수)에 전달하시면 됩니다.

GET /cs-api/v1/account/search/

SearchUsers(condition-name, condition-value) 검색 조건에 해당하는 유저 데이터를 JSON 형식으로 비동기 응답 함수에 전달하시면 됩니다. JSON 데이터는 아이펀 디플로이 API가 요구하는 형식을 그대로 사용합니다.

이 함수는 PageInfo 구조체의 내용을 확인하고, 페이징을 지원해야 합니다. C#의 경우 funapi.Deploy.PageInfo 구조체를 참조해주세요.

GET /cs-api/v1/account/(id)/

GetUser(id) id 에 해당하는 유저 데이터를 result 에 설정하는 구현이 필요합니다.

PUT /cs-api/v1/account/(id)/(field)/(value)/

UpdateUser(id, field, value) id 에 해당하는 유저를 찾아서 field 의 값을 value 로 변경하는 구현이 필요합니다. 만약 field 가 변경 가능한 값으로 지정되지 않았다면 이 함수는 호출되지 않습니다. 성공/실패 여부는 HTTP status code로 응답함수에 전달합니다.

GET /cs-api/v1/account/(id)/connection/

IsLoggedIn(id) id 에 해당하는 유저가 로그인 상태인지 여부를 전달하시면 됩니다. C# 쪽은 IsUserLoggedIn(id) 에 해당합니다.

POST /cs-api/v1/account/(id)/logout/

ForceLogout(id) id 에 해당하는 유저를 강제로 로그아웃하는 구현이 필요합니다. 응답은 HTTP 상태 코드만 필요로 합니다.

C#의 경우 Deploy.ForceLogoutAPI.ForceLogout 을 구현하시면 됩니다.

GET /cs-api/v1/account/(id)/ban/

GetUserBanned(id) id 에 해당하는 유저가 밴 당한 상태인지를 응답 함수에 전달하는 구현이 필요합니다.

C#의 경우 Deploy.BanUserAPI.IsUserBanned() 에 해당합니다.

POST /cs-api/v1/account/(id)/ban/

BanUser(id) id 에 해당하는 유저를 밴하는 구현이 필요합니다. 응답은 HTTP 상태 코드만 필요로 합니다.

C#의 경우 Deploy.BanUserAPI.BanUser() 에 해당합니다.

POST /cs-api/v1/account/(id)/reinstate/

UnbanUser(id) id 에 해당하는 유저를 밴 취소하는 구현이 필요합니다. 응답은 HTTP 상태 코드만 필요로 합니다.

C#의 경우 Deploy.BanUserAPI.UnbanUser() 에 해당합니다.

추가로, UpdateUser 에서 변경할 수 있는 항목은 DeployApiService::SetEditableFieldsForUser() 함수를 호출해서 지정합니다.

C# 의 경우 DeployApiService.SetEditableFieldsForUser() 함수를 호출해서 지정합니다.

47.3. 캐릭터 관련 API 구현

계정 관련 API처럼 아래의 인터페이스를 이용하시면 됩니다.

GET /cs-api/v1/account/(id)/character/

GetCharacters(id). id 에 해당하는 유저의 캐릭터 아이디/화면 표시 이름 정보를 각각 하나의 JSON에 저장하고, 이를 vector<fun::Json> 에 넣은 데이터를 응답 함수에 전달하는 구현이 필요합니다.

C#의 경우 UserAPI.GetCharacters() 를 구현해주시면 됩니다.

GET /cs-api/v1/character/(id)/

GetCharacter(id). id 에 해당하는 유저의 캐릭터 아이디 정보를 JSON object 형태로 응답하는 구현이 필요합니다. 예를 들어 아래와 같은 내용을 응답 함수에 넣어서 호출해주시면 됩니다.

C#의 경우 CharacterAPI.GetCharacter(id) 를 구현해주시면 됩니다.

{
  "nickname": "foo",
  "level": 12
}
PUT /cs-api/v1/character/(id)/(field)/(value)/

UpdateCharacter(id, field, value). id 에 해당하는 캐릭터의 field 의 값을 value 로 변경하는 구현이 필요합니다. 만약 field 가 변경 가능한 값으로 지정되지 않았다면 이 함수는 호출되지 않습니다. 성공/실패 여부는 HTTP status code로 알립니다.

C#의 경우 CharacterAPI.UpdateCharacter(id, field, value) 를 구현해주시면 됩니다.

UpdateCharacter 에서 변경하려는 필드를 DeployApiService::SetEditableFieldsForCharacter() 로 지정해주셔야 합니다.

C#의 경우 DeployApiService.SetEditableFieldsForCharacter() 로 지정해주시면 됩니다.

47.4. 인벤토리 관련 API 구현

GET /cs-api/v1/character/(id)/inventory/

GetCharacterInventoryInfo(id). id 에 해당하는 캐릭터의 인벤토리가 어떤 타입이 있는지를 응답 함수에 전달하는 구현이 필요합니다. 예를 들어 weapon 타입의 슬롯으로 left_hand, right_hand 가 있고, armor 타입의 슬롯으로 head, body 가 있을 경우, 다음과 같이 지정해주시면 됩니다.

using InventoryInfoResponseWriter = ResponseWriterT<
    DeployApiService::DeployApiHandlerBase::InventoryInfo>;

virtual http::StatusCode GetCharacterInventoryInfo(
    const std::string &charcter_id,
    const Ptr<InventoryInfoResponseWriter> &writer) const override {
  DeployApiService::DeployApiHandlerBase::InventoryInfo result;
  result.emplace_back(
      make_pair<string, vector<string>>("weapon", {"left_hand", "right_hand"}));
  result.emplace_back(
      make_pair<string, vector<string>>("armor", {"head", "body"}));
  writer->Write(http::kOk, &result);
}
// InventoryAPI.GetCharacterInventoryInfo
public void GetCharacterInventoryInfo(
    string char_id, InventoryInfoResponseWriter writer) {
  var inventory_list = new List<Tuple<string, List<string>>>();
  var weapon = new Tuple<string, List<String>>("weapon", new List<string>());
  weapon.Item2.Add("left_hand");
  weapon.Item2.Add("right_hand");
  inventory_list.Add(weapon);

  var armor = new Tuple<string, List<String>>("armor", new List<string>());
  armor.Item2.Add("head");
  armor.Item2.Add("body");
  inventory_list.Add(armor);

  writer(StatusCode.kOk, inventory_list);
}
GET /cs-api/v1/inventory/(type)/(id)/

GetInventory(type, id). 해당 인벤토리에 속한 아이템들을 JSON array 형식으로 result 에 설정하는 구현이 필요합니다. (디플로이에서 필요로 하는 형식으로 자동 변환합니다)

이 함수는 페이징 관련 인자를 전달하며, 구현할 때 페이징 처리를 해주셔야 합니다.

POST /cs-api/v1/inventory/<type>/<inv_id>/item/<item_id>/

C++ 쪽에만 있는 함수입니다. (C# 쪽은 이 이후의 함수로 통합되어 있습니다)

DeleteInventoryItem(type, id, item_id, expected_quantity, reclaimed)

인벤토리 타입이 type, 아이디가 id 인 곳에서 item_id 에 해당하는 아이템을 제거할 때 호출하는 함수입니다. 운영툴 UI 상에서 현재 아이템의 갯수가 expected_quantity 라고 생각하고 있으며, 이 중 reclaimed 개를 제거하도록 구현해야 합니다.

결과 값은 HTTP status code로 반환합니다.

POST /cs-api/v1/inventory/

C++ 의 DeleteMultipleInventoryItems(), C#의 InventoryAPI.DeleteInventoryItem 함수에 해당합니다. 운영툴에서 회수할 (인벤터리 타입, 아이디) 별로 목록을 전달하며, 이에 해당하는 제거를 구현해야 합니다.

결과 값은 HTTP status code로 반환합니다.

47.5. 아이템 지급 관련 API 구현

지급 가능한 아이템 아이디는 SetGiftableItems() 함수로 지정합니다. 해당 함수에는 “아이템 아이디, 이름” 쌍의 목록을 전달해주셔야 합니다.

다음 두 가지 API를 구현하면 디플로이 상에서 아이템 지급 처리를 할 수 있습니다. C#의 경우 GiftAPI 인터페이스의 함수에 해당합니다.

POST /cs-api/v1/item/gift/

GiveGift(target_type, title, content, expires, items, users, writer)

target_typeaccount 혹은 character 값이 오며, 이에 맞춰서 계정 혹은 개별 캐릭터에 아이템을 지급하시면 됩니다.

해아이템을 보내면서 메시지를 같이 전달하며 (제목: title, 내용: content) 해당 아이템의 만료 시간은 expires 로 전달합니다. (시간대는 UTC) items 에는 지급할 아이템 목록을 같이 전달하며, 아이템 아이디는 SetGiftableItems() 에서 지정한 것들만 허용됩니다.

users 에는 아이템을 지급 받을 계정 아이디 혹은 캐릭터 아이디가 들어있습니다.

GiveGiftToAll(target_type, title, content, expires, items, writer)

위 함수와는 달리 받을 계정/캐릭터 목록이 없으며 모든 계정 혹은 캐릭터에 지급하는 구현을 추가해주시면 됩니다.

만약 지정되지 않은 아이템을 지급하려고 하면 오류 로그를 남기며, 이 콜백함수는 호출되지 않습니다.

47.6. 캠페인 (이벤트) 관련 API 구현

우선 DeployApiService::RegisterCampaignType() 함수를 이용해서 디플로이에서 지정할 수 있는 캠페인을 설정합니다. 해당 함수에서 설정해야 하는 값은 DeployApiService::Campaign 구조체 타입을 참고해서 지정하시기 바랍니다.

캠페인 시작 시에 호출할 콜백은 RegisterBeginCampaignCallback() 에서, 캠페인 종료 시에 호출할 콜백은 RegisterEndCampaignCallback() 에서 지정하시면 됩니다. 이미 시작한 캠페인을 종료하는 경우를 처리하기 위해서 RegisterCancelCampaignCallback() 의 콜백도 지정할 수 있습니다.

47.6.1. 캠페인 타입 등록하기

캠페인 타입에는 이름과 복수의 보상 종류, 보상 타입을 지정하게 되어있습니다. 예를 들어 인게임 화폐와, 경험치 상승률을 지정하는 로그인 이벤트를 지정해보겠습니다.

fun::DeployApiService::Campaign login_event;
login_event.name.assign("로그인 이벤트");  // type 설정

// reward schema 지정
login_event.reward_schemas.emplace_back(
    fun::DeployApiService::CampaignRewardSchema {
    "gold", "화폐", fun::DeployApiService::CampaignRewardSchema::VT_INT});
login_event.reward_schemas.emplace_back(
    fun::DeployApiService::CampaignRewardSchema {
    "exp_plus", "경험치", fun::DeployApiService::CampaignRewardSchema::VT_INT});
// 실제 등록
DeployApiService::RegisterCampaignType("login_event", login_event);

47.6.2. 캠페인 정보 이용하기

캠페인 관련 콜백 함수들은 CampaignArgument 타입의 인자를 받습니다.

// Arguments passed from iFun Deploy to campaign.
// 아이펀 디플로이에서 캠페인 콜백에게 전달하는 인자.
struct CampaignArgument {
  std::string name;
  std::string description;

  // Arguments will be parsed as specified in CampaignRewardSchema.
  // 각 인자는 CampaignRewardSchema 에 지정한 타입으로 파싱합니다.
  //    VT_INT: int64_t
  //    VT_DOUBLE: double
  //    VT_STRING: string
  typedef boost::variant<int64_t, double, std::string> Value;

  // list of named pairs. (reward)
  // 보상에 해당하는 이름, 값 쌍 목록.
  std::vector<std::pair<std::string, Value> > values;

  fun::WallClock::Value begin_ts;
  fun::WallClock::Value end_ts;

  bool is_recurring;  // is recurring event (or not).  반복 이벤트인지 여부.

  // For recurring campaign (반복 이벤트)
  struct RecurringSchedule {
    // day of week for the campaign (0 = Sun, 1 = Mon, ...)
    // 이벤트에 해당하는 요일 (0 = 일, 1 = 월, ...)
    std::vector<boost::date_time::weekdays> days_of_the_week;

    int32_t begin_ts_of_day;  // 0 .. 86400 (in timezone)
    int32_t end_ts_of_day;  // 0 .. 86400 (in timezone)

    std::string timezone;  // timezone in tz database (eg. Asia/Seoul)
  } recurring_schedule;
};
public struct CampaignArgument {
        public string name;
        public string description;
        public List<Tuple<string, JValue>> values;
        public DateTime begin_ts;  // UTC
        public DateTime end_ts;  // UTC
}
  • name, description: 캠페인의 이름과 설명은 아이펀 디플로이에서 지정한 값을 전달합니다.

  • values: 보상에 해당하는 값들은 values 에 목록으로 전달합니다. 이 값은 캠페인을 등록할 때 사용한 타입에 맞춰서 해석해야 합니다.

  • begin_ts, end_ts: 시작 / 종료 시각. UTC 기준으로 해석한 시간이 들어있습니다.

  • is_recurring, recurring_schedule: 반복 캠페인인 경우 is_recurring 값이 true이며, 이때 recurring_schedule 을 접근할 수 있습니다. 여기에선 다음 항목을 확인할 수 있습니다:

    • 반복 캠페인을 실행하는 요일 정보 (일요일부터 0으로 표현)
    • 실행하는 시각 (하루를 0초부터 86400으로 표현). 해당 시각은 캠페인을 생성한 운영자의 시간대를 따릅니다.
    • 시간대 정보 (예를 들어 Asia/Seoul)

    반복 캠페인 관련 정보는 아직 C# 쪽에 추가되지 않았습니다. 향후 릴리즈에 추가할 예정입니다.

캠페인 보상 정보 접근하기

CampaignArgument 의 values 항목에는 각 보상의 아이디 / 값 정보가 들어있습니다. 해당 값은 캠페인 타입을 등록할 때 사용했던 타입대로 변환해서 사용해야 합니다. C++ 에서는 boost::variant<int64_t, double, string> 타입에서 해당하는 값을 꺼내어 사용할 수 있습니다. C# 에서는 해당하는 값을 JValue 타입에서 꺼내서 사용하면 됩니다.

47.7. 사용자 정의 기능 구현

DeployApiService::RegisterCustomQueryHandler() 함수를 호출해서 사용자 정의 기능을 등록할 수 있습니다.

static void RegisterCustomQueryHandler(
    const std::string &name,  // display name
    const http::Method &method,  // HTTP verb
    const std::string &uri,  // URI (MUST NOT include regex)
    // JSON attributes which should be provided by JSON request body
    const std::vector<std::string> &request_fields,
    // JSON attributes which should be placed in JSON response
    // (for successful response)
    const std::vector<std::string> &response_fields,
    const CustomApiHandler &handler,
    // Optional data which can be used by iFunDeploy to provide
    // addtional UI component or functionality.
    const Ptr<ExtraData> &extra_data);
public class DeployApiService {
  public static void RegisterCustomApiHandler(
      string name,
      http.Method method,
      string uri,
      IEnumerable<string> request_fields,
      IEnumerable<string> response_fields,
      Deploy.CustomApiHandler handler);
}

여기서 name 은 단순히 화면에 표시되는 값입니다. 이 함수에서 지정한 HTTP methodURI 값이 실제 호출할 때 사용하는 API가 됩니다. 이때 HTTP method로는 GET, POST, DELETE, PUT 의 4가지를 사용할 수 있습니다.

그리고 request_fieldsresponse_fields 인자를 통해서는 API 요청 및 응답에 포함되어야 하는 필드 목록을 지정할 수 있습니다. 만약 API 요청에 해당 필드가 없다면 handler 로 지정한 함수를 호출하지 않고 오류 로그만 남기고, handler 함수가 리턴한 응답에 해당 필드가 없다면 응답은 전송하지만 경고 로그를 남깁니다. API 응답 형식으로는 JSON 오브젝트 혹은 JSON 오브젝트의 배열 을 사용할 수 있습니다.

사용자 정의 기능 핸들러도 비동기 HTTP 응답 을 사용합니다.

47.7.1. 추가 설정

아이펀 디플로이는 사용자 정의 기능을 세부적으로 제어할 수 있는 기능을 점차 늘려가고 있습니다. 여기서는 해당 기능을 아이펀 엔진을 통해서 사용할 수 있는 방법을 설명합니다. 해당 기능은 struct DeployApiService::ExtraData 및 보조 함수들을 이용해서 사용할 수 있습니다. 만약 아이펀 디플로이 관련한 특정 추가 설정 기능이 아이펀 엔진에 추가되지 않은 상태여도 직접 JSON 형식의 값을 전달해서 사용하는 것도 가능합니다.

드랍다운 리스트 (셀렉트 박스)

// 방위(direction 입력)에 대한 셀렉트 박스가 뜨도록 옵션 설정
Ptr<DeployApiService::ExtraData> options = DeployApiService::ExtraData::Create();
std::vector<std::string> dir_options = {"North", "East", "South", "West"};
options->SetDropdownList("direction", dir_options);

// NOTE: dir_option 을 RegisterCustomQueryHandler 의 마지막 인자로 전달
DeployApiService::RegisterCustomQuery(
    "SomeCustomHandler",
    http::kPost,
    "some-handler",
    std::vector<string>{"direction"},
    std::vector<string>{/*결과 필드 목록*/},
    some_handler_function,
    options);
해당 기능은 아직 C# 에서 사용할 수 없습니다. 향후 릴리즈에 포함할 예정입니다.