あいつの日誌β

働きながら旅しています。

韓国のソウル、釜山へティーウェイ航空(tway)とPeach航空(ピーチ)を使ってそれぞれ4泊5日, 2泊3日の旅をしてきた(1)

f:id:okamuuu:20190717163139j:plain

あらすじ

とある洋服屋の社長さんとご飯食べ手た時に「韓国人は爆買いしないのか?」と聞いたところ「洋服に関しては韓国の方が安いし種類もあるから日本で買う理由がない」という事でした。

そうかー洋服安いのかー。コスメも安いっていうし、そういえば韓国はすごく近い場所にあるのにあまり良く知らないんだよな、と思ったのでちょっと一週間ほど行ってきました。

観光した場所

ソウルと釜山(ぷさん)に行ってきました。ソウルでは明洞(みょんどん), 益善洞(いくそんどん), 東大門(トンデモン), 弘大(ほんで), COMMON GROUND, COEX MALL へ。釜山では南浦洞(なんぽどう), 西面(そみょん), 海雲台(へうんで)へ行って来ました。

もし上記の場所に興味がある人はこのブログをご覧になってみてください。

旅行日程

場所 日時 行程
東京 2019/07/10 12:00 成田国際空港
ソウル 2019/07/10 14:40 仁川国際空港
ソウル 2019/07/10 18:00 ゲストハウス: ブルーマウンテン明洞 check in
ソウル 2019/07/14 10:00 ゲストハウス: ブルーマウンテン明洞 check out
ソウル 2019/07/14 11:20 KTX: ソウル駅発
釜山 2019/07/14 14:50 KTX: 釜山駅着
釜山 2019/07/14 16:00 ゲストハウス: K-Guesthouse Premium Nampo 1 check in
釜山 2019/07/16 10:00 ゲストハウス: K-Guesthouse Premium Nampo 1 check out
釜山 2019/07/16 16:00 金海(キメ)国際空港発
大阪 2019/07/16 17:30 関西国際空港

かかった移動費と宿泊費は以下の通りです。たまたまウォン安の時だったので少しお得になっているかも。

  • 往路: 11010円(座席番号を指定して+1700)
  • ソウル宿泊費: 220,968ウォン(だいたい21000円)
  • 新幹線: 485,00ウォン(だいたい4500円)
  • 釜山宿泊費: 80,100ウォン(だいたい7500円)
  • 復路: 69,200ウォン(だいたい6500円)

これらが合計で50510円

大半が食事とコーヒー代が3万円ぐらい、地下鉄で4700円ほど(カード代700円+4000円チャージ)かかっていて、洋服Tシャツ3枚買いました(6千円ぐらい)。というわけで一週間滞在して費用が大体9万円です。私の場合飛行機の安い時期をまず選んで格安のゲストハウスに空きがある事を確認してから日程を組んでいるので、お一人様でちゃんとしたホテルに宿泊する場合はもう少し料金かかると思います。

ティーウェイ航空とピーチ航空について

ティーウェイ航空の成田カウンターではセルフチェックインができないです。でも割と成田->ソウル行きの旅行者は多かったです。釜山行きはそうでもなかったです。そんなわけでソウル行きの搭乗手続きのカウンターの前に長蛇の列が出来ており、手続きまでに30分待ちました。出発時刻から逆算すると出発の2時間前にはついてないとちょっと不安だと思います。ちなみにこの日は出発時間が30分遅延していました。

ちなみに私は座席番号をA1(+1,700円)を指定したのですが目の前が壁になっているタイプの航空機なので思ったより広さがなくて残念でした。ただし、隣の席がなぜか無人だったのでそこそこ快適に過ごせました。

手荷物に関しては以下の記載がありました。私は普段から5kg程度のバックパックで旅行しているので重さは問題なし。往路ではハンドバック、ショッピングバック無しですから全く問題なし。

三辺(横・縦・高)の合が115cm, 重量10kg 以下の手荷物 1つに限り、無料で機内持込できます。

https://www.twayairlines.jp/yoyaku/smart/static/baggage.html

ピーチ航空

ピーチやジェットスターなどの格安 LCC は便利なんですが手荷物の制限がかなり厳しいので注意が必要です。2点OKといいつつ 7kg の制限。私は今回お土産を最初から韓国海苔にすると決めてあったのでそこは問題なし。ただ、そこそこ厚手の半袖Tシャツを3着購入したのでそこだけ気をつけました。

https://www.flypeach.com/pc/jp/lm/ai/airports/baggage/carry_on_bag

釜山から大阪まで1時間30分だったので特に座席指定しませんでしたがやっぱり狭いですね。隣に夫婦が座っていらっしゃったのですが「狭いわー足短くてよかったわー」と関西人らしいコテコテのボケを披露していました*1。そういえば釜山から大阪までは赤いパスポート(日本人)持っている人が多かったです。

ゲストハウス: ブルーマウンテン明洞

ちなみにブルーマウンテン明洞ではツインベッド、K-Guesthouse ではダブルベッドを予約しました。シングルルームも存在するようですがあまり空きがなかったのと、そこまで値段に差がでるわけではないので1人旅でもツインベッド、ダブルベッドにしても損をする感覚はないです。

とはいえ2人で宿泊すればお得になるんじゃないか、という話ではあるのですが、正直あまり部屋が広くないのでどんなに気心がしれている友人でも私はちょっと遠慮したい、そんな感じでした。

ブルーマウンテン明洞は立地は最高だったのですが、ドライヤーが温風でなかったり、シャワールームの扉がきちんと閉まらなかったりと、ちょっと微妙、でも別に私1人だったら困らないなあ、という感じだったのでもし、この記事見てブルーマウンテン明洞に興味を持った人はその点ご注意ください。個人的に室内に机がなかったので室内で作業するのはつらいのでノマドワーカーにはあんまり向かないかも。

K-ゲストハウス プレミアム南浦1

K-ゲストハウス プレミアム南浦1 は思ったよりも広くて快適でした。とはいえやはり2人で宿泊するのはつらい、そんな感じです。ブルーマウンテン明洞に比べて K-ゲストハウス は割と広かったし、ややおしゃれでした。ソウルと釜山なので比較しづらいのですが、明洞にも K-ゲストハウスがあるようなので次回ソウルに旅行する際には検討してみたいと思います。

f:id:okamuuu:20190717163615j:plain

韓国の印象

ソウルに関しては滞在先が明洞だった事もあるのですが日本語がかなり通じます。屋台の店員さんや赤い服を着た観光案内のスタッフさんに英語で話しかけても「日本人ですか?」と聞き返されます。彼らからすると日本語の方が案内しやすいんだと思います。あと日本人だからといって冷たくされたと感じた事は一度もなかったです。

それから洋服は確かに安いので時期を見て韓国を訪れるのは面白いと思います。ただし、洋服のサイズが日本と違います。私は日本ではMサイズなのですが、韓国ではSサイズにあたります。そして私が旅行中に気づいたのはSサイズの洋服があんまり無かったです。そして割とフリーサイズの服がいっぱいでした。ちょっとここは誤算でしたがまたセールの時期にでも訪れてみようと思います。ちなみに私は中目黒で6000円ぐらいで売っているTシャツを、ウォン安とセールのおかげもあり2500円ぐらいで買えました。

f:id:okamuuu:20190717163735j:plain
そこそこの厚手があるTシャツ。定価の39000ウォンで購入。SサイズはラインナップにないそうなのでMサイズを買いました。

f:id:okamuuu:20190712204617j:plain
have a good time のポケT。中目黒で6000円ぐらいでで購入したものが韓国のお店でセールのおかげもあって2500円ぐらい。これは Sサイズがありました。

ちなみに、日本人の成人男性の平均身長が 171cm に対して韓国の成人男性の平均身長は 175cm です。そして韓国人男性はやたらと筋肉質の人が多かったです。兵役義務があったり、普通の公園にそこそこの運動器具があったり、サウナや銭湯にウェイトマシーンがあったりするのでそういう事も関係しているのかもしれません。

ごはんに関しては何を注文しても基本的にナムルという惣菜が付いてきて量がたくさんあります。でも値段に関しては東京とそれほど変わらないと思います。あと韓国に来たらこれ食べよう的な料理は大体2人前からなのでお一人様にはつらいかもしれません。そしてブログなどで紹介されている店にも訪問しましたが、個人的にはフードコートで食べるほうが満足度が高かったです。もし土日に旅行する場合はむしろフードコートの方が行列ができないのでおすすめです。

f:id:okamuuu:20190717164514j:plain
サムギョプサル2人前3000円。基本的に2人前からでないと注文できないのでご注意を。この他ナムルと野菜取り放題のシステムなのでボリュームが半端ないです。

まとめ

というわけで1週間ほど韓国に滞在してきました。たまたまウォン安だったし、ご飯も美味しかったので満足しました。韓国は週末にふらっと行ける距離だし、他の国に行く時に経由するのも便利だと思うので今後何度か足を運ぶ事になるだろうなと思いました。

ソウルと釜山で食べたご飯については別の記事にします。

*1:奥さま拾わずにスルーしていました

Meguro.es # 21 @ ビザスク で Vue.draggable について話をしてきた

というわけで行ってまいりました。今回は目黒駅ではなく目黒区という広いくくりでの開催です。

meguroes.connpass.com

スポンサー

株式会社ビザスクさんです。ありがとうございます。前回開催時よりもちゃんとしたビールが置いてあったので私の中ではとても好感度が高いスポンサーです。といっても私は健康診断前日だったのでのめませんでしたけど。

ちなみに8/22にクラフトビールを飲みながらなんか話をする会があるそうです。

visasq.connpass.com

会場が常に足りてない問題

運営の人たちも色々と工夫して「同じ場所での連続開催を避けるように」しているので基本的に開催場所を次回どこにするのか悩ましい問題がいつも発生しているようです。この状況は「自分の会社のスペース貸したら業務終了後にすぐに勉強会に行ける」チャンスです。そこそこの人数を収容できる会社にお勤めの方は是非運営の人に声かけてあげてください。meguro.es に限らず。

LT

というわけで LT です。

ArrayBuffer と binary: @sasurau4

仕事で Bluetooth である機器をつなごうとしたら Binary が現れた。どうする?みたいなお話でした。得体の知れないものを見つけてこれはなんだ?ということでバイナリを調べて触ってたらコンピューターの気持ちになれたっていうお話でした。

speakerdeck.com

私はあんまりバイナリデータに直面する機会がないのですが、実際に局面した人の話が聞けて良かったです。ちなみにあんまりバイナリの事深く調べなくてもやりたいことはできたそうです*1

LitElementとWeb Componentsの比較: @tiwu (Taguchi Wataru)

LitElement の LT。最近 JS 界隈の勉強会で LitElement というワードを聞くのでそろそろ触ってみた方がいいのかもしれない。LitElement の求人増えるのでしょうか?

https://speakerdeck.com/tiwuofficial/litelementtoweb-componentsfalsebi-jiao-number-meguroesspeakerdeck.com

javascript のメモリと WeakRef: @brn0227

名前の由来は青野さんだから。変数作るのにもコストが掛かっていて、空いているメモリのスペースを裏側でどうやって管理しているのかを紹介。ガベージコレクションは知ってたけどヤング領域とオールド領域の話は知らなかった。難しい話ですけど分かりやすく説明してくれてました。まあそれでもだいぶ分かってないですけど。

speakerdeck.com

ヤング、オールドは簡単に言うと「いつまでも生き残っている変数はもう消される可能性低いから別の場所に移動させておけば空き領域の管理楽じゃね?」みたいな話です。たぶん。

サンプルで学ぶ THE Elm Architecture @y_taka_23 チェシャ猫

どうやら勉強会でいつも猫耳をつける人らしいです。そういえばいつもテンガロンハットかぶっているITジャーナリストさんがいるけどそういう事なのかな*2

speakerdeck.com

Elm に関しては随分前に触った記憶があるのですが、今回のトークで Elm は Web アプリ開発に特化しているがサーバーサイドの実装を行う想定はされてない、という事を知りました。

okamuuu.hatenablog.com

Google I/O ’19から見る新しいJavaScript @toshi-toma

Google I/0 で紹介されていたプロポーザルで気になった内容について紹介。

  • Private filed
  • Numeric Separators
  • Array flat
  • Promise.allSettled
  • Promise.any
  • top-level await

speakerdeck.com

個人的には Promise.allSettledPromise.any は頭の片隅に置いてこうかな、でもたぶん私が実装するとしたら一個でも失敗したらやり直し、みたいな富豪的な事するんだろうな...

top-level await はあったら便利だけどなくてもそんなに困らないから無理にプロポーサルしなくてもいいんじゃないかな、とか思ったりします。

Vue.draggable

私のターン。英語の勉強をしたくてアプリ作りました。英語の勉強はしてないです。そして資料のなかで draggable のつづりを間違えている。英語力はそのうちなんとかします。

ちなみにこの記事の一つ前に Vue.draggable の記事を投稿していますが、これはLTの時間が10分間あったにも関わらず間違えて5分だと勘違いしていたためです*3

Promiseの復習と予習 あらや

話そうとしていた内容が前の登壇者にだいぶ話されてしまったのでちょっとやりづらそうでした。聞いている方は違う角度から話きけるので全く気にならないのですが...まあ話す方はやりづらいですよね。

そして Promise.try のインターフェースがたしかに謎。

speakerdeck.com

スポンサーセッション: 株式会社ビザスク

会社の事業内容を説明。スキルの共有・マッチングサイトを運営していて、かなり好調な事業らしいです。エンジニア募集していてカジュアル面談を行なっているそうです。

懇親会

今回は参加せずに帰りました。

今回の勉強会で得た知見

  • ヤング領域とオールド領域
  • Elm はサーバーサイドには関わらない
  • Promise.allSettled, Promise.any

まとめ

というわけで今回も学びが多い勉強会になりました。このような機会を頂けるのも運営の方々のおかげだと思っておりますので今後もよろしくお願いします。

こちらからは以上です。

*1:それが聞きたかった

*2:認知してもらいやすいというメリットがある

*3:詳しい説明はこちらの記事をご覧ください、で5分に収める予定だった

Vue.draggable を Swappable にしたい

どうした?

英単語やら英会話を覚えたいので WEB 単語帳を作ってみました*1

https://flash-cards-marvelous.firebaseapp.com

その時に単語を並べ換える機能が必要だったのですがそこで Vue.draggable を使ってみました。これは Sorable をベースに Vue component 化したものです。

使ってみたところなかなか便利で、すごい、かんたん、気持ちいい Component だなと思いました。

なんですが DB に保存しようとした時に、一つのアイテムを並べ替えたときに変更されるアイテムがたくさんある事に気づきました。

以下の例のように、先頭のアイテムを一番最後に持っていった場合、以下のように全てのアイテムの位置が変わってしまう。

// sort
Before: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After:  [2, 3, 4, 5, 6, 7, 8, 9, 10, 1]

これだと DB に保存する時にちょっと悩ましい。上記のサービスは Firestore を使っているので配列を丸ごと保存しても良いと思いますが、一つの単語帳にたくさんの単語を登録した場合の事も考えないといけない。

将来的に特定の単語を fav したりする機能をつけようとすると コレクション(もしくはサブコレクション)にして ID を持たせて置いた方が良いのですがそうすると order を各行が持つので上記の例だと並び替えが行われる度に複数のドキュメントの order を update しないといけなかったりします。

どうしたい?

というわけで以下のように Swap したいという気分になりました。

// swap
Before: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After:  [10, 2, 3, 4, 5, 6, 7, 8, 9, 1]

なんですけどその機能は正当な理由により Vue.draggable にはありません。

github.com

どうする?

ソースコードを読んでいたら以下のようにする事によって Swap ぽい挙動にする事に成功しました。

以下が通常の使い方です

<template>
  <table>
    <draggable v-model="items" tag="tbody" @end="handleDragEnd">
      <tr v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td>
      </tr>
    </draggable>
  </table>
</template>

<script>
import draggable from "vuedraggable";

export default {
  components: {
    draggable,
  },
  data() {
    const items = [
      { id: 1, name: "Bianka Effertz", age: 37 },
      { id: 2, name: "Mckayla Bogan", age: 20 },
      { id: 3, name: "Estevan Mann", age: 17 },
      { id: 4, name: "Cloyd Ziemann", age: 55 }
    ]
    return { items }
  },
  methods: {
    handleDragEnd() {
      this.$toast.show('dragEnd')
      // update database here
    }
  }
}
</script>

これを以下のように修正しました。

<template>
  <table>
    <draggable v-model="items" tag="tbody" :move="handleMove" @end="handleDragEnd" :options="{animation:500}">
      <tr class="movable" v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td>
      </tr>
    </draggable>
  </table>
</template>

<script>
import draggable from "vuedraggable";

export default {
  components: {
    draggable,
  },
  data() {
    const items = [
      { id: 1, name: "Bianka Effertz", age: 37 },
      { id: 2, name: "Mckayla Bogan", age: 20 },
      { id: 3, name: "Estevan Mann", age: 17 },
      { id: 4, name: "Cloyd Ziemann", age: 55 }
    ]
    return { items }
  },

  methods: {
    handleDragEnd() {
      this.$toast.show('dragEnd')

      this.futureItem = this.items[this.futureIndex]
      this.movingItem = this.items[this.movingIndex]
      const _items = Object.assign([], this.items)
      _items[this.futureIndex] = this.movingItem
      _items[this.movingIndex] = this.futureItem

      this.items = _items
    },
    handleMove(e) {
      const { index, futureIndex } = e.draggedContext
      this.movingIndex = index
      this.futureIndex = futureIndex
      return false // disable sort
    }
}
</script>

サンプルはこちら:

fir-vue-draggable.firebaseapp.com

解説

draggable の :move プロパティはそのまま Sortable に渡されます。ここに関数をセットすることができるのですが false を返すと sort の挙動をキャンセルすることができます。

sort の挙動はキャンセルしつつ、ドラッグした状態で drag している item と drop されるであろう場所にある item を保持します。

sort の挙動は抑制されているので this.items は変化しないのですが、@end イベントで this.items を保持していた item を swap させてつじつまを合わせています。

というわけで

無事(?) Vue.draggable を Swap させる挙動に変更できました。とはいえそのそも Soratable が Swap する事を前提にした設計ではないのでべき論でいうと他の Vue component を使ったほうが良いのかもしれません。

というわけで何か良さげなライブラリーがあったらどなたか教えてください。

こちらからは以上です。

*1:肝心の英語は勉強していない

福岡の勉強会「俺の話を聞け!!LT大会 #14」に参加してきた 【宿とご飯編】

ということで今回の博多で宿泊した施設と立ち飲み屋について

宿泊所

f:id:okamuuu:20190523160624j:plain
お洒落な入り口。夜はライトアップされてより一層いい感じでした。

www.thegatehostel.com

私は旅行する時の宿泊所はホステル(簡易宿泊所)にする事が多いです。私は旅行中も大体作業しているので昼間はノマドして、仕事が終わったら立ち飲み屋に行って酔っ払って帰ってくるので清潔なベッドとシャワールームがあればそれでOKだからです。

そんなわけで私が宿泊所を選ぶ基準は以下の2点が重要になってきます。

  • ノマドスペースが充実している
  • 繁華街の近く

今回は5/24(金)の勉強会への参加が主目的だったのですがちょうど3代目 J SOUL BROTHERS のコンサートと日程が重なっていたせいか、宿泊所が割と予約できる場所が少なかったです。

f:id:okamuuu:20190602221317j:plain
セミダブルドミトリー

そんな中なぜかすんなり予約できたホステルに宿泊してきました。場所はちょっと繁華街からは離れた場所だったのですが、勉強会の会場まで歩ける距離だったので booking.com でなんとなく予約しました。そしてセミダブルドミトリー秘密基地風にに泊まることになりました。これは半個室のセミダブルベッドで、2名まで宿泊できるようです。私はそんな事知らずに普通に予約して一泊3000円程度でした。シングルベッドだったらもっと安かったのかもしれませんが、セミダブルだからすんなり予約できたんだと思います。

私以外の宿泊者はだいたい海外の人で、カップルだったりお友達同士で宿泊していたようです。あくまで私の感想ですが正直2名で泊まるとかなり狭いと思います...

f:id:okamuuu:20190524091019j:plain
1Fフロント前のラウンジ

ノマドスペースに関しては1Fと5Fが使えるのですが1Fのほうがお洒落だったので私はこちらを使っていました。立地に関しては少し繁華街から離れた場所なのでバスで天神まで移動したり、帰りはタクシーを使ったりしていました。

こちらの宿泊所に関してですが、私はそこまで不満を感じることはありませんでした。アニメティが有料だったり、トイレの数が少ないと感じたり、ちょっとおしいな、と感じる程度です。海外からの宿泊客や働いている人も若い人が多かったり、ちょっと非日常的な感覚を味わえたので割と満足しています。

とはいえ観光するには立地が少し不便だったのと、他にも気になる宿泊所がたくさんあるのでしばらく利用する機会はないのかも。でも今回のように勉強会で訪れる場合は割と穴場だと思います。

立ち飲み屋とご飯処

今回博多に来た主目的は勉強会への参加だったのですが、博多の立ち飲み屋を巡りたいという理由もありました。そんなわけで以下視察した立ち飲み屋です

  • ちょい立ち酒場 にどね
  • 峠の玄氣屋xMEGUSTA
  • 立ち呑みとうどん みのり
  • STAND BY ME
  • 立呑 okuto

ちょい立ち酒場 にどね

f:id:okamuuu:20190602221958j:plain

博多はドリンクもフードもかなり安いお店が多いです。その中でもかなり安い部類に入ると思いますが食べ物もお酒も美味しかったです。場所が少し天神から離れた場所なので分かりづらいのですがとてもいいお店でした。この日は遅い時間に訪れたのですがスタッフが一人で頑張っていました。たまたま今日は一人で切り盛りしていたそうですが、空いた時間に話しかけてくれたり、注文も調理も滞りなくこなしていたのでやたらと気配りできる人だなあと感心しました。そんなわけで良い印象が残ったのでまた博多に来たら行きたいと思います。

峠の玄氣屋xMEGUSTA

f:id:okamuuu:20190525234017j:plain

昼はおにぎり屋、夜は立ち飲み屋、「峠の玄氣屋」と「MEGUSTA(メグスタ)」がコレボレーションしたお店らしいです。ここで食べた鯖は美味しかったです。博多駅の筑紫口にあるのですが、便利な場所にあるのでおすすめします。

博多の中心地は天神ではあるものの、最近は博多駅周辺がかなり人気が高いらしく、近年マンションはたくさん立っているが博多駅周辺に住もうと思っても物件に空きが無い状態らしいです。そんな訳でたぶんこの近辺にはまだまだ美味しいお店がたくさんありそうです。

立ち呑みとうどん みのり

f:id:okamuuu:20190602222309j:plain

かなり混雑している時間帯に訪問したのですが、お店のスタッフがすごく気さくな人たちでした。常連さん同士も仲が良さそうで、みなさん盛り上がっていらっしゃいました。ちなみに場所は ちょい立ち酒場 にどね の近くです。

うどんは立ち飲み屋でしたけど上品な味でした。だしがかなり美味しかったです。うどん食べた感じだとおそらく他の料理も美味しいじゃないかと思います。他の立ち飲み屋にも行きたかったので長居しなかったのですが、またうどんを食べに行きたいです。

STAND BY ME

f:id:okamuuu:20190602222402j:plain

博多の人気ホステルが運営している BAR です。今まで博多に来たら必ず立ち寄っているお店です。立地がビジネス街にあるので繁華街から少し離れた場所にあるのですが、お洒落な雰囲気の立ち飲み屋が好きなので毎回来てうまかっちゃんを食べています。

STAND BY ME は宿泊してみたいと思いながら、いつも希望する日程に空きが無くて残念。利用したい方は早めの予約をおすすめします。

stand-by-me.jp

立呑 okuto

f:id:okamuuu:20190523213056j:plain

西中洲という中洲ではないスポットがあって、そこにちょっと良いお値段の飲食店がいくつもあるエリアがあるのですが、そこの立ち飲み屋です。私が訪れた時は割と静かで店内は落ち着いた雰囲気でした。寝る前に少しだけ、静かにお酒呑みたい、という人にはちょうどいいかもしれません。

その他のごはん処

  • 元祖博多めんたい重
  • 博多天ぷら たかお
  • 平四郎

f:id:okamuuu:20190602222740j:plain
インスタ映え抜群の明太子

f:id:okamuuu:20190602222922j:plain
午前中は3代目J Soul Brothers 目当てに博多に来たファンの方々がライブ前に多数いらっしゃったので夕方出直しました。

元祖博多めんたい重はインスタ映えする写真が撮れるのでそういうのがお好きな方にはおすすめです。味に関してはたしかにおいしいんですけど一度食べたらもういいかな、ぐらいでした。というのも博多ではめんたいこが無料で食べられる店がいくつかあるので明太子だけ食べたいっていう気分にそこまでならないです。でもこのめんたい重はみんなのリアクションいいので SNS 好きな方はぜひ。ちなみに午前中は長蛇の列でしたが夕方は並ばなくてもすぐに入店できました。まあ日によるのでいつ食べに行くのが空いているのかはわかっていません。

f:id:okamuuu:20190602223331j:plain

博多天ぷら たかおは私がお気に入りの天ぷら屋さんで、揚げたてのてんぷらをお寿司やさんのように小出ししてくれるスタイルのお店です。キャナルシティにあって、今回勉強会の会場を提供してくれた株式会社ベガコーポーレーションの近所にあります。

f:id:okamuuu:20190523164001j:plain

平四郎はいわゆる回転寿司なのですが割と手頃な値段で美味しかったです。今回宿泊所がキャナルシティから徒歩でいける距離だったのでたかおと平四郎はよく利用させていただきました。

まとめ

というわけで博多で飲み歩きを楽しんできました。今回宿泊した施設は飲み屋さんからは遠いのですが博多で勉強会があるときには意外と重宝するかもしれません。博多には他にも泊まってみたいホステルや行ってみたい飲み屋さんがあるのでまた訪れたいと思います。

福岡の勉強会「俺の話を聞け!!LT大会 #14」に参加してきた

f:id:okamuuu:20190529122312j:plain

というわけで福岡に行ってきました。

https://cdg.connpass.com/event/129574/

経緯

福岡の勉強会を探していたところ、mya-ake 氏が主催する勉強会を見つけました。Nuxt.js あたりで彼の書いた記事に助けられた事があったので興味を持ったので参加してきました。

https://mya-ake.com/

勉強会の雰囲気について

私が普段都内で参加している勉強会と基本的には大きな差は感じませんでした。といっても私が参加している勉強会はだいたい酒を飲みながらスタイルばかりなのでもしかしたら参考になってないかもしれません。終始リラックスした雰囲気で途中で話してもOKみたいな雰囲気だったので、たぶん「俺にも何か喋らせてくれてれる勉強会が好きだ」という人には向いている勉強会だと思います。

ちなみに発表内容はどれもかなり良かったので一度福岡に行ってみると良いと思います。ごはんも美味しいですし。

会場の提供

株式会社ベガコーポレーション様が提供してくれました。ありがとうございます。この勉強会にも多数同社のエンジニアが参加してました。家具のEコマースで Vue.js を使っているそうです。

LT順番について

司会者が「次のLTやりたい人」と言ったあと最初に手を上げた人から順番にLTするシステムでした。途中何回か手をあげたのですがいつも出遅れてしまいだんだん面倒になってきたので結局順番は最後になりました。

ちなみにこの方式、初参戦の私にとって「今話している人誰?」状態になってしまいました。というわけで登壇者とタイトルが間違っている可能性が高いです。というかいくつか間違っているのと話の内容を思い出せない人が何人かいる...

LT

OSSのあれこれ話すイベントやりたいと思った チャンカツ

ベガコーポレーションのフロントエンド。イベントに関わっていると色々な知見があるので共有したい、という話だった気がします。

https://speakerdeck.com/tyankatsu/ossfalsearekorehua-suibentoyaritaitosi-tuta

gatsby + netlify でサイトを作った話

ベガコーポレーションの人。ゆるきゃんの話が好きだそうです。Gatsby.js と Netlify は便利ですよね。私も最近まで使っていました。

福岡でガチエンジニアがブートキャンプをはじめる話

GMOペパポの人。スプラトゥーンが好きだそうです。大名エンジニアカレッジを始めたそうです。

https://speakerdeck.com/udzura/bootcamp-by-engineer-for-engineer https://daimyo-college.pepabo.com

Fukuoka Growth Nextの話 or postalk.ioの話 新5 Fukuoka Growth Next

最近福岡に帰ってきたそうです。カード型チャットサービスの postalk.io で複雑な話し合いをわかりやすくしたい。あと Fukuoka Growth Next で 6/1 にイベントやるそうです。

https://growth-next.com/ja/event/14669

海の住所の話

umeebe の人。地球の7割は海なんだけどその海には住所が割り当てられてないんだけどそれが我々の作っているサービスにおいて問題がおきるので解決を図ったお話。

GANった話 10 Kenta K. Tanaka

ベガコーポレーションのエンジニアさん。姿を消すマント = 物体検出 + GAN(ギャン) というお話です。多分デモ画面見ないと伝わらないと思いますがコマ送りで写っている自分の姿をけすことに成功(?)していました。まあ完全に消えていないので「ここになんかいるよね」というのはバレるんでしょうけど。

AbemaTVのコメビュを作る話

AbemaTV のソースコードを読むと色々仕掛けがあって楽しいそうです。そして福岡新着ITイベント のボットを作ったそうです。

https://speakerdeck.com/loftkun/abematvfalsekomentobiyuawozuo-ruhua

セキュリティ対策してますか?

とある会社の情報システムで働いているそうです。セキュリティで起きる問題、原因は主に外部よりも内部。人はミスする、悪いことを考える、ゆえに100%の対策は無理。テクニカルじゃないほうのセキュリティ対策しましょう。とのことでした。

イラスト普及

就活生がエンジニア向けにイラストの書き方についてふんわりと説明。最初はペンタブで大丈夫 。ウォームアップすると線がきれいにかけるそうです。30秒ドローイングで練習しましょう。 水晶は塗りの基礎。だそうです。

動画でドローイングの様子を再生したのを見せてもらいましたが思った以上に手間かかってるのでイラストレーターってすごいんだな、と思いました。

けんてぃが考えるWebサイトの品質

品質の定義は色々あるけど基礎価値と付加価値を考えた場合、基礎価値はコンテンツとアクセシビリティでは?建物の価値観に通ずるものがあるのではないかとのこと。そして転職活動中だそうです。なぜかLT終わったあとベガコーポレーションのエンジニア達からプチ面接が行われました。

showtime のご紹介 TakeruNarita

思い立って一週間でスライド共有サービスを作った話。実は今回東京から私以外のエンジニアが来ていました。お話を聞いたところ「どうしても話をしたかったが直近のイベントで枠がなかったから新幹線で来た」そうです。

インフラエンジニアがジョブチェンジした話

東京から福岡へ。スタートアップはスピードと結果が大事。話を聞いている限りジョブチェンジというよりもフルスタック化している気がしました。

英単語を覚えたいので単語帳をつくりました。サイト名がまだ決まっていません。

https://flash-cards-marvelous.firebaseapp.com/

というわけで

初めて地方の勉強会に参加してきました。都内の勉強会と全く遜色のないイベントで発表内容もとても面白かったです。福岡でも東京と同じ様に頻繁に勉強会が行われているので時々地方に出向いて勉強会に参加すると楽しいと思います。

というわけで博多はご飯もお酒も美味しかったのでまた勉強をしに行きたいと思います。

Vue.js で Carousel な component を再開発して色々な学びを得たお話

f:id:okamuuu:20190504160726j:plain

あらすじ

Vue.js でシンプルな Carousel を作ります。すでにそういった component は一杯あるのですが、車輪の再開発を通じてお勉強しました、という趣旨の記事です。すでに vue-carousel という定番モジュールがあるのでこれを真似て作成しています。

Just Do it

以下のようにファイルを作成します。

mkdir vue-tiny-carousel && cd $_
mkdir src example
touch src/{index.js,Carousel.js,Slide.vue, Navigation.vue}
touch example/App.vue
yarn init -y
yarn add lodash.debounce
yarn add -D @vue/cli

touch したファイルを以下のように修正します。

src/index.js

import Carousel from "./Carousel.vue";
import Slide from "./Slide.vue";

const install = Vue => {
  Vue.component("carousel", Carousel);
  Vue.component("slide", Slide);
};

export default {
  install
};

export { Carousel, Slide };

src/Carousel.Vue

<template>
  <div class="carousel">
    <div ref="carousel-wrapper" class="carousel-wrapper">
      <div
        ref="carousel-inner"
        class="carousel-inner"
        :style="{
          'transform': `translate(${offset}px, 0)`,
          'transition': dragging ? 'none' : `0.4s ease transform`,
          'flex-basis': `${carouselWidth}px`,
        }"
      >
        <slot></slot>
      </div>
    </div>

    <navigation
      :clickTargetSize=8
      :nextLabel="`>`"
      :prevLabel="`<`"
      @navigationclick="handleNavigation"
    />

  </div>
</template>

<script>
/* eslint-disable no-console  */
import debounce from "lodash.debounce"
import Navigation from "./Navigation.vue";
export default {
  name: "carousel",
  components: {
    Navigation
  },
  mounted() {
    window.addEventListener(
      "resize",
      debounce(this.onResize, this.refreshRate)
    );
    this.setSlideCount()
    this.computeCarouselWidth();
  },
  beforeUpdate() {
    // this.computeCarouselWidth();
  },
  provide() {
    return {
      carousel: this
    };
  },
  data() {
    return {
      slideCount: 0,
      currentPage: 1,
      carouselWidth: 0,
      dragging: false,
      refreshRate: 16,
    };
  },
  computed: {
    offset() {
      return this.carouselWidth * (this.currentPage - 1) * -1
    },
    canAdvanceForward() {
      return this.slideCount - this.currentPage > 0
    },
    canAdvanceBackward() {
      return this.currentPage > 1;
    }
  },
  methods: {
    setSlideCount() {
      const slots = this.$slots
      const tagName = "slide"
      const isSlide = slot => slot.tag && slot.tag.match(`^vue-component-\\d+-${tagName}$`) !== null
      this.slideCount = (slots && slots.default && slots.default.filter(isSlide)).length || 0
    },
    computeCarouselWidth() {
      let carouselInnerElements = this.$el.getElementsByClassName("carousel-inner");
      this.carouselWidth = carouselInnerElements[0].clientWidth || 0;
    },
    handleNavigation(direction) {
      if (direction && direction === "backward") {
        this.goPreviousPage()
      } else if (direction && direction === "forward") {
        this.goNextPage()
      }
    },
    onResize() {
      this.computeCarouselWidth();
      this.dragging = true; // force a dragging to disable animation

      // clear dragging after refresh rate
      const refreshRate = 16
      setTimeout(() => {
        this.dragging = false;
      }, refreshRate);
    },
    goToPage(page) {
      this.currentPage = page
    },
    getNextPage() {
      return this.currentPage < this.slideCount ? this.currentPage + 1 : this.currentPage;
    },
    getPreviousPage() {
      return this.currentPage > 1 ? this.currentPage - 1 : this.currentPage
    },
    goNextPage() {
      this.goToPage(this.getNextPage())
    },
    goPreviousPage() {
      this.goToPage(this.getPreviousPage())
    }
  }
}
</script>

<style scoped>
.carousel {
  height: 100%;
  display: flex;
  flex-direction: column;
  position: relative;
}
.carousel-wrapper {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
}
.carousel-inner {
  height: 100%;
  display: flex;
  flex-direction: row;
  backface-visibility: hidden;
}
</style>

src/Slide.Vue

<template>
    <div class="slide">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "slide",
}
</script>

<style scoped>
.slide {
  flex-basis: inherit;
  flex-grow: 0;
  flex-shrink: 0;
  user-select: none;
  backface-visibility: hidden;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  outline: none;
}
</style>

src/Navigation.Vue

<template>
  <div class="carousel-navigation">
    <button
      type="button"
      aria-label="Previous page"
      class="carousel-navigation-button carousel-navigation-prev"
      v-on:click.prevent="triggerPageAdvance('backward')"
      v-bind:class="{ 'carousel-navigation--disabled': !canAdvanceBackward }"
      v-bind:style="`padding: ${clickTargetSize}px; margin-right: -${clickTargetSize}px;`"
      v-html="prevLabel"></button>
    <button
      type="button"
      aria-label="Next page"
      class="carousel-navigation-button carousel-navigation-next"
      v-on:click.prevent="triggerPageAdvance('forward')"
      v-bind:class="{ 'carousel-navigation--disabled': !canAdvanceForward }"
      v-bind:style="`padding: ${clickTargetSize}px; margin-left: -${clickTargetSize}px;`"
      v-html="nextLabel"></button>
  </div>
</template>

<script>
/* eslint-disable no-console  */
export default {
  name: "navigation",
  inject: ["carousel"],
  props: {
    clickTargetSize: {
      type: Number,
      default: 8
    },
    nextLabel: {
      type: String,
      default: "&#9654"
    },
    prevLabel: {
      type: String,
      default: "&#9664"
    }
  },
  computed: {
    canAdvanceForward() {
      return this.carousel.canAdvanceForward || false;
    },
    canAdvanceBackward() {
      return this.carousel.canAdvanceBackward || false;
    }
  },
  methods: {
    triggerPageAdvance(direction) {
      this.$emit("navigationclick", direction);
    }
  }
};
</script>

<style scoped>
.carousel-navigation-button {
  position: absolute;
  top: 50%;
  box-sizing: border-box;
  color: #000;
  text-decoration: none;
  appearance: none;
  border: none;
  background-color: transparent;
  padding: 0;
  cursor: pointer;
  outline: none;
}
.carousel-navigation-button:focus {
  outline: 1px solid lightblue;
}
.carousel-navigation-next {
  right: 45px ;
  font-size: 20px;
  transform: translateY(-50%) translateX(100%);
  font-family: "system";
}
.carousel-navigation-prev {
  left: 45px;
  font-size: 20px;
  transform: translateY(-50%) translateX(-100%);
  font-family: "system";
}
.carousel-navigation--disabled {
  opacity: 0.2;
  cursor: default;
}
/* Define the "system" font family */
@font-face {
  font-family: system;
  font-style: normal;
  font-weight: 300;
  src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"),
    local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Symbol"),
    local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
</style>

example/App.vue

<template>
  <div style="background: #eee;">
    <Carousel style="width:100%; height: 80vh;">
      <Slide>
        <div class="sample bg-primary">
          <h1>slide 1</h1>
          <p>test test test</p>
        </div>
      </Slide>
      <Slide>
        <div class="sample bg-warning">
          <h1>slide 2</h1>
          <p>test test test</p>
        </div>
      </Slide>
      <Slide>
        <div class="sample bg-danger">
          <h1>slide 3</h1>
          <p>test test test</p>
        </div>
      </Slide>
    </Carousel>
  </div>
</template>

<script>
import { Carousel, Slide } from '../src/index';
export default {
  components: {
    Carousel,
    Slide
  }
}
</script>

<style>
.sample {
  box-sizing: border-box;
  margin: 0;
  padding: 30px 60px;
  height: 100%;
}
.bg-primary { background: hsl(171, 100%, 41%) }
.bg-warning { background: hsl( 48, 100%, 67%) }
.bg-danger  { background: hsl(348, 100%, 61%) }
h1 {
  margin: 0;
  padding: 0;
}
</style>

動作確認

yarn vue serve example/App.vue で動作確認をします。

解説

この Carousel では以下の事を行なっています。おそらく世の中に出回っている Carousel と同じ挙動だと思います。

  • 画面に Carousel を配備する
  • Carousel の内部に Slide を横一列に並べる
  • Slide ひとつの横幅は親要素である Carousel と同じサイズにする
  • 横一列に並んだ Slide は Carousel に対して Slide の数だけ大きくはみ出す。
  • はみ出した部分は非表示にする
  • 次のスライドに遷移させた場合はスライドを一つ分左にずらす

ただし、この vue-tiny-carousel はあくまで初学者向けの教材として利用することを目的としているので細かい調整を行えるように設計していません*1。あらかじめご了承ください。

setSlideCount()

Carousel の中に Slide を複数配備して、その Slide の数をカウントします。この数字を元に offset の数などを計算します。Slide の tagName が vue-component-2-slide という形式なので、それにマッチしたものを Slide だとカウントします。

setSlideCount() {
  const slots = this.$slots
  const tagName = "slide"
  const isSlide = slot => slot.tag && slot.tag.match(`^vue-component-\\d+-${tagName}$`) !== null
  this.slideCount = (slots && slots.default && slots.default.filter(isSlide)).length || 0
}

computeCarouselWidth

ルーセルの表示領域を計算します。window がリサイズされた時にも再計算が必要なので関数化しています。

computeCarouselWidth() {
  let carouselInnerElements = this.$el.getElementsByClassName("carousel-inner");
  this.carouselWidth = carouselInnerElements[0].clientWidth || 0;
}

lodash.debounce

リサイズのイベントはこまめに発生するので lodash.debounce を使って関数の実行を抑制します。以下の記事に詳しい解説が載っていたのでこちらも参考にしてみてください。

https://qiita.com/waterada/items/986660d31bc107dbd91c

provide と inject

parent component である Carousel を、child component である Navigation で使いたい、というケースで便利な機構です。こちらも以下の記事に詳しい解説が載っていたのでこちらも参考にしてみてください。

https://qiita.com/kaorun343/items/397b1fa6afe96fa2b30f

まとめ

というわけで世の中には Carousel のライブラリが多数あるにも関わらず、車輪の再開発を行ってみました。再開発といっても同等の機能を求めた訳でなく、あくまで学習目的なので、厳密には車輪の再開発ではないと思いますが。

私はすでに Vue.js や Nuxt.js でやりたい事ができるような状態ではあったのですが、それでもまだ知らなかった便利な機構が存在したので学べた事があってとても有意義な時間となりました。というわけで車輪の一部を再開発すると他の優れたエンジニアの叡智を学ぶ事ができてなかなか良い経験でした。

今回作成したソースコードは以下に置いてあります。実用的かどうかはさておき、学習する分には適度な量のソースコードだと思いますのでよかったらご覧になってください。

https://github.com/okamuuu/vue-tiny-carousel/

*1:アラビア語のような Right to left に未対応だったり細かいデザインの調整をする props を用意していません