未経験者が実務案件に挑戦し、エンジニアとしての心構えを学ぶ

# はじめに
エンジニアを目指した未経験者が、実務案件に挑戦し、もがき奮闘しながらもやり抜いた結果、エンジニアとしての心構えを身につけることができた話をしていきます。

実務案件は3セクションに分かれており、課題、考え、行ったことをそれぞれのセクションでお伝えできればと思います。長いですがお付き合いくださいませ。

# 筆者について
工場生産ラインの管理業務に携わりながら、エンジニアへの転向を目指している29歳です。
管理業務を効率化するためExcel VBAを独学で始め、プログラミングにのめり込んでいきました。

21年6月より本気でエンジニアを目指して学習を始め、縁あって10月より実務案件に参画させて頂くことになりました。

- 実務案件までに学習してきた内容
 - CS => Linux => Progate => Railsチュートリアル => Docker => AWS

# 実務案件って...？
現役エンジニアであるY氏経由で、とあるクライアントから医院紹介サイト（Rails）のSEOを改善させたいという案件を頂きました。
本案件にはY氏の伴走付きで、私含め3名の未経験者が2ヶ月間取り組みました。

- 実務での取り組み
 1. SEO改善に関するクライアントへの施策提案（2week）
 2. 都道府県選択ページにおける都道府県全国表示対応 （2week）
 3. Googleスプレッドシート上の口コミデータをDBへ自動インポートする仕組みを構築（4week）

# 実務案件を遂行する環境
完全オンラインで実施します。

- メンバーとは基本Slackでやり取りし、定期的にGoogle Meetで情報共有
- クライアントとは会うことも会話することもできず、SlackもしくはGoogleスプレッドシートなどを使ったテキストコミュニケーションのみ
- 開発は以下フローでGitHubを介して納品まで遂行
 1. ローカルで開発
 2. メンバー、Y氏のレビュー 
 3. クライアントのレビュー
 4. 納品

# 1. SEO改善に関するクライアントへの施策提案
早速実務がスタートします。最初にクライアントに頂いたお仕事は、SEO改善の施策提案です。
## 概要
お題は「このサイトのSEOを改善させるために、1週間後に各々施策を10個以上考えてきてくれ」という、なんともざっくりしたものです。
サイトURLとGitHubへの招待、いくつかのアカウント情報を頂き、このお題がメンバーに課せられました。

基本Cloud9のみで学習してきた筆者はまずローカル環境構築で2日ほど奮闘したのですが、ここでは割愛します。
SEOとはなんぞやだし、わからないことがわからない状態のため、関連情報の学習から始まり有効な施策を模索していきました。

## 学習の進め方
SEOでググってみると、Googleの検索順位を上げるWebマーケティング手法の1つということがわかりました。
マーケティングも単語でしか聞いたことがない筆者はマーケティングの学習から始め、SEO対策と進めていきます。

学習を進めるとわかるのですが、このWeb業界は専門用語が多すぎて、知らないことが次から次へと出てきます。
Web検索は概要を学ぶのに留めて詳細は書籍で学び、そして忘れないように自分なりにまとめ、限られた時間で学習を進めていきました。

その上で、本番サイト、ソースコード、サーチコンソール、アナリティクスを分析し、学んだことがサイトに反映できるよう施策検討を進めました。


- 学んだこと
 - マーケティング概要
 - SEO内部対策全般
 - 内部対策に有効なGem
 - コンテンツ充実化も重要なSEO施策の1つであること
 - サーチコンソール、アナリティクスでの分析方法


## 施策提案
なんとか14件の施策を列挙することができました。
一部検討不足でクライアントに提案できる状態でなかったこともあり、内8件をメンバー・Y氏より承認をうけクライアントに提出します。

- 承認頂いた施策提案例
 - titleタグの見直し
 - 画像途切れバグ修正
 - サーチコンソールへのサイトマップ登録
 - meta discriptionの設定
 - トップページのh2タグの強調

## 具体性とインパクトの重要性を学ぶ
施策案提出後、クライアントから以下フィードバックを受け、改めて検討不足を痛感しました。
施策案の数にこだわりすぎて中身が薄く、まさにWHATが欠けている状態でした。

- 全体感がほしい
 - サイトの目的・課題・流入元を把握しビジネスの全体感を抑えた上で提案すること

- 数字がほしい
 - 現状の課題と施策改善によるインパクトを数字を持って説明する。数字がないと判断がつかない

- 具体性がほしい
 - HOWよりWHATがほしい。例えばタイトルの見直しなどでは、ページ種類ごとに課題・改善案を提示すること

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/696578c4-f581-6f07-ec8a-18cbbfdcf574.png)
図：改善施策とクライアントのフィードバック

## 改善後の施策提案
フィードバックを元に、インパクトを考慮しサイトマップ改善の施策についてブラッシュアップしました。
全体感把握のために別のスプレッドシートを用意し、サイトの状況と改善案をまとめることで、全体感と具体性を持たせました。

今回のように、__WHAT（何を）を明確する__ことによって、テキストコミュニケーションであっても比較的__話がスムーズに進む__ことを実感しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/ce49566d-5f97-c7c4-a4b8-c50b0cf4ee9b.png)
図：具体性を持たせた改善施策

# 2. 都道府県選択ページにおける都道府県全国表示対応
いよいよ実装フェーズに入ります。
施策提案を進める中で都道府県リンクを全国表示させたいニーズがあることを確認しておりました。
コンテンツ増量によるSEO改善が図れ、また施策を通してMVCのコードに触れることができると考え、本施策を受けさせて頂くことにしました。

## 課題
都道府県選択ページにおいて、現状3つ（東京、神奈川、埼玉）のみ選択できるようになっています。
都道府県ごとの医院数も増えてきたため、全都道府県を表示できるようにしたいとのことでした。

![prefectures.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/9f4ecfec-b9c2-0b9b-8bf4-150a5058eab2.png)

## 最初に実装した都道府県リンク表示コード
slim記法にはちょっと手間取ったけど、全国の都道府県リンクを表示させるのなんて簡単簡単♪

```apps/controllers/clinics_controller.rb
class ClinicsController < ApplicationController
 def index
  .
  .
  prefectures = Prefecture.all
  @hottohoku  = prefectures.where(id: 1..7)
  @kanto      = prefectures.where(id: 8..14)
  @tyubu      = prefectures.where(id: 15..24)
  @kansai     = prefectures.where(id: 25..30)
  @tyushikoku = prefectures.where(id: 31..39)
  @kyushunawa = prefectures.where(id: 40..47)
 end
 .
 . 
end
```
```app/views/clinics/index.html.slim 
# 以下のようなコードを６エリア分書く

input#acd-check5.acd-check type="checkbox"
label.acd-label for="acd-check5"関東
.acd-content
  ul
    - @kanto.each do |pref|
      li
        = link_to pref.name, prefecture_path(pref)
```
Y氏レビュー
>SQLが無駄に発行されているので削減してね
エリア情報ってマッピングさせた方がいいんじゃないかな
あ、マッピングっていってもクラスの責務は考えてね
Viewのコードが冗長なのでまとめようね

筆者
「ほえっ？SQLが無駄に？マッピング？せきむ〜〜？」

## 色々な壁にぶち当たり、学ぶ
すぐに通るだろうと踏んでいたレビューでは数多くの指摘を受け、受けた内容を1つ1つ調べて理解していくしかありませんでした。
しかしながら、メソッドによるSQL発行数の違いや、単一責任の原則など多くの学びがありました。

### 使用するメソッドによるSQL発行数（挙動）の違い
どれだけSQL数が発行されているか実際にサーバーログを確認したところ、改善前は6つ、改善後は1つであることがわかりました。
色々疑問が出てきて、深掘りしていくと以下のようにメソッドの違いよって挙動が変わってくることがわかります。

- メソッドによる返り値とSQL発行有無の違い
 - エリアごとの都道府県情報を取得するにあたって、改善前はActiveRecordの`where`、改善後はRubyの`select`を使用
 - `where`の返り値は`all`と一緒でActiveRecord::Relationのオブジェクトとなり、SQLを発行する
 - 一方`select`はブロックを渡すとPrefectureクラスのインスタンスが返り値となり、もちろんSQLを発行しない（引数がブロックでないActiveRecordの`select`もあるので注意）

- SQL文の評価タイミングとSQL発行数
 - ActiveRecord::RelationはSQL文を保持し、データが必要なタイミングでSQLを発行する（遅延評価）
 - よって`where`で取得した`@エリア名`はビューにてSQL文が評価され、計6つのSQLが発行される。
 - `all`で取得した`prefectures`はSQL文を保持したままで評価されない
 - 改善後は`prefecutres`がコントローラ内で`select`されるタイミングでSQLが発行され、SQL数は1となる。

今回のように疑問に対して深堀り、挙動を確認していくことがメソッドの理解を深めるのに有効で、何より__調べていて楽しい__と思える瞬間でした。
この挙動を確認すると言う行為は後のタスクでも重要になっていきます。

### 単一責任の原則
一言で言うと「モジュールはたった一つのアクターに対して責務を負うべきである」という原則です。
主にクラス設計で重要になりますが、それ以外にも1つのメソッドに複数の機能を持たせないなどプログラミング全般に通じる原則になります。

最初は、後述する`AREA_IDS`（エリア名と都道県idの範囲とのマッピングデータ）を`app/controllers/clinics_controller.rb`に配置していました。
Y氏の指摘もあり、このマッピングデータはどこが責任を負うべきか考えます。
最終的にコントローラでもビューでもなく、Clinicモデルでもない、Prefectureモデルが適切な配置場所だと理解できるようになりました。

この単一責任の原則は、後のタスクでメソッドを分割する際、責任の範囲を定めるのにも役に立つことになります。

## 納品時のコード
前項での指摘の他、メソッドの本質を表した名前に変更するなどして、クライアントまでApproveを頂き、以下コードを納品することができました。

```app/controllers/clinics_controller.rb
class ClinicsController < ApplicationController
  def index
    .
    .
    prefectures = Prefecture.all
    @area_prefs = group_prefectures_by_areas(Prefecture::AREA_IDS.dup, prefectures)        
  end

  private

  def group_prefectures_by_areas(areas, prefs)
    areas.each do |area_name, pref_id_range|
      areas[area_name] = prefs.select { |pref| pref_id_range.to_a.include?(pref.id) }
    end
  end
  .
  .
end
```
```app/models/prefecture.rb
class Prefecture < ApplicationRecord
  has_many :cities
  AREA_IDS = {
    関東:       8..14,
    北海道・東北: 1..7,
    中部:       15..23,
    近畿:       24..30,
    中国・四国:  31..39,
    九州・沖縄:  40..47,
  }.freeze
end
```
```app/views/clinics/index.html.slim 
- @area_prefs.each do |area_name, area_prefs|
  .search-modal__content-title
    p = area_name
  ul
    - area_prefs.each do |pref|
      li
        = link_to pref.name, prefecture_path(pref)
```
# 3. Googleスプレッドシート上の口コミデータをDBへ自動インポートする仕組みを構築
開発手法にも慣れてきたところで、少し大きめのタスクについて依頼を受けます。
いくつか依頼内容があったのですが、SEO改善と共にサービスの連携を経験できそうで面白そうと、本施策にジョインさせて頂きました。
## 課題
クライアントが抱える課題は以下です。

 - 現状サイトの口コミ投稿数は少ない。実はTypeformから口コミを収集していて、データたくさんあるが取り込む作業が煩雑でDBにインポートできていない状況。

口コミという価値あるコンテンツが、作業が煩雑だから取り込めていないというのは非常にもったいないと感じ、改善したいと思うようになりました。

## 現状確認と認識合わせ
威勢よく受けた割に今回も何も知らないことだらけです。これまでの反省を踏まえて、関連する情報を調べた上でクライアントと綿密に認識のすり合わせを行います。

### Typeformとは
フォーム作成サービスです。
以下特徴があり、Railsで実装するよりも低コストで、アンケートの作成から回答、集計まで実施が可能です。

- 一問一答形式のフォームを作成できる
- テキストだけでなく画像による選択肢も設置できる
- プログラミングの知識がない方でも簡単にフォームが作れる
- GoogleスプレッドシートやSlackなどさまざまな外部ツールと連携できる

実際にTypeformに登録して機能を確認してみます。どうやらGoogleスプレッドシートにデータ出力することで、データ操作が楽になりそうです。

### 現状確認
ソースコードを確認するとcsvファイルをインポートするThorタスクが組み込まれていることが確認できます。
それ以外のソースコードから読み取れないところは、クライアントに確認を取るしかありません。


Typeformで収集しているデータをどこに蓄積しているのか、現状どうインポート作業を行なっているのか、__仮説を元に確認を行い__、認識を擦り合わせました。

- 現状のインポートまでの流れ
 1. ユーザーがTypeform内で口コミを投稿する
 2. TypeformからGoogleスプレッドシートへデータが自動転送される
 3. クライアントが、Googleスプレッドシートをcsvとしてダウンロードする
 4. ユーザーに紐づく、ニックネームなどのユーザー詳細データとなるcsvを手動作成する
 5. クライアントが、dbに各種csvデータをインポートし、口コミがサイトに反映される

![before_composition.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/51b3bf10-6ca4-8267-270a-e890a5428809.png)

### 抽象化とゴールの確認
前項の3〜5の工程が改善対象であることが判明しましたが、最初はcsvをダウンロードして、アップロードする作業をどう自動化しようかと考えていました。
しかしながら、構想段階でも設計が煩雑になることがわかります。

そこで今回の課題を抽象化してみると、「Googleスプレッドシート上データをDBに取り込みたい」となります。
上記ベースで調査を進めると、Rubyでスプレッドシートを直接取得しDBへ格納することができそうです。
抽象化によって、提供するソリューションの__ゴールイメージ__を確認することができました。

## 実装方針策定
現状とゴールの確認がとれたところで、本施策はボリュームが大きくなることがわかります。
事前に実装方針を立ててクライアントに承認をとり、実装に移る流れを取りました。

### 要件定義
クライアントの困りごとを解決するためには、手動操作をなくして全て自動でインポート！となりそうですが、全て自動化してしまうと適当な口コミデータまでインポートしてしまう恐れがあり、以下のように要件を定義しました。

- 投稿された口コミデータにクライアントが手動で掲載可否のフラグを付与できること
- 掲載可のフラグを付したデータのみが自動でDBに格納されること
- データのインポートは週に一度自動で行うこと
- DB内の口コミデータの更新/削除はスコープ外とする

### 策定した実装方針
要件を満たすように実装方針を定めます。
特にデータを収集しているGoogleスプレッドシートは慎重に操作しないといけないと考え、サンプルシートを提供いただき、データ構造を確認した上で方針を検討してきました。

成果物の認識を合わせるため、ここでもクライアントと綿密に調整させていただき、方針の承認を得ました。

- データのインポート対象について
 - スプレッドシート内でDBに存在しておらず、かつ掲載可のフラグが立っているレコードのみ（以下口コミレコード）を対象とし、reviewsテーブルにインポートする。
 - DBとスプレッドシート間のレコードの同一性チェックは、reviewsにtokenカラムを追加して、DBのtokenとスプレッドシート内のtokenとを照合することで行う。
 - スプレッドシートの各レコードがDBに保存済みであることを確認するため、スプレッドシートに新たな列を設け、DBへ保存が成功した段階で、保存したレコードに対応するスプレッドシートレコードに日付を記載する。

- 掲載可否のフラグ付与の方法について
 - スプレッドシート内に専用の列を用意し手動で行えるようにする

- スプレッドシートのデータ取得について
 - 口コミレコードはgoogle-drive-rubyで操作する（詳細は後述）

- データの変換とユーザー詳細データ作成について
 - スプレッドシートとDB間とでデータ型が合わないカラムデータはインポート前にデータ変換する
 - 不規則な値が予測され、システム変換が困難な見積額はクライアントの手入力とする
 - 口コミレコードからユーザー詳細データを抽出し専用テーブルにインポートする

- データのインポートの実行方法について
 - Thorで口コミレコードをインポートするタスクを定義する
 - 取り込むデータに１つでも無効なデータが含まれている場合は、全ての口コミレコードをロールバックさせる。
 - インポートタスクをHeroku schedulerで定期的に実行する

![after_composition.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/dde55218-19aa-2bff-76a8-d89b01bb02e6.png)

## サブタスク分解とプルリク
サブタスクとして大きく3つに分解します。
実装に関しては主要のサブタスクごとにGitHub上もブランチを切り、プルリクを出す形を取りました。

- 口コミインポートタスク
 - レコード取得タスク　
 - レコードインポートタスク
 - Heroku scheduler設定タスク（設定ドキュメントを提供）
![branch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/0044690e-7ab7-1d91-5d15-aa0aea726dcc.png)

## 〜実装準備〜　Googleスプレッドシート上データの操作
まずはGoogleスプレッドシート上のデータをRubyで操作できないことには話が始まりません。
google-drive-rubyの公式ドキュメントや関連記事を確認することになりますが、日本語のドキュメントはまだまだ少ないです。
実際に操作できるようになるまで苦労しましたが、英語を勉強していてよかった...。

### google-drive-rubyとは
Googleドライブ内のファイルやスプレッドシートを読み書きするRubyのGemです。
このGemはサードパーティであり、Google公式のgoogle-api-ruby-clientというGemもあります。
２つを比較すると前者はシンプルにスプレッドシートを操作することができ、後者は認証に関わるコードが複雑で直感的でなかったことから、google-drive-rubyを使用することにしました。

Gemを使用するためには別途Google Cloud PlatformでのAPI設定と認証アカウントが必要になります。
アカウントデータはセキュアに取り扱えるようRailsのCredentialsを使用しました。

### スプレッドシート内データ取得の動作を確認する
Rubyでコードを書き、スプレッドシートのデータを取得してみます。

```
# スプレッドシート内データ「hellow world!」の取得確認
$ bundle exec thor import_reviews:fetch
hello world!
```


.....よし！動いた！
実際にデータを取得できたときは、まさにサービスの連携を体感した瞬間でした。


### API設定手順書の作成
GoogleのUIはよく変更があるのか参考サイトもわかりづらいです。クライアントがすぐにAPI設定できるよう、手順書を作成し提供するようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/9f7349a4-8497-0f3b-6c38-4b7e7045db56.png)
図：設定手順書（抜粋）

## 〜実装1〜 口コミレコードの取得
実装方針である、「DBに存在しておらずかつ掲載可のフラグが立っているレコード」を取得できるよう実装します。

### 実装開始
要件通り動いた！と自信を持ってレビュー依頼を出すも..。


レビューにてY氏
>追加したtokenカラムにはNOT NULL制約をかけてね
追加したtokenカラムには一意性制約もかけてね
既存レコードのtokenカラムにdefaultで適当な値が入るようにしてね
ここでやっていることってpluckで十分じゃないです？ などなどなど...

筆者
「ほえっ？既存レコードにdefalut値〜？　pluck〜？　まあ、調べてみるか...」

### 1つ1つクリアできるようになる
これまでドキュメントアレルギー予備軍（笑）だったのですが、読んでいると段々と読めるようになるものです。
RubyドキュメントやRailsガイドを読んでその通りにやれば解決のスピードも速いし、__実装が一層楽しく__なってきました。
ここで学んだことをご紹介します。

#### NOT NULLなtokenへ、ユニークなdefault値付与
制約の多いtokenカラムを追加するのは一苦労でした。

 - tokenカラムを追加するのと同時に、既存レコードのtokenにユニークな値を入れる必要がある
 - tokenに値が入ったらデフォルト値の付与は不要になる

上記を満たすために次のコードで対応します。
ランダムデータを生成するためにPostgreSQLの関数を使用できることを学びました

```db/migrate/202111*****_add_token_to_reviews.rb
class AddTokenToReviews< ActiveRecord::Migration[6.1]
  def change
    add_column :reviews, :token, :string, null: false, limit: 100, default: -> { "md5(clock_timestamp()::text)" }
    add_index :reviews, :token, unique: true
    change_column_default :reviews, :token, from: -> { "md5(clock_timestamp()::text)" }, to: nil
  end
end
```

#### 便利メソッドは公式にて
既存のtokenを取得するために次のように書いていました。

```ruby
exist_tokens = Review.all.map { |report| report[:token] }
```

今回のように、あるカラムのデータをとってきたいというニーズはよくあるものだと思います。
よくあるニーズには、便利メソッドが用意されています。

```ruby
# tokenの取得にRailsのpluckを使用
exist_tokens = Review.pluck(:token)
```


冗長だな〜、なんかいい方法ありそうだな〜と思ったら、RubyやRailsの公式ドキュメントを覗きに行くようになりました。

#### より良いコードへブラッシュアップ
結局Y氏より30以上のご指摘を受けましたが、何度も食らいつき修正を重ね、ついにapproveを頂きます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/90639ac0-f52f-4db8-e4d2-d720f79dc5c9.png)


### レコード取得タスクを親ブランチにマージ
クライアントのレビューはまさかの一発approveでした！そして「ありがとうございます」の一言...
やってきてよかったとジーンときました。__いいものを作りたい__という気持ちがますます強くなりました。

Y氏よりしっかりレビューを頂いたこともあり一発approveを頂けましたが、まだまだ最初から指摘なしというのは困難だとは思います。ただ、かなり気持ちいいため（笑）今後も目指して行けたらと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/57d449a6-88d6-6404-f7cf-daa727f318ae.png)

## 〜実装２〜　口コミレコードの一括インポート
浮かれていたいところですが、本番はここからです。取得したレコードを一括インポートできるよう実装を進めていきます。
ここまでで学んだRubyのメソッド達の活用などによりインポートロジックの実装は比較的に早く進みましたが...。

### インポート方法
一括インポートの方法として２つ候補を挙げました。

- Railsの`insert_all`
- activerecord-importというGem

Gemを入れるまでなく必要機能は満たせそうだと判断し、当初`insert_all`を採用しました。
`transaction`を張り、例外が発生した場合は全レコードをロールバックするよう実装します。

#### insert_allはバリデーションが効かない
```ruby
def create_user_details(reviews, user_ids)
  nick_names   = reviews.pluck(:nick_name)
  timestamp    = Time.current.strftime('%F %T.%N %z')
  user_details = user_ids.map.with_index do |id, i|
    { user_id:    id,
      nick_name:  nick_names[i],
      created_at: timestamp,
      updated_at: timestamp
    }
  end
  
  UserDetail.insert_all user_details
end
```

一括インポート機能は達成しましたが、動作検証時に無効なデータもインポートしていることが判明しました。
どうやら`insert_all`は直接SQLを発行するため、モデルのバリデーションが効かないようです。
また、タイムスタンプも明示してブロックに渡す必要があり、少々コードが複雑になっています。

ここでもう一つの選択肢であるactiverecord-importに再着目します。

#### activerecord-importの有効なポイント
activerecord-importはまさに今回のインポート要件にマッチした手法でした。

- モデルのインスタンスを引数に取ることができる
- モデルのバリデーションを適用できる

`insert_all`と比べてもコードがシンプルになることがわかります。

```ruby
def create_user_details(user_ids, nick_names)
  user_details = user_ids.map.with_index do |id, i|
    UserDetail.new(user_id: id, nick_name: nick_names[i])
  end

  UserDetail.import! user_details
end
```

最初に２つのインポート手法が出てきたところで、それぞれをもう少し深掘りすれば寄り道することはなかったと思いますが、それぞれの挙動を確認することができたのは収穫でした。


### バリデーションへの対応と妥協
DBにインポートするレコードはモデルのバリデーションをかけるように実装していました。
しかし「治療費データ」のみ、ブランクなどの無効な値が入っている場合、バリデーション時にエラーで落ちてしまいます。
原因はクライアントに記載頂くデータ型がPostgreSQLの範囲型表記文字列であるためです。

※ 既存のcsvデータで範囲型表記文字列が採用されていたことから、本施策で入力されるデータ型も範囲型表記文字列であると思い込んでいました...


#### 無効な範囲型表記文字列に対するActiveRecordの挙動
スプレッドシート内の見積額データに規定の範囲型表記文字列が入力されている場合、`Review.new`でインスタンス化すると、以下のようにRange型に変換されます。

```
範囲型表記文字列: [1500000,2000001)
インスタンス化後: 1500000..2000001
```
無効な値をインスタンス化すると、変換がうまくいかず`*Error in evaluation*`となります。
これに対して`valid?`するとエラーとなってしまい、一括インポートする際も同様に落ちてしまいます。

#### 納期を優先して品質を妥協
できないものはしょうがないと無理矢理バリデーションをかけるメソッドを実装しましたが、他のバリデーション処理と違う対応方法は受け入れられないと、なかなかY氏の承認は通りません。

手詰まりと納期に対する焦りを感じ、機能が満たせるからいいだろうと承認を懇願してしまいました...。

### クライアントとの調整事項を見落とす
手詰まりを感じながらもゼロベースで見直そうとクライアントとの調整事項を再確認すると...。
>■ スプレッドシートに新規追加するパラメータ
命名、データ型は以下の通りにしたいと思いますがいかがでしょか？
.
.
.
④estimated_cost :: range # TypeFormに記入された見積もり費用をrangeに変換したもの

見積額は範囲型表記文字列で格納されると思い込んでいましたが、Range型を格納するよう調整済みでした。
Range型であれば他の項目と同様にモデルのバリデーションをかけることが可能です。
しかしながら、納期が迫っていることもあり、思い込みで悩んでいた時間は相当の痛手となってしまいます。

### 反省と怒涛の追い上げ
調整事項の見落としもありますが、何より__その場凌ぎのコードを書く一瞬の妥協が、コードの品質を落とし、ひいてはサービス全体の価値を落としかねない姿勢__であったと反省しました。

やるべきはクライアントへの価値提供であると心を改め、作業時間を35⇨50h/週に増強し、納期必達に向け追い上げていきます。

### 学びは続く
なんとか形になりレビューを受けます。まだまだ命名や適切なメソッドなど学ぶことはたくさんあります。
またも指摘は30以上となりましたが、ついにY氏とクライアントにapproveを頂きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/ee4cacae-64c7-1307-80e2-6c6d41a4b7d3.png)

### クライアントへの納品
納品したトップタスクのみを掲載します。
このタスクだけ見ても何をやっているのか分かるように、最終的にシンプルにまとまりました。

```lib/tasks/import_reviews.thor
class ImportReviews < Thor
  desc 'import', 'import reviews from the spreadsheet'
  option :file_name,  type: :string,  required: true
  option :sheet_name, type: :string,  required: true
  def import
    puts 'Start import reviews'
    worksheet      = fetch_spreadsheet_worksheet
    review_records = to_review_records(worksheet)

    return if review_records.blank?

    import_all(review_records)
    write_imported_at_to_spreadsheet(worksheet, review_records)
    puts 'Successfully finished to import reviews.'
  rescue ActiveRecord::RecordInvalid => e
    puts "ActiveRecord::RecordInvalid error: #{e.record.errors.full_messages}"
    reset_pk_sequence_for(%w[users user_details reviews])
  rescue ActiveRecord::InvalidForeignKey => e
    puts e.message
    reset_pk_sequence_for(%w[users user_details reviews])
  end
  .
  .
end
```

## 〜ドキュメント作成〜　
本施策ではコードの他に動作検証やスケジューラ設定に関するドキュメントも必要になります。

### 動作検証手順書の作成
大変時間のかかる作業ではありましたが、テストコードを書けていない分、クライアントに負担をかけないような手順構成を意識して作成します。

要件を満たしているか確認するために検証内容は必然的に多くなりますが、テスト項目をシートを分けて
用意するなどして検証手順の簡略化を図りました。

- 検証内容
 - スプレッドシート内から対象レコードのみを格納し、取り込んだデータがサイトの口コミ一覧ページに表示されるか
 - スプレッドシート内対象レコードのimportet_atカラムへ、インポートした日付が記載されているか
 - 取り込むレコードが存在しない場合、正常終了するか。
 - 対象レコードに無効なデータが含まれている場合、エラーログを残して正常終了するか。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/bf8a5e00-d99a-4aa5-e698-1f30f13549f1.png)
図：動作検証手順書（抜粋）



### HerokuScheduler設定手順書の作成
スケジューラ設定はクライアントにオペレート頂くことになるため、テストアプリで動作確認をして手順書を作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/d8cb83eb-59fd-448d-2ff5-ffd5da1e4a1c.png)
図：設定手順書

# 実務を振り返ってエンジニアの心構えを学ぶ
今回の実務を経験させて頂いて感じたことは、エンジニアにおいて重要なのはコードだけでなく、むしろそれ以外の__心構え__にあり、それが結果として__コードの品質にも現れる__ということを強く感じました。
今回の実務を通すことで次の５点を向上することができましたが、今後も意識して定着させていきます。


### 1. ゴールを意識して行動する
ゴールを意識しないとそのギャップを埋めるアクションは起こせません。
コードでもゴールから逆算してデータを操作・作成することにより、無駄なコードや手戻りを低減できます。

### 2. 論点を明確にする
施策提案にしても、実装方針策定にしても、論点を明確にしないと話が進みません。
重要な課題を解決させることはもちろん、余計なことをやらないためにも論点を定める必要があります。

### 3. 相手の発言、意図を正確に理解する
日本語を正解に理解せず勝手に自己解釈すると、すぐに明後日の方向に行ってしまいます。
同様にしてこちらから相手に投げかける場合も、正確な日本語で伝える必要があります。

### 4. 具体化と抽象化を使い分ける
人にアクションを起こしてもらう際、WHATを明確にする具体化が必要になります。
それとは逆に全体を俯瞰する抽象化もエンジニアにおいては必要になります。
抽象化によって物事の本質を表すことができ、それがコードの構造や命名に現れます。

### 5. 品質第一
もちろん納期は大事です。でもそれ以上にコード品質の良し悪しがサービスの良し悪しにも直結します。
機能を満たせばいいと、責務を無視したり、ロジック構造を一部変更することはバグを生み出す原因になります。
その後で開発が続くことを考慮し、誤解を与えない・見やすいコードを書く必要があります。


# 最後に
最後までご覧いただきましてありがとうございます。

実務は思っていたよりも数倍厳しい世界でした。
ただそれ以上に自分で課題を抽出し、設計から実装と一連の行程を遂行した上で、それがサービスの一部となる感動を経験することができ、まさに自分のやりたかったことが、エンジニアになることで叶えることができると確信しました。

まずこんな未経験者に案件を出して頂いた、粋なクライアントに心から感謝したいと思います。
そしてY氏。技術面だけでなく、行動面に重きを置き、常に__どうすれば自分で考えて答えを導き出せるのか__を徹底的に教え込んで頂きました。
最後に一緒に進めてきたメンバーのみんな。しんどい時は励まし合い、また相互の考えをシェアすることで、タスクをやり切ることができました。
これまで一人で学習を進めてきたこともあり、同じ志をもった仲間がいるというのは非常に心強いものでした。

関係するみなさんに感謝すると共に、今回学んだことを定着させるためのアウトプットを継続し、転職活動に臨んでまいります。