CloudFlareにデプロイするためのRemixプロジェクトで検索条件をリッチにする〜一つのキーで複合条件〜

## 概要

長ったらしいタイトルになってしまったが、ようは「タスクカテゴリがStudy or Other」のように指定したい場合の実行方法をまとめる。

## 前提

下記の内容が完了していること。

https://qiita.com/miriwo/items/8d26883adcd609c5fe38

## 現状の確認

タスクカテゴリは検索時に１個だけ指定できる仕様になってしまっている。
これだともちろん単独のタスクカテゴリのタスクだけ見たい場合は問題ないが、カテゴリを複数選択しながらチェックしたい場合にかなり不便となる。

## 方法

まずは現在プルダウンで単一のカテゴリしか指定できない部分を修正する。

1. `app/routes/_index.tsx`を下記のように修正し、プルダウンからチェックボックスに変更し、複数のカテゴリ情報をクエリパラメーターに乗せられるように実装

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate, useLoaderData } from "@remix-run/react";
    import { useState } from "react";
    import { loader } from "./task-category-masters-loader";
    export { loader };
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
      const [isDeleted, setIsDeleted] = useState(false);
      const [isComplete, setIsComplete] = useState(false);
      const [taskCategoryMasterIds, setTaskCategoryMasterId] = useState<string[]>([]); // 初期値を空配列に設定
    
      const handleSearchClick = () => {
        const queryParams = new URLSearchParams();
        if (isDeleted) {
          queryParams.append("isDeleted", isDeleted.toString());
        }
        if (isComplete) {
          queryParams.append("isComplete", isComplete.toString());
        }
        if (taskCategoryMasterIds.length >= 1) { // 配列の要素が1つ以上あるかで配列の中身をチェック
          taskCategoryMasterIds.map((taskCategoryMasterId) => {
            queryParams.append("taskCategoryMasterId", taskCategoryMasterId.toString());
          });
        }
        navigate(`/tasks?${queryParams.toString()}`); // クエリパラメーターを含めて遷移
      };
    
      interface TaskCategoryMaster {
        id: string;
        name: string;
      }
    
      const taskCategoryMasters = useLoaderData<TaskCategoryMaster[]>();
    
      const handleCheckboxChange = (id: string) => {
        setTaskCategoryMasterId((prevIds) =>
          prevIds.includes(id)
            ? prevIds.filter((prevId) => prevId !== id)
            : [...prevIds, id]
        );
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isDeleted}
                  onChange={(e) => setIsDeleted(e.target.checked)}
                />
                削除済みを表示
              </label>
            </div>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isComplete}
                  onChange={(e) => setIsComplete(e.target.checked)}
                />
                完了済みを表示
              </label>
            </div>
            <div>
              <div>カテゴリ</div>
              {taskCategoryMasters.map((taskCategoryMaster: TaskCategoryMaster) => (
                <label key={taskCategoryMaster.id}>
                  <input
                    type="checkbox"
                    value={taskCategoryMaster.id}
                    checked={taskCategoryMasterIds.includes(taskCategoryMaster.id)}
                    onChange={() => handleCheckboxChange(taskCategoryMaster.id)}
                  />
                  {taskCategoryMaster.name}
                </label>
              ))}
            </div>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. `app/routes/tasks-loader.ts`を下記のように修正し、複数のクエリパラメーターを受け取り、SQLのクエリに起こせるように実装

    ```app/routes/tasks-loader.ts
    import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
    
    export const loader = async ({ context, request }: LoaderFunctionArgs) => {
      const url = new URL(request.url);
      const searchParams = url.searchParams;
    
      const isCompleteParam: unknown = searchParams.get("isComplete");
      const isComplete: boolean = isCompleteParam === "true" ? true : false;
    
      const isDeletedParam: unknown = searchParams.get("isDeleted");
      const isDeleted: boolean = isDeletedParam === "true" ? true : false;
    
      const taskCategoryMasterIdParams: string[] = searchParams.getAll("taskCategoryMasterId");
      const taskCategoryMasterIds: number[] = taskCategoryMasterIdParams.map(id => Number(id));
    
      const isTrue = 1; // フラグONの意味 マジックナンバー申し訳ない。。
    
      try {
        const tasks = await context.db.tasks.findMany({
          where: {
            ...(isComplete && { is_complete: isTrue }),
            ...(isDeleted && { deleted_at: {not: null}}),
            ...(taskCategoryMasterIds.length > 0 && { task_category_master_id: { in: taskCategoryMasterIds } }),
          },
        });
        return tasks;
      } catch (error) {
        console.error("Failed to load tasks:", error);
        throw new Response("Internal Server Error", { status: 500 });
      }
    }
    ```

1. 下記のように複合条件でクエリパラメーターが発行され、一覧側画面が問題なく表示されれば完了

    ![CleanShot 2024-06-18 at 00.32.30.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/dac85696-7acd-01a0-3dc0-548ba4141092.gif)