kazpgmの日記

『プログラム自動作成@自動生成』作成の日記

フロント側をFlutter(スマホ)Thymeleaf(PC)、バックエンド側SpringBootの自動作成勉強中

14:50
①今日は、大分類が変更されたらHTTPで中分類を取ってくる、中分類が変更されたらHTTPで小分類を取ってくるっていうあたりの続きをやってる。SpringBootバックエンド側のエレメントを取ってきてるし、エラーの時はエラーも表示できてる。出来上がりってところ。
①-1. SpringBootバックエンド側で入力エラー(例えば、大分類の未入力エラー)のとき、スマホへの返却に『late File? gazoFlFile; // 画像File』など、ファイル項目があると、バックエンドからFileをJSONで戻そうとして、例外が発生した。ので、Fileは戻さないようにした。(通常のWEB画面でも入力エラーの項目があると、ファイル入力はなかったことになるので同じ感じになる)
でも、ちょっとひねって、”『画像』を再度選び直してください。”ってエラーメッセージ欄に表示するようにした。

①-2. 出来たものは、こんな感じ。
画像情報登録を表示した

大分類をクリックし、内容を表示した

「B・・・」を選択した。HTTPで中分類を取ってドロップダウンリストに設定してる

中分類をクリックし、HTTPで取得した内容を表示した

「03・・・」を選択した。HTTPで小分類を取ってドロップダウンリストに設定してる

小分類をクリックし、HTTPで取得した内容を表示した

「031・・・」を選択した

登録ボタンを押下した、『画像』を必須入力としたので、エラーが出る

『画像』『ファイル』を選択し、『小分類』を未入力にし、登録ボタンを押下する。

『小分類』が未入力のため、エラーになる。『画像』も再入力になってしまうので”『画像』を再度選び直してください。”、『ファイル』も再入力になってしまうので”『ファイル』を再度選び直してください。”をエラーメッセージ欄に表示するようにした。

今度は、『画像』を選択し、『ファイル』は選択しないで、『小分類』を未入力にし、登録ボタンを押下する。

『小分類』が未入力のため、エラーになる。『画像』も再入力になってしまうので”『画像』を再度選び直してください。”、『ファイル』は入力されていなかったのでエラーメッセージはない。

必須入力項目を埋めて登録ボタンを押下する

SpringBootバックエンド側から、登録OKメッセージが返ってきた

16:00
スマホ側Flutterはこんな感じ
■gazo_register_amend.dart

class _GazoRegisterAmendState extends State<GazoRegisterAmend> {
・・・
 Widget build(BuildContext context) {
・・・
  List<Widget> _makeWidgets() {
・・・
  //--ドロップダウンリスト選択 start----------------------------
  contentWidgets.add(Container(
    margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
    padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),
    decoration: CommUtils.commBoxDecoration(),
    //Wrapによって折り返しするが、SizeBox使って左寄せする
    child: SizedBox(
    width: double.infinity,
    child:Wrap(
       direction: Axis.horizontal,
      children: <Widget>[
      const Text("大分類",
        overflow: TextOverflow.clip,
      ),
      DropdownButton(
        items: CommUtils.getItemsFromELEMENTS(_gazoForm.publicDbELEMENTS(_gazoForm.dbELEMENTS, 'categoryCd', '---未選択---'), _gazoForm.categoryCd),
        value: _gazoForm.categoryCd,
        icon: const Icon(Icons.arrow_downward),
        elevation: 16,
        style: const TextStyle(color: Colors.deepPurple),
        underline: Container(
        height: 2,
        color: Colors.deepPurpleAccent,
        ),
        onChanged: (value)  async {
        if (_gazoForm.categoryCd != value as String) {
          // httpアクセス
          Map<String, dynamic>? _dbELEMENTS = await CommUtils
            .createLrgMidChange(
            context,
            value,
            '',
            'subcategoryCd',
            '2',
            '/elements/elem',
            'ary_lrgmidsml_category',
            _gazoForm.dbELEMENTS,
            widget.headers,
            widget.cookies);
          setState(() {
          _gazoForm.categoryCd = value;
          _gazoForm.subcategoryCd = '';
          _gazoForm.extracategoryCd = '';
          _gazoForm.dbELEMENTS = _dbELEMENTS;
          });
        }
        },
      ),
      Text(
        // HTTPで返却されたエラーメッセージを表示する。(エラーなしはText高=0にしている)
        _gazoForm.gazoErrForm.categoryCdErr.join("\n"),
        textAlign: TextAlign.left,
        overflow: TextOverflow.clip,
        style: TextStyle(height:(_gazoForm.gazoErrForm.categoryCdErr==[])?0:1.2,
          fontSize: 12, fontWeight: FontWeight.normal, color: Colors.red),
      ),
      ]),
    ),
    ),
  );
  contentWidgets.add(Container(
    margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
    padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),
    decoration: CommUtils.commBoxDecoration(),
    //Wrapによって折り返しするが、SizeBox使って左寄せする
    child: SizedBox(
    width: double.infinity,
    child:Wrap(
       direction: Axis.horizontal,
      children: <Widget>[
      const Text("中分類",
        overflow: TextOverflow.clip,
      ),
      DropdownButton(
        items: CommUtils.getItemsFromELEMENTS(_gazoForm.publicDbELEMENTS(_gazoForm.dbELEMENTS, 'subcategoryCd', '---未選択---'), _gazoForm.subcategoryCd),
        value: _gazoForm.subcategoryCd,
        icon: const Icon(Icons.arrow_downward),
        elevation: 16,
        style: const TextStyle(color: Colors.deepPurple),
        underline: Container(
        height: 2,
        color: Colors.deepPurpleAccent,
        ),
        onChanged: (value) async {
        if (_gazoForm.subcategoryCd != value as String) {
          // httpアクセス
          Map<String, dynamic>? _dbELEMENTS = await CommUtils
            .createLrgMidChange(
            context,
            _gazoForm.categoryCd,
            value,
            'extracategoryCd',
            '2',
            '/elements/elem',
            'ary_lrgmidsml_category',
            _gazoForm.dbELEMENTS,
            widget.headers,
            widget.cookies);
          setState(() {
          _gazoForm.subcategoryCd = value;
          _gazoForm.extracategoryCd = '';
          _gazoForm.dbELEMENTS = _dbELEMENTS;
          });
        }
        },
      ),
      Text(
        // HTTPで返却されたエラーメッセージを表示する。(エラーなしはText高=0にしている)
        _gazoForm.gazoErrForm.subcategoryCdErr.join("\n"),
        textAlign: TextAlign.left,
        overflow: TextOverflow.clip,
        style: TextStyle(height:(_gazoForm.gazoErrForm.subcategoryCdErr==[])?0:1.2,
          fontSize: 12, fontWeight: FontWeight.normal, color: Colors.red),
      ),
      ]),
    ),
    ),
  );
  contentWidgets.add(Container(
    margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
    padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),
    decoration: CommUtils.commBoxDecoration(),
    //Wrapによって折り返しするが、SizeBox使って左寄せする
    child: SizedBox(
    width: double.infinity,
    child:Wrap(
      direction: Axis.horizontal,
      children: <Widget>[
      const Text("小分類",
        overflow: TextOverflow.clip,
      ),
      DropdownButton(
        items: CommUtils.getItemsFromELEMENTS(_gazoForm.publicDbELEMENTS(_gazoForm.dbELEMENTS, 'extracategoryCd', '---未選択---'), _gazoForm.extracategoryCd),
        value: _gazoForm.extracategoryCd,
        icon: const Icon(Icons.arrow_downward),
        elevation: 16,
        style: const TextStyle(color: Colors.deepPurple),
        underline: Container(
        height: 2,
        color: Colors.deepPurpleAccent,
        ),
        onChanged: (value) {
        setState(() {
          _gazoForm.extracategoryCd= value as String;
        });
        },
      ),
      Text(
        // HTTPで返却されたエラーメッセージを表示する。(エラーなしはText高=0にしている)
        _gazoForm.gazoErrForm.extracategoryCdErr.join("\n"),
        textAlign: TextAlign.left,
        overflow: TextOverflow.clip,
        style: TextStyle(height:(_gazoForm.gazoErrForm.extracategoryCdErr==[])?0:1.2,
          fontSize: 12, fontWeight: FontWeight.normal, color: Colors.red),
      ),
      ]),
    ),
    ),
  );
  //--ドロップダウンリスト選択 start----------------------------
・・・
  /// 情報登録、情報変更、情報一覧へのhttpアクセス
  void httpForInfo(String _modeBase) async {
  try {
    String _mode;
    http.Response response;
    if (_modeBase == 'list_back') {
・・・
    } else {
    _mode = (_gazoForm.messageForm.mode == 'ins' || _gazoForm.messageForm.mode == 'ins_do')?'ins_do':'upd_do';
    widget.headers["content-type"]= "multipart/form-data";
    widget.headers["X-XSRF-TOKEN"]= "${widget.cookies['XSRF-TOKEN']}";
    final url = Uri.parse("${Consts.myHttpUrl}/members/admin/gazo/gazo/" + _mode);
    var request = http.MultipartRequest("POST", url);
    request.fields['formComm.page'] = _gazoForm.messageForm.page.toString();
    if (_gazoForm.gazoFlFile != null) {
      final mimeTypeData = lookupMimeType(
        _gazoForm.gazoFlFile!.path, headerBytes: [0xFF, 0xD8])!.split('/');
      final file = await http.MultipartFile.fromPath(
        'gazoForm.gazoFlFile', _gazoForm.gazoFlFile!.path,
        contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
      request.files.add(file);
    }
    if (_gazoForm.fileFile != null) {
      final mimeTypeData = lookupMimeType(
        _gazoForm.fileFile!.path, headerBytes: [0xFF, 0xD8])!.split('/');
      final file = await http.MultipartFile.fromPath(
        'gazoForm.fileFile', _gazoForm.fileFile!.path,
        contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
      request.files.add(file);
    }
    request.fields['gazoForm.categoryCd'] = _gazoForm.categoryCd;
    request.fields['gazoForm.subcategoryCd'] = _gazoForm.subcategoryCd;
    request.fields['gazoForm.extracategoryCd'] = _gazoForm.extracategoryCd;
    request.headers.addAll(widget.headers);
    final streamedResponse = await request.send();
    response = await http.Response.fromStream(streamedResponse);
    }
    if (response.statusCode != 200) {
    setState(() {
      int statusCode = response.statusCode;
      if (response.statusCode == 401 || response.statusCode == 403) {
      _errorSuccessMsg = "ログインしてください";
      } else {
      _errorSuccessMsg = "エラーが発生しました $statusCode";
      }
    });
    // 画面を先頭に戻す
    _scrollController.animateTo(0,
      duration: const Duration(milliseconds:600),
      curve: Curves.easeInQuint);
    return;
    }
    CommUtils.updateCookie(response, widget.cookies, widget.headers);
    // response.bodyをutf8でdecodeする。
    String _resData = utf8.decode(response.body.runes.toList());
    if (kDebugMode) {
    print(_resData);
    }
    // バックエンドで例外発生の場合MessageFormの値しか戻らないため、ここで確認する
    MessageForm _messageForm = MessageForm.initData();
    _messageForm.fromJson(_resData);
    // SpringBootで例外発生の場合
    if (_messageForm.mode =="SystemError") {
    // エラー画面
    Navigator.of(context).push(
      MaterialPageRoute(
      builder: (context) => Error(title: widget.title, username: widget.username,
        headers: widget.headers, cookies: widget.cookies, resData: _resData),
      ),
    );
    // 正常処理
    } else {
    if (_messageForm.mode == 'list_back') {
・・・
    } else {
      setState(() {
      _gazoForm.fromJson(_resData);
      _errorSuccessMsg =
        _gazoForm.messageForm.errorMessage + _gazoForm.messageForm.successMessage;
      if (_errorSuccessMsg == '' &&
          _gazoForm.messageForm.itemErrorMessages != '') {
        _errorSuccessMsg = '項目エラーを確認してください';
      }
      });
    }
    }
  } catch (e) {
    setState(() {
    _errorSuccessMsg = "エラーが発生しました" + e.toString();
    });
  }
  // 画面を先頭に戻す
  _scrollController.animateTo(0,
    duration: const Duration(milliseconds:600),
    curve: Curves.easeInQuint);
  }

  @override
  void initState() {
  super.initState();
  // displayText = "";
  // 画面初期データを設定する
  _gazoForm = GazoForm.initData();
  _gazoForm.fromJson(widget.resData);
  _scrollController= ScrollController();
  if (_gazoForm.messageForm.mode == 'ins' || _gazoForm.messageForm.mode == 'ins_do') {
    _thisTitle = '画像情報登録';
  } else {
    _thisTitle = '画像情報更新';
  }
  }

■comm_utils.dart

  /// ELEMENTSキー毎のコード一覧(LinkedHashMap)返却
  ///  str ELEMENTSのキー
  ///  dflt ”▼選択してください"や"未選択"を設定する。設定しない場合""を指定する。
  ///  elements Elements.javaのエレメント又は、DBから読み込んだElements
  ///  ELEMENTSキー毎のコード一覧(LinkedHashMap)を返却
  static Map<Object, String> publicELEMENTS(String str, String dflt,
    Map<String, dynamic>? elements) {
  Map<String, String> map = <String, String>{};
  if (dflt.isNotEmpty ) {
    map.addAll({"" : dflt});
  }
  if (elements != null) {
    Map<String, String> dynamicMap = Map<String, String>.from(elements[str]);
    map.addAll(dynamicMap);
  }
  return map;
  }

  /// エレメントのドロップダウンリストを作成する
  static List<DropdownMenuItem<String>> getItemsFromELEMENTS(Map<Object, String> elements, String? selectItem) {
  List<DropdownMenuItem<String>> _items = <DropdownMenuItem<String>>[];
  elements.forEach((key, value) {
    if (selectItem != null && selectItem == key as String) {
    _items.add(DropdownMenuItem(
      value: key,
      child: Text(value, style: const TextStyle(fontWeight: FontWeight.normal, color: Colors.black, backgroundColor: Colors.yellow)),
    ));
    } else {
    _items.add(DropdownMenuItem(
      value: key as String,
      child: Text(value, style: const TextStyle(fontWeight: FontWeight.normal, color: Colors.black)),
    ));
    }
  });
  return _items.toList();
  }

  ///大分類、中分類のチェンジ関数。DBデータをpostで取得して、dbELEMENTSを戻す。
  /// 大分類及び中分類のchange関数。
  /// 補足:大分類が選択された時は中分類がある場合、中分類が選択された時は小分類がある場合のみ使用する。
  /// (選択されたテーブルに、子テーブルがない場合使用しない。子テーブルが無いと、エレメントが取れないので。)
  /// 中分類の値が空白の時、中分類のELEMENTS、以外は小分類のELEMENTSを作成する。
  /// _context      : BuildContext
  /// lrgValue      : 大分類の値
  /// midValue      : 中分類の値(大分類が選択された時は空白、中分類が選択された時は値を入れる)
  /// midSmlTableId   : 中分類の値が空白の時、中分類のテーブルID、以外は小分類のテーブルIDを設定する。
  /// inopenkbn     : '1'非公開を含む、'2'非公開を含まない
  /// urlArg      : ajax先のURL
  /// fmElementNm     : エレメント名
  /// dbELEMENTS    : 各フォームのdbELEMENTS
  /// headers       : ヘッダー
  /// cookies       : クッキー
  static Future<Map<String, dynamic>?> createLrgMidChange(BuildContext context,
    String lrgValue, String midValue, String midSmlTableId,
    String inopenkbn, String urlArg, String fmElementNm,
    Map<String, dynamic>? dbELEMENTS, Map<String, String> headers, Map<String, String> cookies) async {
  http.Response response;

  if (dbELEMENTS == null) {
    return dbELEMENTS;
  }
  headers["content-type"]= "application/json; charset=UTF-8";
  headers["X-XSRF-TOKEN"]= "${cookies['XSRF-TOKEN']}";
  Map<String, String> data = {"fmElementNm":fmElementNm, "fmLrgKey":lrgValue,
    "fmMidKey":midValue, "inopenkbn":inopenkbn};
  final url = Uri.parse(Consts.myHttpUrl + urlArg);
  response = await http.post(url, headers: headers, body: json.encode(data));
  if (response.statusCode != 200) {
    // エラー
    await CommUtils.openDialogOkComm(context, "通信エラーが発生しました。");
  } else {
    // response.bodyをutf8でdecodeする。
    String _resData = utf8.decode(response.body.runes.toList());
    Map<String, dynamic> _resDataMap = jsonDecode(_resData);
    dbELEMENTS[midSmlTableId] = _resDataMap;
  }
  return dbELEMENTS;
  }

■gazo_form.dart

import 'dart:convert';
import 'dart:io';
import '../utils/comm_utils.dart';
import 'comm/message_form.dart';
import 'gazo_err_form.dart';

/// Gazo情報画面Form
class GazoForm {
  //プロパティ 入出力データ用
  // late Map<String, Map<String, String>>? dbELEMENTS; // DBから作成するELEMENTS
  late Map<String, dynamic>? dbELEMENTS; // DBから作成するELEMENTS
  late String seqid; // seqid
  late String gazoFl; // 画像名
  late String gazoFlDel; // 画像削除
  late File? gazoFlFile; // 画像File
  late String file; // ファイル名
  late String fileDel; // ファイル削除
  late File? fileFile; // ファイルFile
  late String categoryCd;
  late String subcategoryCd;
  late String extracategoryCd;

  //プロパティ エラーメッセージ
  late GazoErrForm gazoErrForm; // プロパティ エラーメッセージ
  // 共通・メッセージ
  late MessageForm messageForm; // 共通・メッセージ

  /// コンストラクタ
  // Dartでは「コンストラクタ名.任意名称」で複数のコンストラクタを定義する
  // 「クラス名(this.name,・・・);」コンストラクタはいらないので,
  // これだけにする。
  // 初期値のクラスを作成している
  GazoForm.initData()  {
  //入出力データ用
  dbELEMENTS = null;
  seqid = "";
  gazoFl = "";
  gazoFlDel = "";
  gazoFlFile = null;
  file = "";
  fileDel = "";
  fileFile = null;
  categoryCd = "";
  subcategoryCd = "";
  extracategoryCd = "";
  // プロパティ エラーメッセージ
  gazoErrForm = GazoErrForm.initData();
  // 共通・メッセージ
  messageForm = MessageForm.initData();
  }

  /// HTTPレスポンスデータを反映する
  fromJson(String resData) {
  Map<String,dynamic> _resDataMap =
        jsonDecode(resData);
  //入出力データ用
  if (_resDataMap["gazoForm"]==null) {
    dbELEMENTS = null;
    seqid = "";
    gazoFl = "";
    gazoFlDel = "";
    gazoFlFile = null;
    file = "";
    fileDel = "";
    fileFile = null;
    categoryCd = "";
    subcategoryCd = "";
    extracategoryCd = "";
  } else {
    dbELEMENTS = _resDataMap["gazoForm"]["dbELEMENTS"];
    seqid = _resDataMap["gazoForm"]["seqid"] ?? 0;
    gazoFl = _resDataMap["gazoForm"]["gazoFl"];
    gazoFlDel = _resDataMap["gazoForm"]["gazoFlDel"];
    gazoFlFile = _resDataMap["gazoForm"]["gazoFlFile"];
    file = _resDataMap["gazoForm"]["file"];
    fileDel = _resDataMap["gazoForm"]["fileDel"];
    fileFile = _resDataMap["gazoForm"]["fileFile"];
    categoryCd = _resDataMap["gazoForm"]["categoryCd"];
    subcategoryCd = _resDataMap["gazoForm"]["subcategoryCd"];
    extracategoryCd = _resDataMap["gazoForm"]["extracategoryCd"];
  }
  // プロパティ エラーメッセージ
  gazoErrForm.fromJson(resData);
  // 共通・メッセージ
  messageForm.fromJson(resData);
  }

  /// HTTPレスポンスデータを作成する
  Map<String,dynamic> toJsonMap (String _mode, String _csrf, int page) {
  Map<String,dynamic> gazoFormMap = {
    'dbELEMENTS': dbELEMENTS,
    'seqid': seqid,
    'gazoFl': gazoFl,
    'gazoFlDel': gazoFlDel,
    'gazoFlFile': gazoFlFile,
    'file': file,
    'fileDel': fileDel,
    'fileFile': fileFile,
    'categoryCd': categoryCd,
    'subcategoryCd': subcategoryCd,
    'extracategoryCd': extracategoryCd,
  };
  Map<String,dynamic> rtnMap = {
    'gazoForm': gazoFormMap,
    'formComm': messageForm.toJsonMap(_mode, _csrf, page),
  };
  return rtnMap;
  }

  /// http.postのbodyで使用するJsonデータを返却する
  String toJson(String _mode, String _csrf, [int page=1]) {
  return json.encode(toJsonMap(_mode, _csrf, page));
  }

  /// publicDbELEMENTSメソッド
  /// ELEMENTS(DB値)から指定したキーのエレメントを戻す
  ///  str ELEMENTSのキー
  ///  dflt ”▼選択してください"や"未選択"を設定する。設定しない場合""を指定する。
  ///  ELEMENTSキー毎のコード一覧(LinkedHashMap)を戻す
  Map<Object, String> publicDbELEMENTS(Map<String, dynamic>? dbELEMENTS, String str, String dflt) {
  return CommUtils.publicELEMENTS(str, dflt, dbELEMENTS);
  }
}

■gazo_err_form.dart

import 'dart:convert';

/// ユーザー情報画面プロパティ エラーメッセージ用Form
class GazoErrForm {
  //プロパティ エラーメッセージ用
  late List<String> seqidErr;
  late List<String> gazoFlFileErr;
  late List<String> checkGazoFlHissuErr;
  late List<String> checkGazoFlErr;
  late List<String> fileFileErr;
  late List<String> checkFileHissuErr;
  late List<String> checkFileErr;
  late List<String> categoryCdErr;
  late List<String> subcategoryCdErr;
  late List<String> extracategoryCdErr;

  /// コンストラクタ
  // Dartでは「コンストラクタ名.任意名称」で複数のコンストラクタを定義する
  // 「クラス名(this.name,・・・);」コンストラクタはいらないので,
  // これだけにする。
  // 初期値のクラスを作成している
  GazoErrForm.initData()  {
  //エラーメッセージ用
  seqidErr = <String>[];
  gazoFlFileErr = <String>[];
  checkGazoFlHissuErr = <String>[];
  checkGazoFlErr = <String>[];
  fileFileErr = <String>[];
  checkFileHissuErr = <String>[];
  checkFileErr = <String>[];
  categoryCdErr = <String>[];
  subcategoryCdErr = <String>[];
  extracategoryCdErr = <String>[];
  }

  /// HTTPレスポンスデータを反映する
  fromJson(String resData) {
  Map<String,dynamic> _resDataMap =
        jsonDecode(resData);
  //エラーメッセージ用
  if (_resDataMap["resErrorData"]==null) {
    seqidErr = <String>[];
    gazoFlFileErr = <String>[];
    checkGazoFlHissuErr = <String>[];
    checkGazoFlErr = <String>[];
    fileFileErr = <String>[];
    checkFileHissuErr = <String>[];
    checkFileErr = <String>[];
    categoryCdErr = <String>[];
    subcategoryCdErr = <String>[];
    extracategoryCdErr = <String>[];
  } else {
    seqidErr = _resDataMap["resErrorData"]["gazoForm.seqid"]!=null?_resDataMap["resErrorData"]["gazoForm.seqid"].cast<String>():<String>[];
    gazoFlFileErr = _resDataMap["resErrorData"]["gazoForm.gazoFlFile"]!=null?_resDataMap["resErrorData"]["gazoForm.gazoFlFile"].cast<String>():<String>[];
    checkGazoFlHissuErr = _resDataMap["resErrorData"]["gazoForm.checkGazoFlHissu"]!=null?_resDataMap["resErrorData"]["gazoForm.checkGazoFlHissu"].cast<String>():<String>[];
    checkGazoFlErr = _resDataMap["resErrorData"]["gazoForm.checkGazoFl"]!=null?_resDataMap["resErrorData"]["gazoForm.checkGazoFl"].cast<String>():<String>[];
    fileFileErr = _resDataMap["resErrorData"]["gazoForm.fileFile"]!=null?_resDataMap["resErrorData"]["gazoForm.fileFile"].cast<String>():<String>[];
    checkFileHissuErr = _resDataMap["resErrorData"]["gazoForm.checkFileHissu"]!=null?_resDataMap["resErrorData"]["gazoForm.checkFileHissu"].cast<String>():<String>[];
    checkFileErr = _resDataMap["resErrorData"]["gazoForm.checkFile"]!=null?_resDataMap["resErrorData"]["gazoForm.checkFile"].cast<String>():<String>[];
    categoryCdErr = _resDataMap["resErrorData"]["gazoForm.categoryCd"]!=null?_resDataMap["resErrorData"]["gazoForm.categoryCd"].cast<String>():<String>[];
    subcategoryCdErr = _resDataMap["resErrorData"]["gazoForm.subcategoryCd"]!=null?_resDataMap["resErrorData"]["gazoForm.subcategoryCd"].cast<String>():<String>[];
    extracategoryCdErr = _resDataMap["resErrorData"]["gazoForm.extracategoryCd"]!=null?_resDataMap["resErrorData"]["gazoForm.extracategoryCd"].cast<String>():<String>[];
  }
  }
}

②バックエンド側SpringBootはこんな感じ
■GazoCommController.java

package com.kaz02u.demo.controller.adminComm;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;

import com.kaz02u.demo.entity.Gazo;
import com.kaz02u.demo.fForm.FormComm;
import com.kaz02u.demo.form.GazoForm;
import com.kaz02u.demo.form.SessionGazoSrchForm;
import com.kaz02u.demo.form.SessionGazoSrchOrderForm;
import com.kaz02u.demo.response.ResData;
import com.kaz02u.demo.service.DbElementsService;
import com.kaz02u.demo.service.GazoService;
import com.kaz02u.demo.service.SeqService;
import com.kaz02u.demo.utils.AppProperties;
import com.kaz02u.demo.utils.Consts;
import com.kaz02u.demo.utils.Functions;

import lombok.extern.slf4j.Slf4j;

/**
 * GAZO管理のコントローラー
 *
 */
@Controller
//log出力用
@Slf4j
public class GazoCommController implements Consts{

  /**
   * 一覧表示時の、ソート項目の項目名(Entityの項目名。テーブル項目名ではない)
   */
  private static String[] sortItemNames = {"seqid", "gazoFl", "file"};
  
  /**
   * PageableDefaultのsize(1画面中の表示レコード数)を指定
   */
  public static final int pageableDefaultSize = 10;

  /**
   * Gazoのseqid項目のsizeを指定
   */
  public static final int gazoSeqidLen = 11;

  @Autowired
  AppProperties appProperties;
  @Autowired
  Functions functions;
  @Autowired
  GazoService gazoService;
  @Autowired
  SeqService seqService;
  @Autowired
  SessionGazoSrchForm sessionGazoSrchForm;
  @Autowired
  SessionGazoSrchOrderForm sessionGazoSrchOrderForm;
  @Autowired
  MessageSource messageSource;
  @Autowired
  DbElementsService dbElementsService;
 

  /**
   * checkPkForGazoFormメソッド
   * 主キーをチェックする。(u:すべての主キーをチェックできる。 i:主キーにシーケンスが使われていないときチェックできる。)
   * 
   * @param kbn i:挿入時チェック、u:更新時チェック
   * @param gazoForm フォーム
   * @param result result。指定しないときはnullとすること。
   */
  protected void checkPkForGazoForm(String kbn, GazoForm gazoForm, BindingResult result) {
    checkPkForGazoForm(kbn, gazoForm, result, "");
  }

  /**
   * checkPkForGazoFormメソッド
   * 主キーをチェックする。(u:すべての主キーをチェックできる。 i:主キーにシーケンスが使われていないときチェックできる。)
   * 
   * @param kbn i:挿入時チェック、u:更新時チェック
   * @param gazoForm フォーム
   * @param result result。指定しないときはnullとすること。
   * @param prefixes リスト配列に当オブジェクトがある時のprefixes。指定しないときは空文字とすること。
   */
  protected void checkPkForGazoForm(String kbn, GazoForm gazoForm, BindingResult result, String prefixes) {
    //更新時チェック
    if (kbn.equals("u")) {
      //主キーがテーブルになければtrue
      if (gazoService.findByPk(gazoForm.getSeqid()) == null) {
        if (result != null) {
          result.rejectValue(prefixes + "seqid", "validation.already-deleted", new String[] {"『シーケンス(A+yymmdd+数字)』"}, "");  
        }
      }
    //挿入時チェック。主キーにシーケンスが使われていないときのみチェックする。
    //主キーにシーケンスを使っているときは、ここではチェックせずに、挿入時の重複例外を使う。
    } else {
      //主キーがテーブルにあればtrue
      if (gazoService.findByPk(gazoForm.getSeqid()) != null) {
        if (result != null) {
          result.rejectValue(prefixes + "seqid", "validation.already-registered", new String[] {"『シーケンス(A+yymmdd+数字)』"}, "");  
        }
      }
    }
  }
  
  //=======================================================
  // GAZO登録
  //=======================================================
  
  /**
   * リターン共通処理(Flutter、PC・WEB共用)
   * リターン共通処理
   *
   * @param url 遷移先
   * @param model モデル
   * @param result チェック結果
   * @param flutterFlg true:Flutter用 false:PC・WEB用
   * @param resFormName レスポンスForm名
   * @param page 検索以外の時のページ
   * @return Flutter用String:jsonデータ PC・WEB用 Map<String, Object>:遷移先
   */
  protected Object returnComm(String url ,Model model,BindingResult result, boolean flutterFlg, FormComm formComm, String resFormName, String srchFormName, String srchOrdeFormName) {
    if (flutterFlg) {
      Map<String, Object> modelMap = model.asMap();
      if (resFormName.equals("gazoForm") && modelMap!=null && modelMap.get("gazoForm")!=null) {
        GazoForm gazoForm = (GazoForm)modelMap.get("gazoForm");
        // MultipartFileの入力があった場合、再度選び直してもらうためのメッセージを出す。
        if (modelMap.get("errorMessage") !=null) {
          if (gazoForm.getGazoFlFile() !=null) {
            if (result != null) {
              result.rejectValue("gazoForm.checkGazoFl", null, "『画像』を再度選び直してください。");  
            }
//            modelMap.put("errorMessage", modelMap.get("errorMessage") + "\n※ 画像、ファイルは選び直してください。" );
          }
          if (gazoForm.getFileFile() != null) {
            if (result != null) {
              result.rejectValue("gazoForm.checkFile", null, "『ファイル』を再度選び直してください。");  
            }
//            modelMap.put("errorMessage", modelMap.get("errorMessage") + "\n※ 画像、ファイルは選び直してください。" );
          }
        }
        // MultipartFileは返却しないのでnullにする。
        gazoForm.setGazoFlFile(null);
        gazoForm.setFileFile(null);
      }

      ResData resData = new ResData(model, result);
      return resData.getResDataMap(messageSource, formComm, resFormName, srchFormName, srchOrdeFormName);
    } else {
      return url;
    }
  }

  /**
   * gazoInsSubメソッド
   * GAZO登録を表示する処理のサブモジュール
   * 
   * @param model モード
   */
  protected void gazoInsSub(Model model) {
    GazoForm gazoForm = new GazoForm();
    //DBエレメントを取得する
    LinkedHashMap<String, Map<Object, String>> dbELEMENTS = new LinkedHashMap<String, Map<Object, String>>();
    LinkedHashMap<String, Map<Object, String>> dbELEMENTS1;
    dbELEMENTS1 = dbElementsService.getDbEleAryLrgmidsmlCategory(false, null, true, 
        null, true, 
        "categoryCd", "subcategoryCd", "extracategoryCd");
        dbELEMENTS.putAll(dbELEMENTS1);
        gazoForm.setDbELEMENTS(dbELEMENTS);
    model.addAttribute("gazoForm", gazoForm);
    model.addAttribute("mode", "ins");
  }
    
  /**
   * GAZO登録処理(Flutter、PC・WEB共用)
   * GAZO登録するする共通処理
   *
   * @param gazoForm GAZO登録
   * @param result チェック結果
   * @param mode モード
   * @param model モデル
   * @param flutterFlg true:Flutter用 false:PC・WEB用
   * @return Flutter用:jsonデータ PC・WEB用:遷移先
   */
  protected Object gazoInsDoComm(GazoForm gazoForm, BindingResult result, Model model, boolean flutterFlg) {
    if (flutterFlg) {
      model.addAttribute("gazoForm", gazoForm);
    }
    //エラーになったときのモードを設定
    model.addAttribute("mode", "ins");
    //大分類、中分類、小分類をチェックし、DBエレメントを作成する
    checkAndEditLrgmidsmlForGazoForm(gazoForm, result);
    if (result.hasErrors()) {
      model.addAttribute("errorMessage", "エラーが発生しました");
      model.addAttribute("itemErrorMessages", result.toString());
      return returnComm("/members/admin/gazo/gazoRegister", model, result, flutterFlg, null, "gazoForm", null, null);
    }
    String seqid="";
    List<String> inslist = new ArrayList<String>();
    try {
      String mode = "ins_do";
      //主キー作成
      seqid = seqService.getDbSeq("gazo", gazoSeqidLen, "あり", 6);
      String itemName;
      if (gazoForm.getGazoFlFile() != null) {
        if (!ObjectUtils.isEmpty(gazoForm.getGazoFlFile().getOriginalFilename()) || 
            !ObjectUtils.isEmpty(gazoForm.getGazoFlDel())) {
          itemName = functions.setAndDelUploadFile(gazoForm.getGazoFlFile(),
                    "gazo", seqid, "gazoFl", mode, gazoForm.getGazoFlDel());
          gazoForm.setGazoFl(itemName);
          if (!ObjectUtils.isEmpty(itemName)) {
            inslist.add("gazo/" + seqid + "/" + itemName);
          }
        }
      }
      if (gazoForm.getFileFile() != null) {
        if (!ObjectUtils.isEmpty(gazoForm.getFileFile().getOriginalFilename()) || 
            !ObjectUtils.isEmpty(gazoForm.getFileDel())) {
          itemName = functions.setAndDelUploadFile(gazoForm.getFileFile(),
                    "gazo", seqid, "file", mode, gazoForm.getFileDel());
          gazoForm.setFile(itemName);
          if (!ObjectUtils.isEmpty(itemName)) {
            inslist.add("gazo/" + seqid + "/" + itemName);
          }
        }
      }
    } catch(Exception e){
      e.printStackTrace();
      log.error("エラーが発生しました", e);
      //エラーになったので、リストしたファイルを削除しておく
      functions.delFileList(inslist);
      throw e;
    }
    try {
      gazoService.register( seqid, gazoForm.getGazoFl(), gazoForm.getFile(),
          gazoForm.getCategoryCd(),
          gazoForm.getSubcategoryCd(),
          gazoForm.getExtracategoryCd());
    } catch(Exception e){
      e.printStackTrace();
      log.error("DBエラーが発生しました", e);
      //エラーになったので、リストしたファイルを削除しておく
      functions.delFileList(inslist);
      throw e;
    }
    Gazo gazo = null;
    try {
      //メールのためにGAZOを取得する
      gazo = gazoService.findByPk(seqid);
      if (gazo == null) {
        log.error("gazo not found:pk seqid={}", seqid);
        throw new RuntimeException("invalid pk");
      }
      gazoService.send(appProperties.getMailTo(), appProperties.getMailFrom(), "登録メール", gazo);
    } catch(Exception e){
      e.printStackTrace();
      log.error("メール送信でエラーが発生しました。(但し、GAZOは登録完了済みです)", e);
      model.addAttribute("errorMessage", "エラーが発生しました。(但し、GAZOは登録完了済みです)");
      return returnComm("/members/admin/gazo/gazoRegister", model, result, flutterFlg, null, "gazoForm", null, null);
    }

    gazoInsSub(model);
    model.addAttribute("successMessage", "GAZO登録が完了しました");
    return returnComm("/members/admin/gazo/gazoRegister", model, result, flutterFlg, null, "gazoForm", null, null);
  }

  /**
   * checkAndEditLrgmidsmlForGazoFormメソッド
   * 大分類、中分類、小分類をチェックし、DBエレメントを作成する
   * 
   * @param gazoForm フォーム
   * @param result result。指定しないときはnullとすること。
   */
  protected void checkAndEditLrgmidsmlForGazoForm(GazoForm gazoForm, BindingResult result) {
    checkAndEditLrgmidsmlForGazoForm(gazoForm, result, "");
  }
  /**
   * checkAndEditLrgmidsmlForGazoFormメソッド
   * 大分類、中分類、小分類をチェックし、DBエレメントを作成する
   * 
   * @param gazoForm フォーム
   * @param result result。指定しないときはnullとすること。
   * @param prefixes リスト配列に当オブジェクトがある時のprefixes。指定しないときは空文字とすること。
   */
  protected void checkAndEditLrgmidsmlForGazoForm(GazoForm gazoForm, BindingResult result, String prefixes) {
    boolean categoryCdFlg = true;
    boolean subcategoryCdFlg = true;
    boolean extracategoryCdFlg = true;
    //『大分類』がcategoryテーブルにあればtrue(削除されているものは取得しない)
    if (!dbElementsService.isCheckCategoryCd(gazoForm.getCategoryCd(), false)) {
      categoryCdFlg = false;
      if (result != null) {
        result.rejectValue(prefixes + "categoryCd", "validation.category", new String[] {"『大分類』"}, "");  
      }

    }
    //『中分類』がsubcategoryテーブルにあればtrue(削除されているものは取得しない)
    if (!dbElementsService.isCheckSubcategoryCd(gazoForm.getCategoryCd(), 
      gazoForm.getSubcategoryCd(), false, categoryCdFlg)) {
      subcategoryCdFlg = false;
      if (result != null) {
        result.rejectValue(prefixes + "subcategoryCd", "validation.category", new String[] {"『中分類』"}, "");      
      }

    }
    //『小分類』がextracategoryテーブルにあればtrue(削除されているものは取得しない)
    if (!dbElementsService.isCheckExtracategoryCd(gazoForm.getCategoryCd(), 
      gazoForm.getSubcategoryCd(), gazoForm.getExtracategoryCd(), false, subcategoryCdFlg)) {
      extracategoryCdFlg = false;
      if (result != null) {
        result.rejectValue(prefixes + "extracategoryCd", "validation.category", new String[] {"『小分類』"}, "");      
      }

    }
    //DBエレメントを取得する
    LinkedHashMap<String, Map<Object, String>> dbELEMENTS = new LinkedHashMap<String, Map<Object, String>>();
    LinkedHashMap<String, Map<Object, String>> dbELEMENTS1;
    dbELEMENTS1 = dbElementsService.getDbEleAryLrgmidsmlCategory(false, gazoForm.getCategoryCd(), categoryCdFlg, 
            gazoForm.getSubcategoryCd(), subcategoryCdFlg, 
            "categoryCd", "subcategoryCd", "extracategoryCd");
    dbELEMENTS.putAll(dbELEMENTS1);
    gazoForm.setDbELEMENTS(dbELEMENTS);
  }

}

■GazoFlutterController.java

package com.kaz02u.demo.controller.adminFlutter;

import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.kaz02u.demo.controller.adminComm.GazoCommController;
import com.kaz02u.demo.fForm.GazoFForm;
import com.kaz02u.demo.form.GazoForm;
import com.kaz02u.demo.validation.GroupOrder1;
import com.kaz02u.demo.validation.GroupOrder2;

import lombok.extern.slf4j.Slf4j;

/**
 * Flutter用GAZO管理のコントローラー
 *
 */
@Controller
//log出力用
@Slf4j
public class GazoFlutterController extends GazoCommController{

  
  //=======================================================
  // GAZO登録
  //=======================================================

  /**
   * GAZO登録表示処理(Flutter用)
   * GAZO登録を表示する処理
   *
   * @param model モデル
   * @return jsonデータ
   */
  @SuppressWarnings("unchecked")
  @PostMapping("/members/admin/gazo/gazo/ins")
  @ResponseBody
  public Map<String, Object> gazoInsFlutter(Model model) {
    gazoInsSub(model);
    return (Map<String, Object>)returnComm("/members/admin/gazo/gazoRegister", model, null, true, null, "gazoForm", null, null);
  }
    
  /**
   * GAZO登録処理(Flutter用)
   * GAZO登録する処理
   *
   * @param gazoFForm Flutter向けGAZO登録
   * @param result チェック結果
   * @param model モデル
   * @return jsonデータ
   */
  @SuppressWarnings("unchecked")
  @PostMapping("/members/admin/gazo/gazo/ins_do")
  @ResponseBody
  public  Map<String, Object> gazoInsDoFlutter(@Validated({GroupOrder1.class, GroupOrder2.class})  GazoFForm gazoFForm,
      BindingResult result,
      Model model) {
    return (Map<String, Object>)gazoInsDoComm(gazoFForm.getGazoForm(), result, model, true);
  }
}

■GazoForm.java

package com.kaz02u.demo.form;

import java.io.Serializable;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import com.kaz02u.demo.utils.Elements;
import com.kaz02u.demo.utils.Functions;
import com.kaz02u.demo.utils.ValidCheck;
import com.kaz02u.demo.validation.CheckHalfLetterDigitBig;
import com.kaz02u.demo.validation.CheckHalfStartEndSpace;
import com.kaz02u.demo.validation.CheckHalf;
import com.kaz02u.demo.validation.GroupOrder1;
import com.kaz02u.demo.validation.GroupOrder2;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Component
public class GazoForm implements Serializable {
  
  /**
   * 
   */
  private static final long serialVersionUID = 1L;

  /**
   * DBから作成するELEMENTS
   */
  private LinkedHashMap<String, Map<Object, String>> DbELEMENTS;

  /**
   * publicDbELEMENTSメソッド
   * ELEMENTS(DB値)から指定したキーのエレメントを戻す
   * 
   * @param str ELEMENTSのキー
   * @param dflt ”▼選択してください"や"未選択"を設定する。設定しない場合""を指定する。
   * @return ELEMENTSキー毎のコード一覧(LinkedHashMap)
   */
  public LinkedHashMap<Object, String> publicDbELEMENTS(String str, String dflt) {
    return Functions.publicELEMENTS(str, dflt, DbELEMENTS);
  }

  
  //全項目の必須チェック及びサイズチェックを「GroupOrder1」グループで行ったのち、
  //全項目の属性を「GroupOrder2」グループでチェックする。

  @Size(min = 0, max = 11, groups={GroupOrder1.class})
  @CheckHalfLetterDigitBig(groups={GroupOrder2.class})
  @CheckHalfStartEndSpace(groups={GroupOrder2.class})
  private String seqid = "";

  private String gazoFl = "";
  private String gazoFlDel = "";
  private MultipartFile gazoFlFile;

  @AssertTrue(message="『画像』に指定したファイルの拡張子は、アップロード対象外でした", groups={GroupOrder1.class})
  public boolean isCheckGazoFl() {
    return ValidCheck.isImageFile(gazoFlFile);
  }

  private String file = "";
  private String fileDel = "";
  private MultipartFile fileFile;

  @AssertTrue(message="『ファイル』に指定したファイルの拡張子は、アップロード対象外でした", groups={GroupOrder1.class})
  public boolean isCheckFile() {
    return ValidCheck.isFile(fileFile);
  }

  //DbELEMENTSでのチェックはControllerで行う
  @NotBlank(groups={GroupOrder1.class})
  private String categoryCd = "";

  //DbELEMENTSでのチェックはControllerで行う
  @NotBlank(groups={GroupOrder1.class})
  private String subcategoryCd = "";

  //DbELEMENTSでのチェックはControllerで行う
  @NotBlank(groups={GroupOrder1.class})
  private String extracategoryCd = "";
  
  @AssertTrue(message="『画像』は必須入力です", groups={GroupOrder1.class})
  public boolean isCheckGazoFlHissu() {
    return ValidCheck.isFileHissu(gazoFlFile, gazoFl, gazoFlDel);
  }
}

22:19
①これも書いておこう
■DbElementsFlutterController.java

package com.kaz02u.demo.controller.adminFlutter;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import com.kaz02u.demo.entity.Extracategory;
import com.kaz02u.demo.entity.Subcategory;
import com.kaz02u.demo.form.EleForm;
import com.kaz02u.demo.repository.ExtracategoryRepository;
import com.kaz02u.demo.repository.SubcategoryRepository;
import com.kaz02u.demo.service.DbElementsService;
import com.kaz02u.demo.utils.Functions;

import lombok.extern.slf4j.Slf4j;

/**
 * ajaxから呼ばれるエレメント取得のコントローラー
 *
 */
@Controller
//log出力用
@Slf4j
public class DbElementsFlutterController {
  @Autowired
  SubcategoryRepository subcategoryRepository;
  @Autowired
  ExtracategoryRepository extracategoryRepository;

  @Autowired
  DbElementsService dbElementsService;
  @PostMapping("/elements/elem")
  @ResponseBody
  public Map<Object, Object> dbElemFlutter(@RequestBody EleForm eleForm) {
    LinkedHashMap<Object, Object> map = new LinkedHashMap<Object, Object>();
    //非公開を取得するかのフラグをbooleanにする
    boolean getAllFlg = !ObjectUtils.isEmpty(eleForm.getInopenkbn()) && eleForm.getInopenkbn().equals("1") ? true:false;
    //エレメント名によるDB取得
    if (eleForm.getFmElementNm().equals("ary_lrgmidsml_category")) {
      if (!Functions.isItemEmptyOrNoGood(eleForm.getFmLrgKey(), false) && Functions.isItemEmptyOrNoGood(eleForm.getFmMidKey(), false)) {
        //中分類エレメントを設定
        List<Subcategory> subcategoryList = subcategoryRepository.findByCategoryCd(eleForm.getFmLrgKey());
        for (Subcategory subcategoryRec: subcategoryList) {
          //公開、非公開に関係なく取得する場合
          if (!(subcategoryRec.getOpenkbn().equals("2") && getAllFlg == false)) {
            map.put(subcategoryRec.getSubcategoryCd(), subcategoryRec.getSubcategoryCd() + ":" + subcategoryRec.getSubcatname());
          }
        }
      } else if (!Functions.isItemEmptyOrNoGood(eleForm.getFmLrgKey(), false) && !Functions.isItemEmptyOrNoGood(eleForm.getFmMidKey(), false)) {
        //小分類エレメントを設定
        List<Extracategory> extracategoryList = extracategoryRepository.findByCategoryCdAndSubcategoryCd(eleForm.getFmLrgKey(), eleForm.getFmMidKey());
        for (Extracategory extracategoryRec: extracategoryList) {
          //公開、非公開に関係なく取得する場合
          if (!(extracategoryRec.getOpenkbn().equals("2") && getAllFlg == false)) {
            map.put(extracategoryRec.getExtracategoryCd(), extracategoryRec.getExtracategoryCd() + ":" + extracategoryRec.getExcatname());
          }
        }
      }
    }
    return map;
  }
}

■2022/06/15に、勉強した成果:『Flutter_JavaSpringプログラム自動作成◎自動生成ツール』をVectorに載せました。Zenn本も書きました。使ってみての感想や間違いの指定や、こうやったほうがいいとかの情報があればメールください。
Vector
www.vector.co.jp
・Zenn本(Flutter_JavaSpringプログラム自動作成)
zenn.dev