Archive for the ‘google’ Category

6月
25

Android Market公開を目指してAndroidアプリを開発する!(メイン画面作成編)の続きです。

今回は、google翻訳APIを呼び出す部分を実装します。

過去に、TranslatAIRでActionScriptを使って実装しているので、それをJavaに移植します。

まず、前回ご紹介したSpinnerコントロールを初期化する部分を少し修正します。なぜかというと、google翻訳APIに対して、「英語を日本語に訳して」というような指示をするには、langpairパラメータを指定しますが、「English」ではなく「en」というように指定します。この例で言いますと「langpair=en|ja」となります。

Spinnerコントロールのラベルと値を使い分けられるようにします。

strings.xmlに以下の部分を追記します。

    <string-array name="language_value">
        <item>en</item>
        <item>ja</item>
        <item>zh</item>
        <item>it</item>
        <item>es</item>
        <item>fr</item>
        <item>de</item>
        <item>ru</item>
        <item>ko</item>
    </string-array>

Spinnerコントロールのラベルと値を保持するためのモデルクラスを作成します。

package jp.flashcast.translator.android.model;

public class LanguageModel {
	private String label;
	private String value;

	public LanguageModel(String label, String value) {
		this.label = label;
		this.value = value;
	}

	public void setLabel(String label) {
		this.label = label;
	}

	public String getLabel() {
		return label;
	}

	public void setValue(String value) {
		this.value = value;
	}

	public String getValue() {
		return value;
	}

	@Override
	public String toString() {
		return label;
	}
}
toString()メソッドをオーバーライドすると、その結果を、Spinnerコントールのリストに表示することができます。

次に、Spinnerコントロールに値も格納するように、前回のサンプルを一部修正します。

●修正前

	String[] labels = getResources().getStringArray(R.array.language_label);
	ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, labels);
	adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

●修正後

    	String[] labels = getResources().getStringArray(R.array.language_label);
    	String[] values = getResources().getStringArray(R.array.language_value);
    	ArrayAdapter<LanguageModel> adapter = new ArrayAdapter<LanguageModel>(this, android.R.layout.simple_spinner_item);
    	adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

翻訳に必要な情報をまとめるためのモデルクラスを作成しました。

package jp.flashcast.translator.android.model;

public class TranslateModel {
	private String original;
	private String translated;
	private String srcLanguage;
	private String dstLanguage;
	private String dt;
	private boolean success;

	public TranslateModel() {
		this.original = "";
		this.translated = "";
		this.srcLanguage = "en";
		this.dstLanguage = "ja";
		this.success = false;
	}

	public void setOriginal(String original) {
		this.original = original;
	}

	public String getOriginal() {
		return original;
	}

	public void setTranslated(String translated) {
		this.translated = translated;
	}

	public String getTranslated() {
		return translated;
	}

	public void setSrcLanguage(String srcLanguage) {
		this.srcLanguage = srcLanguage;
	}

	public String getSrcLanguage() {
		return srcLanguage;
	}

	public void setDstLanguage(String dstLanguage) {
		this.dstLanguage = dstLanguage;
	}

	public String getDstLanguage() {
		return dstLanguage;
	}

	public void setDt(String dt) {
		this.dt = dt;
	}

	public String getDt() {
		return dt;
	}

	public void setSuccess(boolean success) {
		this.success = success;
	}

	public boolean isSuccess() {
		return success;
	}
}

翻訳APIを呼び出すところを実装します。
こちらもやり方いろいろあると思いますが、

長時間実行されうるネットワーク操作・データベース操作や、ビットマップのリサイズといった計算時間のかかるものについては、サブスレッド内で実行すべきです(あるいはデータベースアクセスについては非同期リクエストを利用する方法もあります)。

みゅお(muo_jp)によるAndroidのドキュメント翻訳より引用。

ということなので、Threadクラスを継承した別クラスで処理させることにしました。

package jp.flashcast.translator.android.thread;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;

import jp.flashcast.translator.android.model.TranslateModel;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.Handler;

public class TranslateThread extends Thread {
	private Handler handler;
	private Runnable runnable;
	private final HttpClient client;
	private final HttpPost post;
	private final static String G_TRANSLATE_API_URL = "http://ajax.googleapis.com/ajax/services/language/translate";
	private final static String G_TRANSLATE_API_VERSION = "1.0";
	private TranslateModel model;

	public TranslateThread(Handler handler, Runnable runnable, TranslateModel model) {
		this.handler = handler;
		this.runnable = runnable;
		this.model = model;
		this.client = new DefaultHttpClient();
		this.post = new HttpPost(G_TRANSLATE_API_URL);
	}

	private String Translate(String query, String srcLanguage, String dstLanguage) {
		ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(4);
		params.add(new BasicNameValuePair("v", G_TRANSLATE_API_VERSION));
		params.add(new BasicNameValuePair("q", query));
		params.add(new BasicNameValuePair("format", "text"));
		params.add(new BasicNameValuePair("langpair", srcLanguage + "|" + dstLanguage));

		StringBuilder builder = new StringBuilder();

		try {
			post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
			final HttpResponse response = client.execute(post);

			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				InputStream stream = response.getEntity().getContent();
				BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

				String line;

				while ((line = reader.readLine()) != null) {
					builder.append(line);
				}
				stream.close();
			}
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return builder.toString();
	}

	@Override
	public void run() {
		if (model != null) {
			String translated = Translate(model.getOriginal(), model.getSrcLanguage(), model.getDstLanguage());

			try {
				JSONObject json = new JSONObject(translated);

				if (json.has("responseStatus") && json.getInt("responseStatus") == 200) {
					model.setTranslated(json.getJSONObject("responseData").getString("translatedText"));
					model.setDt(new Date().toLocaleString());
					model.setSuccess(true);
				}
				else {
					model.setSuccess(false);
				}
			} catch (JSONException e) {
				model.setSuccess(false);
			}

			handler.post(runnable);
		}
	}
}

呼び出し部分は、こんな感じです。

    private void Translate() {
    	if (dialog == null) {
    		dialog = new ProgressDialog(TranslatorSample2.this);
    		dialog.setIndeterminate(true);
    	}
		dialog.setMessage("Translating... Please wait.");
    	dialog.show();

    	(new TranslateThread(new Handler(), TranslatorSample2.this, model)).start();
    }

処理中のダイアログを出してから、翻訳処理に入ります。
こんな感じです。

device

この関数を、前回実装していなかったボタンクリック時の処理で呼び出します。

        btnTranslate.setOnClickListener(new OnClickListener() {

			public void onClick(View v) {
    			EditText text;
    			Spinner spSrc;
    			Spinner spDst;

    			ToggleButton btnVector = (ToggleButton)findViewById(R.id.vector);

    			if (btnVector.isChecked()) {
    				text = (EditText)findViewById(R.id.dsttext);
    				spSrc = (Spinner)findViewById(R.id.dstlanguage);
    				spDst = (Spinner)findViewById(R.id.srclanguage);
    			}
    			else {
    				text = (EditText)findViewById(R.id.srctext);
    				spSrc = (Spinner)findViewById(R.id.srclanguage);
    				spDst = (Spinner)findViewById(R.id.dstlanguage);
    			}

    			if (!text.getText().toString().equals("")) {
    				if (model == null) {
    					model = new TranslateModel();
    				}

    				model.setOriginal(text.getText().toString());
    				model.setSrcLanguage(((LanguageModel)spSrc.getSelectedItem()).getValue());
    				model.setDstLanguage(((LanguageModel)spDst.getSelectedItem()).getValue());

    				Translate();
    			}
		}

翻訳が終わったら、処理中ダイアログを消して、翻訳結果を画面に表示させますので、Runnableインターフェイスを実装します。

public class TranslatorSample2 extends Activity implements Runnable {

run()メソッドを実装して、翻訳の後処理をします。

	public void run() {
		dialog.dismiss();

		if (model.isSuccess()) {
			((EditText)findViewById(
				(!((ToggleButton)findViewById(R.id.vector)).isChecked()) ? R.id.dsttext : R.id.srctext))
				.setText(model.getTranslated());
		}
		else {
			Toast.makeText(this, "Failed to translate...", Toast.LENGTH_SHORT).show();
		}
	}

最後に、アプリケーションに対して、Internetに接続する権限を付与します。AndroidManifest.xmlに1行追記します。

	<uses-permission android:name="android.permission.INTERNET" />

これで、Internetに接続し、9ヶ国語の翻訳が出来るようになりました!

サンプルソース

2月
23

郵便番号検索 -zipnavi-にて、地図から郵便番号検索機能をリリースしました。
この機能は近所や公共施設など、地図上の場所は分かるけど、住所と郵便番号が分からない、といった場合などに使ってもらえる想定で作りました。
これで「住所」、「都道府県、市区町村、町域」、「地図」の3つの視点から郵便番号の検索する事が可能になりました。

新たにリリースした地図から郵便検索の画面イメージは以下の様な感じです。
地図検索ページ


初期状態は日本全体なので、駅名など検索したいエリア付近の情報を入力して地図を絞り込みます。そして地図上をクリックすると、吹き出しが表示されて、そこに住所と「郵便番号検索」ボタンが表示されます。
地図上の地点をクリック時


「郵便番号検索」ボタンをクリックすると、zipnaviの検索エンジンにより郵便番号検索が行なわれ、結果が噴出し内に表示されます。
郵便番号検索

郵便番号検索 -zipnavi- 地図から検索にて実際に動作を確認できます。


ちなみに仕組みを簡単に説明すると以下の通りです。

1.GoogleMAPS APIを使用して、地図上から緯度経度を取得する。
2.GoogleMAPS APIの逆ジオコーディングを使用して、緯度経度から住所を取得する。
3.zipnaviの検索エンジンを使用して、住所から郵便番号を検索する。

上記を実現する為の方法として、1、2は、メンバーの「もじゃもじゃ」さんが既に開拓済みだった為、そちらの記事を参考にさせてもらい、それに若干のアレンジを加えました。
flashcast:フリーで働くITエンジニア集団のブログ: iPhone OS 3.0のSafariでGPS機能を使ったWeb Applicationを作る!(iPhoneでテスト編)

あと、GoogleMAP上にAdsenseの広告も表示させましたが、これもflashcast:フリーで働くITエンジニア集団のブログ: Strayed Childの地図に広告を載せてみた!の記事を参考にさせてもらいました。


ただ、開発中のテストで気付いたのですが、上記2の方法で取得できる住所情報にはちょっとクセがあります。
A.地図上の道路を指定すると、「日本国道1号線」。海上を指定すると、「日本」といった情報が表示される。
国道海上
B.地図上の陸地を指定しているのに、住所ではなく、郵便番号が表示される。
郵便番号



Aの様な場合、googleのAPIでは正確な住所が表示されないようです。

Bはちょっとナゾです。
コレは何?
ネットで調べてもよく分かりません。
ただ表示された郵便番号をよ~く見ると・・・
これはたぶん事業所の郵便番号の様に思われます。

上記の様なケースを考慮し、Aの場合は噴出しに警告メッセージを表示しました。
またBの場合は、新たに郵便番号検索するまでもないので、GoogleMAPS APIで取得できた郵便番号をそのまま表示させました。

GoogleMAPS APIの仕様では上記の様な記述はありませんでしたが、色々とテストをしていく中で、上記の様な判断条件でほぼ処理出来る事が分かりました。
ただ、一部の地域で上記A、Bいも該当しない例外がありますが、その際はzipnaviの検索エンジンがエラーと判断するので、間違った郵便番号が検索される事はありません。
具体的には以下の様な地域です。
茨城県かすみがうら市
※「茨城県かすみがうら市」だけでは、郵便番号を特定できません。この様な場合も、googleのAPIでは正確な住所が表示されないようです。


3については取得した住所をAjaxを使用して、zipnaviの検索エンジンに渡し、その検索結果(郵便番号)を地図上に表示しています。
なお、今回の機能追加に合わせて、サイトのデザインもリニューアルしました。
新しくなったzipnaviを今後ともよろしくお願いします!

12月
21

やっと、アプリが完成したので、リリースしましたっ!

名前は、TranslatAIRです。翻訳するAIRアプリということで、translateとAIRを合体してつくった造語です。

基本機能は、今までブログに書いてきたものと同等ですが、いくつか機能を追加しました。

  1. 翻訳結果を表示する機能。
  2. 任意の文章を翻訳する機能。
  3. 翻訳履歴を保存・閲覧する機能。
  4. 設定変更機能
  5. クリップボードの中身を翻訳結果に差し替える機能。

などです。

1は説明するのが難しいのですが、「小さい画面にじわっと浮き出て、じわっと消えていく」と言った感じでしょうか。

info1info2info3info4info5

2、3、4に関しては各画面を用意しました。

翻訳画面

翻訳画面

翻訳履歴画面

翻訳履歴画面

設定変更画面

設定変更画面

これらの画面を呼び出すためのメニューを、アイコンのところに実装しました。

menu

5のクリップボードの中身を翻訳結果に差し替える機能は、その名前の通りで、翻訳後にペーストすると、中身が翻訳されているといった感じになります。

と、こんなところでしょうか。
詳しくはこちらをご覧ください。

以外に便利だと思いますので、是非、使ってみてください!

TranslatAIR

12月
20

Google Japan Blog: Google Maps API でマネタイズを見て、Google Mapsに広告を載せられることを知りました。

これはいい!ということで、以前、iphone向け(現在は、Android、Firefox 3.5に対応済)に開発したWebアプリ、Strayed Childの地図に載せたいと思います。

バックナンバーはこちら

Services – Google Maps API – Google Codeを参考に実装してみました、非常に簡単です。

	var adsManagerOptions = {
		maxAdsOnMap : 2,
		style: G_ADSMANAGER_STYLE_ADUNIT
	};

	adsManager = new GAdsManager(map, 'pub-XXXXXXXXXX', adsManagerOptions);
	adsManager.enable();

という数行を、初期化処理に入れればいいだけです。

ただ、これだと、

map

のように、住所表示が広告に押し出されて、画面からはみでるような感じになってしまいます。

Google Maps API Reference – Google Maps API – Google Codeを見ると、

style

G_ADSMANAGER_STYLE_ICON

というオプションがありました。広告がアイコン風の表示になるのだと思い、試してみたのですが、広告自体が表示されなくなってしまいました。

日本語訳のGoogle Maps API リファレンス – Google Maps API – Google Codeを見ると、なんと、styleオプション自体がありません。。。

options

えー。。。

とりあえず、アイコン風表示は対応されるまで待つことにします。

とはいえ、このままでは住所が見づらいので、広告の表示は1件にしました。

表示されている地図に関連した広告が出るはずなので、便利になると思います!iPhoneやAndroidをお持ちの方、是非、試してみてください!

Strayed Child

11月
24

flashcast:フリーで働くITエンジニア集団のブログ: google翻訳APIを使ったAIRアプリを作る!(クリップボードを監視編)の続きです。

前回、実装していなかったgoogle翻訳API呼び出しの部分を実装していきたいと思います。

flashcast:フリーで働くITエンジニア集団のブログ: ActionScript3.0でJSONを読み込む方法を参考に思い出しながら、実装しました。方法、内容はあまり変わっていません。

as3corelibが新しくなっていましたので、最新のas3corelib-.92.1.zipをダウンロードして利用しています。

APIを呼び出す部分は、別クラスにしました。

package
{
	import com.adobe.serialization.json.JSON;

	import flash.net.URLVariables;

	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	public class TranslateManager
	{
		private var service:HTTPService;

		public function TranslateManager()
		{
		}

		public function initTranslateManager():void {
			service = new HTTPService();
			service.url = "http://ajax.googleapis.com/ajax/services/language/translate";
			service.method = "post";
			service.addEventListener(ResultEvent.RESULT, onResult);
			service.addEventListener(FaultEvent.FAULT, onFault);
		}

		public function Translate(v:String, q:String, languageFrom:String, languageTo:String):void {
			var forms:URLVariables = new URLVariables();

			forms.v = v;
			forms.q = q;
			forms.format = "text";
			forms.langpair = languageFrom + "|" + languageTo;
			service.request = forms;
			service.send();
		}

		private function onResult(event:ResultEvent):void {
			var json:Object = JSON.decode(event.result.toString());

			if (json.responseStatus == 200) {
				if (json.responseData.translatedText != "") {
					trace(json.responseData.translatedText);
				}
				else {
					trace("翻訳失敗...");
				}
			}
			else {
				trace("翻訳失敗...");
			}
		}

		private function onFault(event:FaultEvent):void {
			trace("翻訳失敗...\n" + event.fault.message);
		}

	}
}
翻訳結果のフォーマットを指定するために、32行目のようにformatパラメータを渡します。デフォルトはhtmlでエンコーディングされた結果が得られます。今回は、ブラウザに表示するわけではないのでtextにしています。

とりあえず、値が変わったときにAPIを呼び出すようにしたので、メインのロジックには、上記クラスのインスタンスを生成して、関数をコールする部分を追加しています。

package
{
	import mx.binding.utils.ChangeWatcher;
	import mx.core.Application;
	import mx.events.PropertyChangeEvent;

	public class TranslatorSample4Base extends Application
	{
		private var imanager:IconManager;
		private var cmanager:ClipboardManager;
		private var gmanager:TranslateManager;

		private var gmodel:TranslateModel;

		public function TranslatorSample4Base():void {
		}

		public function initApp():void {
			gmodel = new TranslateModel();

			imanager = new IconManager();
			cmanager = new ClipboardManager();
			gmanager = new TranslateManager();

			imanager.initIconManager();
			cmanager.initClipboardManager(gmodel);
			gmanager.initTranslateManager();

			ChangeWatcher.watch(gmodel, '_original', onClipboardChangeHandler);
		}

		private function onClipboardChangeHandler(event:PropertyChangeEvent):void {
			gmanager.Translate("1.0", gmodel.Original, "en", "ja");
		}
	}
}

それと、前回まではカスタムイベントクラスを作って、そのイベントが発生した際に翻訳にいくようにしていました(上記ソースの29行目のところです)。

cmanager.addEventListener(TranslatorSampleEvent.CLIPBOARD_CHANGE, onClipboardChangeHandler);

ここは、ChangeWatcherクラスを利用することにしました。

ChangeWatcher クラスは、バインド可能な Flex プロパティと共に使用できるユーティリティメソッドを定義します。 これらのメソッドを使用すると、バインド可能プロパティが更新されるたびに実行されるイベントハンドラを定義できます。

ChangeWatcher – ActionScript 3.0 言語およびコンポーネントリファレンスより引用。

ということで、この部分を以下のようにしています。

ChangeWatcher.watch(gmodel, '_original', onClipboardChangeHandler);

このために、新たにModelクラスを作成しました。

package
{
	[Bindable]
	public class TranslateModel
	{
		public var _original:String;

		public function TranslateModel()
		{
		}

		public function set Original(original:String):void {
			_original = original;
		}

		public function get Original():String {
			return _original;
		}

	}
}

カスタムイベントとイベントを発生させる部分は不要になってしまいましたので、イベントを発生させていた部分は以下のように非常にシンプルになりました。

package
{
	import flash.desktop.Clipboard;
	import flash.desktop.ClipboardFormats;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.utils.Timer;

	public class ClipboardManager extends EventDispatcher
	{
		private var timer:Timer;
		private var _gmodel:TranslateModel;

		public function ClipboardManager()
		{
		}

		public function initClipboardManager(gmodel:TranslateModel):void {
			_gmodel = gmodel;

			timer = new Timer(1000);
			timer.addEventListener(TimerEvent.TIMER, onTimerHandler);

			if (Clipboard.generalClipboard.hasFormat(ClipboardFormats.TEXT_FORMAT)) {
				_gmodel.Original = Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
			}

			timer.start();
		}

		private function onTimerHandler(event:TimerEvent):void {
			if (Clipboard.generalClipboard.hasFormat(ClipboardFormats.TEXT_FORMAT)) {
				var clipboard:String = Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;

				if (clipboard != _gmodel.Original) {
					_gmodel.Original = clipboard;
				}
			}
		}

	}
}

わかりにくいですが、以下が、実行した結果になります。
翻訳結果をデバッグトレースしています。

sample4

よくよく考えると、このやり方だと、クリップボードにコピーされると、何でもかんでもインターネットを流れてしまいます。

要件の

翻訳元の文章は、インターネットを流れるので、ユーザが意図しない翻訳は極力避ける。

flashcast:フリーで働くITエンジニア集団のブログ: google翻訳APIを使ったAIRアプリを作る!より引用。

に反します。

う~ん、どうしよう・・・
とりあえず作戦を考えたいと思います。

サンプルソース

サンプルソースをダウンロードできるようにしておきます。

※ クリップボードにコピーした文字が無条件にgoogleに送られるので、利用時には注意してください。

  • Search:
  • flashcastとは?

    東京を中心に、現在フリーランスとして活動しているITエンジニア、および、かつてフリーランスとして活動していた起業家達が立ち上げたコミュニティーです。

    みんなで集まって面白いことをやろう!形に残そう!ということで、ブログをはじめました。

    技術情報や、フリーエンジニアに役立つ情報などを、ご紹介できたらと思っています。

    お問い合わせ:
    info@flashcast.jp
  • カレンダー

    2010年7月
    日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日
    « 6月    
     123
    45678910
    11121314151617
    18192021222324
    25262728293031
  • メンバー紹介

    もじゃもじゃ
    flashcastのリーダー

    3年ほどフリーのITエンジニアとして活動。現在は、社員2名の株式会社を経営しています。

    一攫千金を夢見る野心家です。

    ライブキャスト

    yasu
    ダイバー

    自宅サーバーでホームページを作り始めました。

    少しずつ記事を増やしていきますので足を運んでください。

    よろしくお願いします。

    sa-sa-ki.jp

    のら
    たびびと

    ねこ好きに悪人はいなーいっ!!

    バイクや車も好きです。

    めぐ
    デザイナーのたまご

    音楽とデザインとお酒をこよなく愛する永遠のダイエッター。

    現在ペンタブレットでイラストを勉強中。

    Hiro
    コンサル

    PMやSEの案件を業務委託で請けることが多いですが、小規模案件も受託でやっています。

    得意な分野はマイクロソフト製品や関連技術によるシステム構築です。

    KEI
    取締役の風格

    最年少なのに、メンバーで1番の貫禄の持ち主!?

    C#や.netなどサーバ側の開発が得意。

    ろっきー
    美食家★パパ

    自分にとっての息抜きは、ドライブして温泉に入って、美味しいご飯を食べる事。

    ココロとカラダのリフレッシュを大切にし、日々の仕事に励む一児の父親です。

    郵便番号検索

    my-hobby

    とのさま
    げーむのおうさま

    大人なのに好きなことしかやらない駄目人間。

    Web系が得意、アクセスは苦手><

    tonosamart.com

    セクレタリアト
    ギャンブラー

    フリーランス時代は仲間の現場を探すことが多く、それをきっかけに会社を設立。

    現在はSI業に特化せず、他の業種にも興味を持ち始めています。

    メドレー株式会社

  • 広告