見出し画像

Next.js とSupabaseで求人マッチングアプリを作る⑪~求職者から企業をブロックする機能~

今回は『求職者側で企業をブロックする機能』を作成します。

この機能はなぜ、求職者にとって必要でしょうか?
単に『スパムのようにオファーを掛けてくる企業をブロックしたい』という理由や『この企業は評判が悪いからブロックしたい』というケースもありますが、
一番は『今現在勤めている会社に、マッチングサイトへ登録していることを知られたくない』という理由が大きいです。

求職者にとって安心して使ってもらうための物なので、ぜひこの機能も実装してください。

Supabaseの設定

今回企業のブロックをするにあたり、求職者と企業を結び付けたテーブルを新たに作成します。

m2m_block_job_seeker_company

`m2m_block_job_seeker_company`テーブルを作成します。
添付画像のような設定で列を作成しましょう。


追加の列はすべてnullを許容せず、
job_seeker_idは`mst_job_seeker`の`user_uid`を参照してください。
また、company_idは`mst_company`の`user_uid`を参照してください。

ポリシー設定

ポリシーは双方ともに`ALL`かつtrueで作成します。

TypeScriptの型生成

Next.jsのプロジェクト直下でSupabaseにログインします。

npx supabase login

その後、下記を実行して今回のテーブルの追加を反映しましょう。

npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts

Next.jsの実装

components/jobseeker/CompanyList.tsx

企業一覧にブロック機能を設定し、すでにブロック済みの場合はその旨を表示するようにしています。

"use client";
import {createClient} from "@/utils/supabase/client";
import {useEffect, useState} from "react";
import {Database} from "@/types/supabase";
import Link from "next/link";

type Props = {
    searchText: string | null;
};
export default function CompanyList({searchText}: Props) {
    const supabase = createClient();
    const [companyData, setCompanyData] = useState<
        Database["public"]["Tables"]["mst_company"]["Row"][]
    >([]);
    const [blockIDs, setBlockIDs] = useState<string[]>([])

    useEffect(() => {
        init();
    }, [searchText]);

    const init = async () => {
        await getBlockIDs()
        if (searchText) {
            await searchCompanyData()
        } else {
            await getCompanyData()
        }
    }

    const getBlockIDs = async () => {
        const {
            data: { user },
        } = await supabase.auth.getUser();
        const {data, error} = await supabase.from("m2m_block_job_seeker_company").select().eq("job_seeker_id", user?.id)
        if (error) {
            console.log(error);
            return;
        }

        const tmp_data: Database["public"]["Tables"]["m2m_block_job_seeker_company"]["Row"][] =
            data as Database["public"]["Tables"]["m2m_block_job_seeker_company"]["Row"][];

        const ids = []
        for (let index = 0; index < tmp_data.length; index++) {
            ids.push(tmp_data[index]["company_id"])
        }

        setBlockIDs(ids);
    }

    const getCompanyData = async () => {
        const {data, error} = await supabase.from("mst_company").select();
        if (error) {
            console.log(error);
            return;
        }

        console.log(data);

        const tmp_data: Database["public"]["Tables"]["mst_company"]["Row"][] =
            data as Database["public"]["Tables"]["mst_company"]["Row"][];
        setCompanyData(tmp_data);
    };

    const searchCompanyData = async () => {
        const searchTexts = searchText!.split(/[\s\u3000]+/);
        let result = `'${searchTexts[0]}'`;
        if (searchTexts.length > 1) {
            // like(あいまい検索)もできるが一旦全文検索で作成
            for (let index = 1; index < searchTexts.length; index++) {
                const element = searchTexts[index];
                result += ` | `;
                result += `'${element}'`;
            }
        }

        const nameData:Database["public"]["Tables"]["mst_company"]["Row"][] | undefined = await nameSearch(result)
        const addressData: Database["public"]["Tables"]["mst_company"]["Row"][] | undefined = await addressSearch(result)
        const jobData: Database["public"]["Tables"]["mst_company"]["Row"][] | undefined = await jobSearch(result)

        const newData = (nameData? nameData : []).concat(addressData? addressData : []).concat(jobData? jobData : [])
        setCompanyData(newData)
    }

    const nameSearch = async (result: string) => {
        const {data, error} = await supabase.from("mst_company").select()
            .textSearch("name", result);

        if (error) {
            console.log(error);
            return;
        }

        return data as Database["public"]["Tables"]["mst_company"]["Row"][]
    }

    const addressSearch = async (result: string) => {
        // address1とaddress2をまとめて返すaddress関数をデータベースに作成した。
        const {data, error} = await supabase.from("mst_company").select()
            .textSearch("address", result);

        if (error) {
            console.log(error);
            return;
        }

        return data as Database["public"]["Tables"]["mst_company"]["Row"][];
    }

    const jobSearch = async (result: string) => {
        const { data, error } = await supabase.from("trn_job").select()
            .textSearch("name", result)

        if (error) {
            console.log(error);
            return [];
        }
        if (data?.length > 0) {
            let tmpData: Database["public"]["Tables"]["trn_job"]["Row"][] = data
            return await getCompanyDataFromJob(tmpData)
        }
    }

    const getCompanyDataFromJob = async(jobs: Database["public"]["Tables"]["trn_job"]["Row"][]) => {
        let resultData: Database["public"]["Tables"]["mst_company"]["Row"][] = []
        for (let index = 0; index < jobs.length; index++) {
            const {data, error} = await supabase.from("mst_company").select()
                .eq("user_uid", jobs[index]["company_uid"]);

            if (error) {
                console.log(error);
            }
            if (data) {
                resultData = resultData.concat(data as Database["public"]["Tables"]["mst_company"]["Row"][])
            }
        }

        return resultData
    }

    const blockCompany = async (id: string) => {
        const {
            data: { user },
        } = await supabase.auth.getUser();
        const { error } = await supabase
            .from('m2m_block_job_seeker_company')
            .insert({ job_seeker_id: user?.id, company_id: id })
    }

    return (
        <div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
            <h1>企業一覧</h1>
            <ul>
                {companyData.map((item) => (
                    <li key={item.user_uid}>
                        <Link
                            href={"/companydetail?companyid=" + item.user_uid}
                            className="text-blue-700 hover:text-blue-600 border-b"
                        >
                            {item.name}
                        </Link>
                        {blockIDs.includes(item.user_uid) ? (<span className="ml-4">ブロック済み(企業があなたを検索できなくなります。)</span>) : (<button className="hover:border-b ml-4" onClick={() => blockCompany(item.user_uid)}>この企業をブロックする</button>
                            )}
                    </li>
                ))}
            </ul>
        </div>
    );
}

components/company/JobSeekerList.tsx

`m2m_block_job_seeker_company`テーブルのデータをもとに、ブロックされている場合は求職者を表示しないような変更をしました。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { Database } from "@/types/supabase";
import Link from "next/link";
import JobSeekerSearch from "@/components/company/JobSeekerSearch";

export default function JobSeekerList() {
  const supabase = createClient();
  const [jobSeekerData, setJobSeekerData] = useState<
    Database["public"]["Tables"]["mst_job_seeker"]["Row"][]
  >([]);
  const [blockedIDs, setBlockedIDs] = useState<string[]>([])

  useEffect(() => {
    getBlockedIDs()
    getJobSeekerData();
  }, []);

  const getBlockedIDs = async () => {
    const {
      data: { user },
    } = await supabase.auth.getUser();
    const { data, error } = await supabase.from("m2m_block_job_seeker_company").select().eq("company_id", user?.id);
    if (error) {
      console.log(error);
      return;
    }

    console.log(data);

    const tmp_data: Database["public"]["Tables"]["m2m_block_job_seeker_company"]["Row"][] =
        data as Database["public"]["Tables"]["m2m_block_job_seeker_company"]["Row"][];

    const ids = []
    for (let index = 0; index < tmp_data.length; index++) {
      ids.push(tmp_data[index]["job_seeker_id"])
    }

    setBlockedIDs(ids);
  }

  const getJobSeekerData = async () => {
    const { data, error } = await supabase.from("mst_job_seeker").select();
    if (error) {
      console.log(error);
      return;
    }

    console.log(data);

    const tmp_data: Database["public"]["Tables"]["mst_job_seeker"]["Row"][] =
      data as Database["public"]["Tables"]["mst_job_seeker"]["Row"][];
    setJobSeekerData(tmp_data);
  };

  return (
    <div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
      <h1>求職者一覧</h1>
      <JobSeekerSearch tableData={setJobSeekerData}></JobSeekerSearch>
      <ul>
        {jobSeekerData.map((item) => (
            blockedIDs.includes(item.user_uid) ? (<></>) : (
                <li
                    key={item.user_uid}
                    className="bg-gray-200 rounded-md p-5 mt-4 hover:bg-gray-300"
                >
                  <Link
                      href={"/jobseekerdetail?jobseekerid=" + item.user_uid}
                      className="block w-full"
                  >
                    <h2>{item.last_name + " " + item.first_name}</h2>
                    <p>ユーザID:{item.user_uid}</p>
                    <div className="flex">
                      <div className="w-1/2">
                        <h3>性別</h3>
                        <p>
                          {item.gender == 0
                              ? "女性"
                              : item.gender == 1
                                  ? "男性"
                                  : "その他"}
                        </p>
                      </div>
                      <div className="w-1/2">
                        <h3>生年月日</h3>
                        <p>{item.birthday}</p>
                      </div>
                    </div>
                  </Link>
                </li>
            )
        ))}
      </ul>
    </div>
  );
}

実装の確認

求職者側でログインし、企業一覧を表示すると企業の横にブロックするボタンが表示されます。

ブロックするボタンを押すと、ブロック済みに変更されます。

次に企業側にログインして求職者を開きます。
するとブロックした求職者が表示されなくなります。

その他参考資料など

弊社では『マッチングワン』という『低コスト・短期にマッチングサービスを構築できる』サービスを展開しており、今回ご紹介するコードは、その『マッチングワン』でも使われているコードとなります。
本記事で紹介したようなプロダクトを開発されたい場合は、是非お問い合わせください。

またTodoONada株式会社では、この記事で紹介した以外にも、Supabase・Next.jsを使ったアプリの作成方法についてご紹介しています。
下記記事にて一覧を見ることができますので、ぜひこちらもご覧ください!

お問合せ&各種リンク

presented by


サポートしていただくと、筆者のやる気がガンガンアップします!