Next.js とSupabaseで求人マッチングアプリを作る④~求職者のプロファイル変更~

今回は前回の記事の続きで、『求職者の基本情報を変更する』UIを追加します。
基本的な流れは『企業のプロファイル変更』と同じです。
なのでもし自学目的で見ておられる方は、企業プロファイルのコードを見ながら、求職者部分のUI実装を行ってみてください。

目次

Next.js実装

components/userinfo/MainContents.tsx

以前同様に、求職者の基本情報の変更画面も追加します。

import { userInfoContentsType } from "@/utils/userinfocontentstype";
import JobSeekerBasic from "./JobSeekerBasic";
import CompanyBasic from "./CompanyBasic";

type Props = {
  contentsType: userInfoContentsType;
};

export default function MainContents({ contentsType }: Props) {
  return (
    <>
      {contentsType == userInfoContentsType.job_seeker_basic ? (
        <JobSeekerBasic></JobSeekerBasic>
      ) : contentsType == userInfoContentsType.company_basic ? (
        <CompanyBasic></CompanyBasic>
      ) : (
        <p>
          ログインに問題があるようです。
          <br />
          求職者は求職者用のドメイン、企業は企業用のドメインからアクセスをお願いします。
        </p>
      )}
    </>
  );
}

components/userinfo/JobSeekerBasic.tsx

求職者の基本情報を更新するための画面です。
内容としては`mst_job_seeker`のデータがあれば表示し、必須項目を入力したうえで更新を行えばSupabase側に反映されます。
コードは長いですが、そこまで難しいことは行っておりません。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { redirect } from "next/navigation";
import { SubmitButton } from "../../components/SubmitButton";
import { Database } from "@/types/supabase";

export default function JobSeekerBasic() {
  const [message, setMessage] = useState("");
  const supabase = createClient();
  const [nationalityData, setNationalityData] = useState<
    Database["public"]["Tables"]["mst_nationality"]["Row"][]
  >([]);
  const [occupationData, setOccupationData] = useState<
    Database["public"]["Tables"]["mst_occupation"]["Row"][]
  >([]);
  const [residenceQualificationData, setResidenceQualificationData] = useState<
    Database["public"]["Tables"]["mst_residence_qualification"]["Row"][]
  >([]);

  // フォームデータを保持する
  const [lastname, setLastname] = useState("");
  const [firstname, setFirstname] = useState("");
  const [middlename, setMiddlename] = useState("");
  const [gender, setGender] = useState("");
  const [birthday, setBirthday] = useState("");
  const [zipcode, setZipcode] = useState("");
  const [address1, setAddress1] = useState("");
  const [address2, setAddress2] = useState("");
  const [phone, setPhone] = useState("");
  const [email1, setEmail1] = useState("");
  const [email2, setEmail2] = useState("");
  const [email3, setEmail3] = useState("");
  const [nationality_id, setNationality_id] = useState<string | null>(null);
  const [current_annual_income, setCurrent_annual_income] = useState<number>();
  const [desired_annual_income, setDesired_annual_income] = useState<number>();
  const [spouse, setSpouse] = useState<string | null>(null);
  const [desired_occupation_id_1, setDesired_occupation_id_1] = useState<
    string | null
  >(null);
  const [desired_occupation_id_2, setDesired_occupation_id_2] = useState<
    string | null
  >(null);
  const [desired_occupation_id_3, setDesired_occupation_id_3] = useState<
    string | null
  >(null);
  const [desired_change_job_date, setDesired_change_job_date] = useState<
    string | null
  >(null);
  const [residence_qualification_id, setResidence_qualification_id] = useState<
    string | null
  >(null);
  const [residence_qualification_expired, setResidence_qualification_expired] =
    useState<string | null>(null);
  const [
    residence_qualification_front_image_url,
    setResidence_qualification_front_image_url,
  ] = useState("");
  const [
    residence_qualification_back_image_url,
    setResidence_qualification_back_image_url,
  ] = useState("");
  const [profile_image_url, setProfile_image_url] = useState("");
  const [resume_file_url, setResume_file_url] = useState("");
  const [resume_file_name, setResume_file_name] = useState("");

  useEffect(() => {
    getNationalityData();
    getOccupationData();
    getResidenceQualificationData();
    setData();
  }, []);

  const setData = async () => {
    const { data, error } = await supabase.from("mst_job_seeker").select();

    if (data != null && data?.length != 0) {
      const job_seeker_data: Database["public"]["Tables"]["mst_job_seeker"]["Row"] =
        data[0];
      setLastname(job_seeker_data.last_name);
      setFirstname(job_seeker_data.first_name);
      if (job_seeker_data.middle_name != null) {
        setMiddlename(job_seeker_data.middle_name);
      }
      setGender("" + job_seeker_data.gender);
      setBirthday(job_seeker_data.birthday);
      if (job_seeker_data.zipcode != null) {
        setZipcode(job_seeker_data.zipcode);
      }
      if (job_seeker_data.address1 != null) {
        setAddress1(job_seeker_data.address1);
      }
      if (job_seeker_data.address2 != null) {
        setAddress2(job_seeker_data.address2);
      }
      if (job_seeker_data.phone != null) {
        setPhone(job_seeker_data.phone);
      }
      setEmail1(job_seeker_data.email1);
      if (job_seeker_data.email2 != null) {
        setEmail2(job_seeker_data.email2);
      }
      if (job_seeker_data.email3 != null) {
        setEmail3(job_seeker_data.email3);
      }
      if (job_seeker_data.nationality_id != null) {
        setNationality_id(job_seeker_data.nationality_id);
      }
      if (job_seeker_data.current_annual_income != null) {
        setCurrent_annual_income(job_seeker_data.current_annual_income);
      }
      if (job_seeker_data.desired_annual_income != null) {
        setDesired_annual_income(job_seeker_data.desired_annual_income);
      }
      if (job_seeker_data.spouse != null) {
        setSpouse("" + job_seeker_data.spouse);
      }
      if (job_seeker_data.desired_occupation_id_1 != null) {
        setDesired_occupation_id_1(job_seeker_data.desired_occupation_id_1);
      }
      if (job_seeker_data.desired_occupation_id_2 != null) {
        setDesired_occupation_id_2(job_seeker_data.desired_occupation_id_2);
      }
      if (job_seeker_data.desired_occupation_id_3 != null) {
        setDesired_occupation_id_3(job_seeker_data.desired_occupation_id_3);
      }
      if (job_seeker_data.desired_change_job_date != null) {
        setDesired_change_job_date(job_seeker_data.desired_change_job_date);
      }
      if (job_seeker_data.residence_qualification_id != null) {
        setResidence_qualification_id(
          job_seeker_data.residence_qualification_id
        );
      }
      if (job_seeker_data["residence qualification_expired"] != null) {
        setResidence_qualification_expired(
          job_seeker_data["residence qualification_expired"]
        );
      }
      if (job_seeker_data["residence qualification_front_image_url"] != null) {
        setResidence_qualification_front_image_url(
          job_seeker_data["residence qualification_front_image_url"]
        );
      }
      if (job_seeker_data["residence qualification_back_image_url"] != null) {
        setResidence_qualification_back_image_url(
          job_seeker_data["residence qualification_back_image_url"]
        );
      }
      if (job_seeker_data.profile_image_url != null) {
        setProfile_image_url(job_seeker_data.profile_image_url);
      }
      if (job_seeker_data.resume_file_url != null) {
        setResume_file_url(job_seeker_data.resume_file_url);
      }
      if (job_seeker_data.resume_file_name != null) {
        setResume_file_name(job_seeker_data.resume_file_name);
      }
    }
  };

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

    const fixed_data: Database["public"]["Tables"]["mst_nationality"]["Row"][] =
      data;
    setNationalityData(fixed_data);
  };

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

    const fixed_data: Database["public"]["Tables"]["mst_occupation"]["Row"][] =
      data;
    setOccupationData(fixed_data);
  };

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

    const fixed_data: Database["public"]["Tables"]["mst_residence_qualification"]["Row"][] =
      data;
    setResidenceQualificationData(fixed_data);
  };

  const onSubmit = async () => {
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      return redirect("/login");
    }
    const timeStamp = new Date().toISOString();

    const { error } = await supabase.from("mst_job_seeker").upsert({
      user_uid: user.id,
      last_name: lastname,
      first_name: firstname,
      middle_name: middlename,
      gender: parseInt(gender),
      birthday: birthday,
      zipcode: zipcode,
      address1: address1,
      address2: address2,
      phone: phone,
      email1: email1,
      email2: email2,
      email3: email3,
      nationality_id: nationality_id,
      current_annual_income: current_annual_income,
      desired_annual_income: desired_annual_income,
      spouse: spouse,
      desired_occupation_id_1: desired_occupation_id_1,
      desired_occupation_id_2: desired_occupation_id_2,
      desired_occupation_id_3: desired_occupation_id_3,
      desired_change_job_date: desired_change_job_date,
      residence_qualification_id: residence_qualification_id,
      "residence qualification_expired": residence_qualification_expired,
      "residence qualification_front_image_url":
        residence_qualification_front_image_url,
      "residence qualification_back_image_url":
        residence_qualification_back_image_url,
      profile_image_url: profile_image_url,
      resume_file_url: resume_file_url,
      resume_file_name: resume_file_name,
      updated_at: timeStamp,
    });
    console.log(error);

    if (error) {
      setMessage("保存に失敗しました。");
      return;
    } else {
      setMessage("");
    }
    return redirect("/userpage");
  };

  return (
    <div className="flex-1 flex flex-col w-96 py-8 sm:max-w-md justify-center gap-2">
      <form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
        <h2>求職者の登録情報</h2>
        <label className="text-md" htmlFor="lastname">
          姓*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="lastname"
          placeholder="山田"
          value={lastname}
          onChange={(e) => setLastname(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="firstname">
          名*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="firstname"
          placeholder="太郎"
          value={firstname}
          onChange={(e) => setFirstname(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="middlename">
          ミドルネーム
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="middlename"
          value={middlename}
          onChange={(e) => setMiddlename(e.target.value)}
          placeholder="ミドルネーム"
        />
        <label className="text-md" htmlFor="gender">
          性別*
        </label>
        <select
          name="gender"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={gender}
          onChange={(e) => setGender(e.target.value)}
          required
        >
          <option value="">--選択してください--</option>
          <option value="1">男性</option>
          <option value="0">女性</option>
          <option value="2">その他</option>
        </select>
        <label className="text-md" htmlFor="birthday">
          誕生日*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="birthday"
          value={birthday}
          onChange={(e) => setBirthday(e.target.value)}
          type="date"
          required
        />
        <label className="text-md" htmlFor="zipcode">
          郵便番号
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="zipcode"
          value={zipcode}
          onChange={(e) => setZipcode(e.target.value)}
          placeholder="0000000 ※ハイフンなしでご入力ください"
        />

        <label className="text-md" htmlFor="address1">
          住所1
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="address1"
          value={address1}
          onChange={(e) => setAddress1(e.target.value)}
          placeholder="東京都港区浜松町2丁目2番15号"
        />
        <label className="text-md" htmlFor="address2">
          住所2
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="address2"
          value={address2}
          onChange={(e) => setAddress2(e.target.value)}
          placeholder="浜松町ダイヤビル2F"
        />

        <label className="text-md" htmlFor="phone">
          電話番号
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="phone"
          value={phone}
          onChange={(e) => setPhone(e.target.value)}
          placeholder="00000000000 ※ハイフンなしでご入力ください"
        />

        <label className="text-md" htmlFor="email1">
          メールアドレス1*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email1"
          value={email1}
          onChange={(e) => setEmail1(e.target.value)}
          placeholder="email1@example.com"
          type="email"
          required
        />
        <label className="text-md" htmlFor="email2">
          メールアドレス2
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email2"
          value={email2}
          onChange={(e) => setEmail2(e.target.value)}
          placeholder="email2@example.com"
          type="email"
        />
        <label className="text-md" htmlFor="email3">
          メールアドレス3
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email3"
          value={email3}
          onChange={(e) => setEmail3(e.target.value)}
          placeholder="email3@example.com"
          type="email"
        />

        <label className="text-md" htmlFor="nationalityId">
          国籍
        </label>
        <select
          name="nationalityId"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={nationality_id ? nationality_id : ""}
          onChange={(e) => setNationality_id(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {nationalityData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.nationality}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="current_annual_income">
          現在の年収
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="current_annual_income"
          value={current_annual_income}
          onChange={(e) => setCurrent_annual_income(parseInt(e.target.value))}
          placeholder="現在の年収"
          type="number"
        />

        <label className="text-md" htmlFor="desired_annual_income">
          希望年収
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="desired_annual_income"
          value={desired_annual_income}
          onChange={(e) => setDesired_annual_income(parseInt(e.target.value))}
          placeholder="希望年収"
          type="number"
        />

        <label className="text-md" htmlFor="spouse">
          配偶者
        </label>
        <select
          name="spouse"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={spouse ? spouse : ""}
          onChange={(e) => setSpouse(e.target.value)}
        >
          <option value="">--選択してください--</option>
          <option value="false">なし</option>
          <option value="true">あり</option>
        </select>

        <label className="text-md" htmlFor="desired_occupation_id_1">
          希望職種1
        </label>
        <select
          name="desired_occupation_id_1"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={desired_occupation_id_1 ? desired_occupation_id_1 : ""}
          onChange={(e) => setDesired_occupation_id_1(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="desired_occupation_id_2">
          希望職種2
        </label>
        <select
          name="desired_occupation_id_2"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={desired_occupation_id_2 ? desired_occupation_id_2 : ""}
          onChange={(e) => setDesired_occupation_id_2(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="desired_occupation_id_3">
          希望職種3
        </label>
        <select
          name="desired_occupation_id_3"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={desired_occupation_id_3 ? desired_occupation_id_3 : ""}
          onChange={(e) => setDesired_occupation_id_3(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="desired_change_job_date">
          転職希望日
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="desired_change_job_date"
          value={desired_change_job_date ? desired_change_job_date : ""}
          onChange={(e) => setDesired_change_job_date(e.target.value)}
          type="date"
        />

        <label className="text-md" htmlFor="residence_qualification_id">
          在留資格
        </label>
        <select
          name="residence_qualification_id"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={residence_qualification_id ? residence_qualification_id : ""}
          onChange={(e) => setResidence_qualification_id(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {residenceQualificationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.residence_qualification}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="residence_qualification_expired">
          在留資格期限
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="residence_qualification_expired"
          value={
            residence_qualification_expired
              ? residence_qualification_expired
              : ""
          }
          onChange={(e) => setResidence_qualification_expired(e.target.value)}
          type="date"
        />

        <label
          className="text-md"
          htmlFor="residence_qualification_front_image_url"
        >
          在留資格カード表のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="residence_qualification_front_image_url"
          value={residence_qualification_front_image_url}
          onChange={(e) =>
            setResidence_qualification_front_image_url(e.target.value)
          }
          placeholder="在留資格カード表のファイルURL"
        />

        <label
          className="text-md"
          htmlFor="residence_qualification_back_image_url"
        >
          在留資格カード裏のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="residence_qualification_back_image_url"
          value={residence_qualification_back_image_url}
          onChange={(e) =>
            setResidence_qualification_back_image_url(e.target.value)
          }
          placeholder="在留資格カード裏のファイルURL"
        />

        <label className="text-md" htmlFor="profile_image_url">
          プロフィール画像のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="profile_image_url"
          value={profile_image_url}
          onChange={(e) => setProfile_image_url(e.target.value)}
          placeholder="プロフィール画像のファイルURL"
        />

        <label className="text-md" htmlFor="resume_file_url">
          履歴書のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="resume_file_url"
          value={resume_file_url}
          onChange={(e) => setResume_file_url(e.target.value)}
          placeholder="履歴書のファイルURL"
        />
        <label className="text-md" htmlFor="resume_file_name">
          履歴書のファイル名
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="resume_file_name"
          value={resume_file_name}
          onChange={(e) => setResume_file_name(e.target.value)}
          placeholder="履歴書のファイル名"
        />

        <SubmitButton
          formAction={onSubmit}
          className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"
          pendingText="ユーザ情報更新中..."
        >
          保存
        </SubmitButton>
        {message !== "" && (
          <p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
            {message}
          </p>
        )}
      </form>
    </div>
  );
}

これで今回の実装はできたため、確認してみましょう!

実装の確認

masterにプッシュしてvercelのデプロイ完了を待ちます。
デプロイが終わったら、求職者側のドメインで作成したユーザでログインします。

メールアドレスをクリックし、ユーザページにアクセスすると求職者の登録情報画面がまず表示されます。

必須項目中心に入力して情報を更新すると、保存が完了し、Supabase側でも行が追加されたことが確認できます。

求職者側のプロファイルの変更ができることが確認できました!

その他参考資料など

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

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

お問合せ&各種リンク

presented by

  • システム開発、アプリ開発
  • マッチングアプリ開発
  • インフラ構築支援等、なんでもご相談ください。
よかったらシェアしてね!
  • URLをコピーしました!
目次