忠臣蔵 × kintone

# はじめに
[kintone Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kintone) 14日目の記事です。

さて、12月14日といえば、忠臣蔵です！
ということで、忠臣蔵にひっかけたkintoneカスタマイズをしてみました！
（※旧暦の12月14日なので、今の時代だと1月30日になるそうですが…）

# デモ
こんなアプリをつくってみました！
吉良邸屋敷図の図面上に設置したボタンから、各義士の配置をセットできるカスタマイズです。
標準の入力フィールドよりも、直感的に入力をすることが可能となります。
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953166/885db591-fb4d-3349-a841-7e207b1aa7b6.gif)


# 要件と実現方法
1. 隊編成アプリの入力画面に、吉良邸屋敷図を表示する。
    - スペースフィールドを設置し、そこに吉良邸屋敷図の画像を表示させる。
1. 吉良邸屋敷図上における隊の配置場所にボタンを設置する。
    - cssによるスタイリングを施し、吉良邸屋敷図画像の上にボタンを設置する。
1. ボタンを押したとき、隊に関する情報を各フィールドにセットする。
    - 設置したボタンにクリックイベントを追加し、ボタン押下でフィールドに値をセットする。
1. 登録済みのメンバーの配置がわかるようにする。
    - 関連レコードにて対応する。

# 作り方
## 吉良邸屋敷図を作成
Wordで吉良邸屋敷図を作成し、画像で保存しました。[^1]
![map.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953166/06cfac37-4c1e-2b8b-4ee9-8c8583a6c14f.png)

## kintoneアプリ作成
まず、土台となるアプリを作成します。
アプリを作成した後に、kintone REST API をたたいて、取得したフォーム設計情報を記載します。 
※今回作成した隊編成アプリIDは203。名前フィールドの値は、アプリID204のマスタ用のアプリからルックアップで取得する構成です。

```javascript:
kintone.api(kintone.api.url('/k/v1/form.json', true), 'GET', { app: 203}, (resp) => { console.log(resp) });
```

```json:resp
{
    "properties": [
        {
            "type": "SINGLE_LINE_TEXT",
            "label": "名前",
            "noLabel": "false",
            "code": "名前",
            "required": "false",
            "relatedApp": "204"
        },
        {
            "type": "DROP_DOWN",
            "label": "エリア",
            "noLabel": "false",
            "code": "エリア",
            "required": "false",
            "options": [
                "表門",
                "裏門"
            ],
            "defaultValue": null
        },
        {
            "type": "DROP_DOWN",
            "label": "役割",
            "noLabel": "false",
            "code": "役割",
            "required": "false",
            "options": [
                "司令",
                "屋内討入",
                "屋外",
                "場の内"
            ],
            "defaultValue": null
        },
        {
            "type": "SINGLE_LINE_TEXT",
            "label": "隊",
            "noLabel": "false",
            "code": "隊",
            "required": "false",
            "minLength": null,
            "maxLength": null,
            "expression": "",
            "hideExpression": "false",
            "unique": "false",
            "defaultValue": ""
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "司令部",
            "noLabel": "false",
            "code": "表門隊_司令部",
            "relatedApp": "203"
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "屋内討入隊",
            "noLabel": "false",
            "code": "表門隊_屋内討入隊",
            "relatedApp": "203"
        },
        {
            "type": "SINGLE_LINE_TEXT",
            "label": "プロジェクト名",
            "noLabel": "false",
            "code": "プロジェクト名",
            "required": "false",
            "minLength": null,
            "maxLength": null,
            "expression": "",
            "hideExpression": "false",
            "unique": "false",
            "defaultValue": "吉良邸討入"
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "場の内隊",
            "noLabel": "false",
            "code": "表門隊_場の内隊",
            "relatedApp": "203"
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "屋外隊",
            "noLabel": "false",
            "code": "表門隊_屋外隊",
            "relatedApp": "203"
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "司令部",
            "noLabel": "false",
            "code": "裏門隊_司令部",
            "relatedApp": "203"
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "屋内討入隊",
            "noLabel": "false",
            "code": "裏門隊_屋内討入隊",
            "relatedApp": "203"
        },
        {
            "type": "REFERENCE_TABLE",
            "label": "屋外隊",
            "noLabel": "false",
            "code": "裏門隊_屋外隊",
            "relatedApp": "203"
        },
        {
            "type": "NUMBER",
            "label": "#",
            "noLabel": "false",
            "code": "ナンバー",
            "required": "false",
            "minValue": null,
            "maxValue": null,
            "digit": "false",
            "unique": "false",
            "defaultValue": null,
            "displayScale": null,
            "unit": null,
            "unitPosition": "BEFORE"
        },
        {
            "type": "SPACER",
            "elementId": "sp_map"
        }
    ]
}
```
## コーディング
### JavaScript
```javascript:index.js
(() => {
  
  'use strict';

  // ================================================
  // EVENT : create.show, edit.show
  // ================================================
  kintone.events.on(['app.record.create.show', 'app.record.edit.show'], event => {

    const record = event.record;

    const imgMapUrl = 'https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953166/06cfac37-4c1e-2b8b-4ee9-8c8583a6c14f.png';
    const spMap = kintone.app.record.getSpaceElement('sp_map');

    const divMap = document.createElement('div');
    divMap.className = 'hazime__p-container__content';
    spMap.appendChild(divMap);

    const imgMap = document.createElement('img');
    imgMap.src = imgMapUrl;
    imgMap.className = 'hazime__c-media__image__map';
    divMap.appendChild(imgMap);

    // *********************
    // 表門隊
    // *********************

    // 司令部
    const btnFrontGateHeadquarters = document.createElement('button');
    btnFrontGateHeadquarters.textContent = '司令部';
    btnFrontGateHeadquarters.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__headerquarters';

    btnFrontGateHeadquarters.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '表門';
      rec.record.役割.value = '司令';
      rec.record.隊.value = '表門隊_司令部';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnFrontGateHeadquarters);

    // 屋内討入隊
    const btnFrontGateIndoorAttack = document.createElement('button');
    btnFrontGateIndoorAttack.textContent = '屋内討入隊';
    btnFrontGateIndoorAttack.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__indoor-attack';

    btnFrontGateIndoorAttack.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '表門';
      rec.record.役割.value = '屋内討入';
      rec.record.隊.value = '表門隊_屋内討入隊';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnFrontGateIndoorAttack);

    // 場の内隊
    const btnFrontGateInside = document.createElement('button');
    btnFrontGateInside.textContent = '場の内隊';
    btnFrontGateInside.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__inside';

    btnFrontGateInside.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '表門';
      rec.record.役割.value = '場の内';
      rec.record.隊.value = '表門隊_場の内隊';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnFrontGateInside);

    // 屋外隊
    const btnFrontGateOutdoor = document.createElement('button');
    btnFrontGateOutdoor.textContent = '屋外隊';
    btnFrontGateOutdoor.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__outdoor';

    btnFrontGateOutdoor.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '表門';
      rec.record.役割.value = '屋外';
      rec.record.隊.value = '表門隊_屋外隊';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnFrontGateOutdoor);


    // *********************
    // 裏門隊
    // *********************

    // 司令部
    const btnBackGateHeadquarters = document.createElement('button');
    btnBackGateHeadquarters.textContent = '司令部';
    btnBackGateHeadquarters.className = 'hazime__c-button__back-gate hazime__c-button__back-gate__headerquarters';

    btnBackGateHeadquarters.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '裏門';
      rec.record.役割.value = '司令';
      rec.record.隊.value = '裏門隊_司令部';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnBackGateHeadquarters);

    // 屋内討入隊
    const btnBackGateIndoorAttack = document.createElement('button');
    btnBackGateIndoorAttack.textContent = '屋内討入隊';
    btnBackGateIndoorAttack.className = 'hazime__c-button__back-gate hazime__c-button__back-gate__indoor-attack';

    btnBackGateIndoorAttack.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '裏門';
      rec.record.役割.value = '屋内討入';
      rec.record.隊.value = '裏門隊_屋内討入隊';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnBackGateIndoorAttack);

    // 屋外隊
    const btnBackGateOutdoor = document.createElement('button');
    btnBackGateOutdoor.textContent = '屋外隊';
    btnBackGateOutdoor.className = 'hazime__c-button__back-gate hazime__c-button__back-gate__outdoor';

    btnBackGateOutdoor.addEventListener('click', () => {
      let rec = kintone.app.record.get();
      rec.record.エリア.value = '裏門';
      rec.record.役割.value = '屋外';
      rec.record.隊.value = '裏門隊_屋外隊';
      kintone.app.record.set(rec);
    });

    divMap.appendChild(btnBackGateOutdoor);


    return event;

  });


  // ================================================
  // EVENT : detail.show
  // ================================================

  kintone.events.on(['app.record.detail.show'], event => {

    const record = event.record;

    // 全配員表示用のレコードのときは、以下のフィールドを非表示
    if (record.名前.value === '全配員表示用') {
      kintone.app.record.setFieldShown('名前', false);
      kintone.app.record.setFieldShown('エリア', false);
      kintone.app.record.setFieldShown('役割', false);
      kintone.app.record.setFieldShown('隊', false);
      kintone.app.record.setFieldShown('ナンバー', false);
    }

    const imgMapUrl = 'https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953166/06cfac37-4c1e-2b8b-4ee9-8c8583a6c14f.png';
    const spMap = kintone.app.record.getSpaceElement('sp_map');

    const divMap = document.createElement('div');
    divMap.className = 'hazime__p-container__content';
    spMap.appendChild(divMap);

    const imgMap = document.createElement('img');
    imgMap.src = imgMapUrl;
    imgMap.className = 'hazime__c-media__image__map';
    divMap.appendChild(imgMap);

    // *********************
    // 表門隊
    // *********************

    // 司令部
    const btnFrontGateHeadquarters = document.createElement('button');
    btnFrontGateHeadquarters.disabled = true;
    btnFrontGateHeadquarters.textContent = '司令部';
    btnFrontGateHeadquarters.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__headerquarters';

    if (record.隊.value === '表門隊_司令部') {
      btnFrontGateHeadquarters.classList.add('hazime__c-button__his-placement');
    }

    // btnFrontGateHeadquarters.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnFrontGateHeadquarters);

    // 屋内討入隊
    const btnFrontGateIndoorAttack = document.createElement('button');
    btnFrontGateIndoorAttack.disabled = true;
    btnFrontGateIndoorAttack.textContent = '屋内討入隊';
    btnFrontGateIndoorAttack.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__indoor-attack';

    if (record.隊.value === '表門隊_屋内討入隊') {
      btnFrontGateIndoorAttack.classList.add('hazime__c-button__his-placement');
    }

    // btnFrontGateIndoorAttack.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnFrontGateIndoorAttack);

    // 場の内隊
    const btnFrontGateInside = document.createElement('button');
    btnFrontGateInside.disabled = true;
    btnFrontGateInside.textContent = '場の内隊';
    btnFrontGateInside.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__inside';

    if (record.隊.value === '表門隊_場の内隊') {
      btnFrontGateInside.classList.add('hazime__c-button__his-placement');
    }

    // btnFrontGateInside.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnFrontGateInside);

    // 屋外隊
    const btnFrontGateOutdoor = document.createElement('button');
    btnFrontGateOutdoor.disabled = true;
    btnFrontGateOutdoor.textContent = '屋外隊';
    btnFrontGateOutdoor.className = 'hazime__c-button__front-gate hazime__c-button__front-gate__outdoor';

    if (record.隊.value === '表門隊_屋外隊') {
      btnFrontGateOutdoor.classList.add('hazime__c-button__his-placement');
    }

    // btnFrontGateOutdoor.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnFrontGateOutdoor);


    // *********************
    // 裏門隊
    // *********************


    // 司令部
    const btnBackGateHeadquarters = document.createElement('button');
    btnBackGateHeadquarters.disabled = true;
    btnBackGateHeadquarters.textContent = '司令部';
    btnBackGateHeadquarters.className = 'hazime__c-button__back-gate hazime__c-button__back-gate__headerquarters';

    if (record.隊.value === '裏門隊_司令部') {
      btnBackGateHeadquarters.classList.add('hazime__c-button__his-placement');
    }

    // btnBackGateHeadquarters.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnBackGateHeadquarters);

    // 屋内討入隊
    const btnBackGateIndoorAttack = document.createElement('button');
    btnBackGateIndoorAttack.disabled = true;
    btnBackGateIndoorAttack.textContent = '屋内討入隊';
    btnBackGateIndoorAttack.className = 'hazime__c-button__back-gate hazime__c-button__back-gate__indoor-attack';

    if (record.隊.value === '裏門隊_屋内討入隊') {
      btnBackGateIndoorAttack.classList.add('hazime__c-button__his-placement');
    }

    // btnBackGateIndoorAttack.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnBackGateIndoorAttack);

    // 屋外隊
    const btnBackGateOutdoor = document.createElement('button');
    btnBackGateOutdoor.disabled = true;
    btnBackGateOutdoor.textContent = '屋外隊';
    btnBackGateOutdoor.className = 'hazime__c-button__back-gate hazime__c-button__back-gate__outdoor';
    btnBackGateOutdoor.disabled === true;

    if (record.隊.value === '裏門隊_屋外隊') {
      btnBackGateOutdoor.classList.add('hazime__c-button__his-placement');
    }

    // btnBackGateOutdoor.addEventListener('click', () => {
    //   let rec = kintone.app.record.get();

    //   kintone.app.record.set(rec);
    // });

    divMap.appendChild(btnBackGateOutdoor);


    return event;

  });

})();
```

### SCSS
今回のスタイリングについては、SassのSCSS記法を使っています。コンパイルしたCSSファイルをkintoneに適用します。（今回は、VS Codeの拡張機能であるLive Sass Compiler でコンパイルしました。）
```scss:scss/style.scss
// ==========================================================================
// Object
// ==========================================================================

// -----------------------------------------------------------------
// Component
// -----------------------------------------------------------------

@import "object/component/_button";
@import "object/component/_media";

// -----------------------------------------------------------------
// Project
// -----------------------------------------------------------------

@import "object/project/_container";

```

```scss:scss/object/component/_button
$cButtonColorFrontGate: rgb(241, 184, 231);
$cButtonColorBackGate: rgb(187, 189, 255);
$cButtonColorHover: rgb(161, 254, 145);
$cButtonHisPlacement: rgb(255, 0, 0);
$cButtonRadius: 10px;

.hazime__c-button {

  // 表門隊
  &__front-gate {

    position: absolute;
    border-radius: $cButtonRadius;
    background-color: $cButtonColorFrontGate;

    &:hover {
      background-color: $cButtonColorHover;
    }

    &:disabled {
      background-color: $cButtonColorFrontGate;
      cursor: default;
    }

    // 司令部
    &__headerquarters {
      top: 245px;
      left: 880px;
    }

    // 屋内討入隊
    &__indoor-attack {
      top: 280px;
      left: 800px;
    }

    // 場の内隊
    &__inside {
      top: 255px;
      left: 690px;
    }

    // 屋外隊
    &__outdoor {
      top: 340px;
      left: 680px;
    }

  }

  // 裏門隊
  &__back-gate {

    position: absolute;
    border-radius: $cButtonRadius;
    background-color: $cButtonColorBackGate;

    &:hover {
      background-color: $cButtonColorHover;
    }

    &:disabled {
      background-color: $cButtonColorBackGate;
      cursor: default;
    }

    // 司令部
    &__headerquarters {
      top: 110px;
      left: 150px;
    }

    // 屋内討入隊
    &__indoor-attack {
      top: 245px;
      left: 185px;
    }

    // 屋外隊
    &__outdoor {
      top: 355px;
      left: 350px;
    }

  }

  // 詳細画面で配置されている隊
  &__his-placement {
    background-color: $cButtonHisPlacement;
    font-weight: bold;
    color: rgb(255, 255, 255);

    &:disabled {
      background-color: $cButtonHisPlacement;
    }
  }

}
```
```scss:scss/object/component/_media
.hazime__c-media {

  &__image__map {
    width: 1000px;
  }

}
```
```scss:scss/object/project/_container
.hazime__p-container{

  &__content {
    position: relative;
  }
  
}
```


[^1]:参考文献：[吉良邸討入（元禄赤穂事件） | akoinfo.com](https://akoinfo.com/akogisi/ninjoujiken/utiiri.html)