Next.js とSupabaseで求人マッチングアプリを作る⑤~求人募集の登録・修正機能~

今回は前回の続きで求人募集の登録・修正機能とそのUIを追加します。

これも『企業のプロファイル』『求職者のプロファイル』と同じように実装したいところですが、少し違う点があります。
例えば『”新規作成”と”既存求人の編集”の切り替え』や『作成・編集の権限』等です。
そのあたりに注意して構築を行いましょう。

目次

Supabase側準備

求人テーブルの作成(trn_job)

Supabaseのダッシュボードから`Table Editor`→`New Table`を押下し、新しいテーブルを作成します。

テーブルの列設定は以下のような形です。

  • id
    • uuid型
    • デフォルト値は`get_random_uuid()`
    • 主キー
  • company_uid
    • uuid型
    • 企業ユーザ(mst_company)のidを外部キーとして連携します。
    • not null
  • name
    • varchar型
    • not null
    • 案件名
  • description
    • text型
    • not null
    • 説明
  • work_location
    • varchar型
    • not null
    • 勤務地
  • work_location_detail
    • text型
    • 勤務地詳細
  • working_hours
    • varchar型
    • not null
    • 勤務時間
  • day_off
    • varchar型
    • not null
    • 休日(mst_day_off)IDを外部キーとして連携します。
  • day_off_detail
    • text型
    • 休日詳細
  • employment_class
    • integer型
    • not null
    • 雇用区分
    • 1:正社員、2:契約社員、3:アルバイトといった形で設定します。
  • annual_income
    • integer
    • not null
    • 想定年収
  • annual_income_detail
    • text型
    • 想定年収詳細
  • treatment
    • text型
    • 待遇
  • employee_benefits
    • text型
    • 福利厚生
  • qualification
    • text型
    • not null
    • 応募資格
  • required_skills
    • text型
    • 必須スキル
  • skills
    • text型
    • 歓迎スキル
  • occupation_id
    • varchar型
    • not null
    • 職種(mst_occupation)のidを外部キーとして連携します。
  • industry_id
    • varchar型
    • not null
    • 業界(mst_industry)のidを外部キーとして連携します。
  • job_image_url_1
    • text型
    • 画像URL1
  • job_image_url_2
    • text型
    • 画像URL2
  • job_image_url_3
    • text型
    • 画像URL3
  • job_image_url_4
    • text型
    • 画像URL4
  • job_image_url_5
    • text型
    • 画像URL5
  • published_at
    • timestamp_tz型
    • not null
    • 公開日
    • デフォルト値はnow()
  • unpublished_at
    • timestamp_tz型
    • 非公開日
  • created_at
    • timestamp_tz型
    • not null
    • 作成日
    • デフォルト値はnow()
  • updated_at
    • timestamp_tz型
    • not null
    • 更新日
    • デフォルト値はnow()
  • delete_flg
    • bool型
    • not null
    • 削除フラグ
    • デフォルト値はfalse

ポリシーの設定

求人テーブルのポリシーは

  • 誰でも閲覧できるようにする
    • selectがtrue
  • 作成、更新は企業のアカウントしかできないようにする
    • insert と updateを(auth.uid() = company_uid)

とします。

参考画像

Next.jsの実装

前回までとの変更点を記します。

Supabaseの型生成

まずSupabase CLIでログインします。

npx supabase login

その後、下記で型を生成します。
$PROJECT_REFにはSupabaseプロジェクトのReference IDが入ります。

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

supabase.tsに`trn_job`のテーブル情報が追加されればOKです。

components/Header.tsx

今回の実装に合わせてヘッダーに自社の求人一覧へのリンクを追加します。
こちらは企業ユーザでログインしたときのみ表示されます。

import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect } from "next/navigation";
import { userType, getUserTypeFromEnv } from "@/utils/usertype";

export default async function Header() {
  const supabase = createClient();

  const usertype = getUserTypeFromEnv();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  const signOut = async () => {
    "use server";

    const supabase = createClient();
    await supabase.auth.signOut();
    return redirect("/login");
  };

  return (
    <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
      <div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
        {user && usertype == userType.company ? (
          <div className="flex items-center gap-4">
            <Link href="/joblist">自社の求人一覧</Link>
            <Link href="/userpage">ユーザページ</Link>
            <form action={signOut}>
              <button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
                ログアウト
              </button>
            </form>
          </div>
        ) : user && usertype == userType.job_seeker ? (
          <div className="flex items-center gap-4">
            <Link href="/userpage">ユーザページ</Link>
            <form action={signOut}>
              <button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
                ログアウト
              </button>
            </form>
          </div>
        ) : (
          <Link
            href="/login"
            className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
          >
            ログイン
          </Link>
        )}
      </div>
    </nav>
  );
}

見た目はこんな感じになります。

components/UserInfo.tsx

ユーザ情報を分岐させるコンポーネントです。
前回テーブルから取得して作成していましたが、最適化して環境変数で分岐するよう変更しました。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useEffect, useState } from "react";
import { userType, getUserTypeFromEnv } from "@/utils/usertype";
import { userInfoContentsType } from "@/utils/userinfocontentstype";
import MainContents from "./userinfo/MainContents";

export default function UserInfo() {
  const [usertype, setUsertype] = useState<userType>(userType.none);
  const [userInfoContents, setUserInfoContents] =
    useState<userInfoContentsType>(userInfoContentsType.none);
  const supabase = createClient();

  const getUsertype = async () => {
const usertype = getUserTypeFromEnv();
    switch (usertype) {
      case userType.job_seeker:
        setUsertype(userType.job_seeker);
        setUserInfoContents(userInfoContentsType.job_seeker_basic);
        break;
      case userType.company:
        setUsertype(userType.company);
        setUserInfoContents(userInfoContentsType.company_basic);
        break;
      case userType.admin:
        setUsertype(userType.admin);
        break;

      default:
        break;
    }
  };

  useEffect(() => {
    getUsertype();
  }, []);

  return (
    <div className="animate-in flex-1 flex gap-20 opacity-0 max-w-4xl px-3">
      {usertype == userType.job_seeker ? (
        <>
          <aside
            id="default-sidebar"
            className="w-48 h-auto transition-transform -translate-x-full sm:translate-x-0"
            aria-label="Sidebar"
          >
            <div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
              <ul className="space-y-2 font-medium">
                <li
                  className="text-center p-2 text-gray-900 rounded-lg hover:bg-gray-100"
                  onClick={() =>
                    setUserInfoContents(userInfoContentsType.job_seeker_basic)
                  }
                >
                  登録情報
                </li>
              </ul>
            </div>
          </aside>
          <main className="flex-1 flex flex-col gap-6 w-full">
            <MainContents contentsType={userInfoContents}></MainContents>
          </main>
        </>
      ) : usertype == userType.company ? (
        <>
          <aside
            id="default-sidebar"
            className="w-48 h-auto transition-transform -translate-x-full sm:translate-x-0"
            aria-label="Sidebar"
          >
            <div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
              <ul className="space-y-2 font-medium">
                <li
                  className="text-center p-2 text-gray-900 rounded-lg hover:bg-gray-100"
                  onClick={() =>
                    setUserInfoContents(userInfoContentsType.company_basic)
                  }
                >
                  登録情報
                </li>
              </ul>
            </div>
          </aside>
          <main className="flex-1 flex flex-col gap-6 w-full">
            <MainContents contentsType={userInfoContents}></MainContents>
          </main>
        </>
      ) : usertype == userType.admin ? (
        <>管理者ページ</>
      ) : (
        <>
          <aside
            id="default-sidebar"
            className="w-48 h-auto transition-transform -translate-x-full sm:translate-x-0"
            aria-label="Sidebar"
          >
            <div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
              <ul className="space-y-2 font-medium"></ul>
            </div>
          </aside>
          <main className="flex-1 flex flex-col gap-6 w-full">
            <MainContents contentsType={userInfoContents}></MainContents>
          </main>
        </>
      )}
    </div>
  );
}

app/jobedit/page.tsx

求人情報編集、作成ページです。
パラメータに与えられたjobidをもとに情報取得し表示、そのうえで情報の編集ができます。
jobidがないときは新規作成となります。

import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { userType, getUserTypeFromEnv } from "@/utils/usertype";
import MyJobEdit from "@/components/company/MyJobEdit";

export default async function JobEditPage({
  searchParams,
}: {
  searchParams: { jobid: string };
}) {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    return redirect("/login");
  }

  const usertype = getUserTypeFromEnv();

  if (usertype !== userType.company) {
    return redirect("/");
  }

  return (
    <div className="flex-1 w-full flex flex-col items-center">
      <Header />
      <MyJobEdit jobid={searchParams.jobid}></MyJobEdit>
      <Footer />
    </div>
  );
}

app/joblist/page.tsx

求人情報一覧ページになります。
企業アカウントに結びついた求人情報が一覧で表示され、求人の新規作成や編集ができます。

import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import MyJobList from "@/components/company/MyJobList";
import { getUserTypeFromEnv, userType } from "@/utils/usertype";

export default async function JobListPage() {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    return redirect("/login");
  }

  const usertype = getUserTypeFromEnv();

  if (usertype !== userType.company) {
    return redirect("/");
  }

  return (
    <div className="flex-1 w-full flex flex-col items-center">
      <Header />
      <MyJobList></MyJobList>
      <Footer />
    </div>
  );
}

components/company/MyJobEdit.tsx

求人編集の実際の機能を持つコンポーネントです。

"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";
import Link from "next/link";

/**
 * 自社の求人編集ページ
 */
type Props = {
  jobid: string | null;
};
export default function MyJobEdit({ jobid }: Props) {
  const [message, setMessage] = useState("");
  const supabase = createClient();
  const [work_locationData, setWork_locationData] = useState<
    Database["public"]["Tables"]["mst_work_location"]["Row"][]
  >([]);
  const [day_offData, setDay_offData] = useState<
    Database["public"]["Tables"]["mst_day_off"]["Row"][]
  >([]);
  const [occupation, setOccupation] = useState<
    Database["public"]["Tables"]["mst_occupation"]["Row"][]
  >([]);
  const [industry, setIndustry] = useState<
    Database["public"]["Tables"]["mst_industry"]["Row"][]
  >([]);

  const [name, setName] = useState<string>("");
  const [description, setDescription] = useState<string>("");
  const [work_location, setWork_location] = useState<string>("");
  const [work_location_detail, setWork_location_detail] = useState<
    string | null
  >(null);
  const [working_hours, setWorking_hours] = useState<string>("");
  const [day_off, setDay_off] = useState<string>("");
  const [day_off_detail, setDay_off_detail] = useState<string | null>(null);
  const [employment_class, setEmployment_class] = useState<number>();
  const [annual_income, setAnnual_income] = useState<number>();
  const [annual_income_detail, setAnnual_income_detail] = useState<
    string | null
  >(null);
  const [treatment, setTreatment] = useState<string | null>(null);
  const [employee_benefits, setEmployee_benefits] = useState<string | null>(
    null
  );
  const [qualification, setQualification] = useState<string>("");
  const [required_skills, setRequired_skills] = useState<string | null>(null);
  const [skills, setSkills] = useState<string | null>(null);
  const [occupation_id, setOccupation_id] = useState<string>("");
  const [industry_id, setIndustry_id] = useState<string>("");
  const [job_image_url_1, setJob_image_url_1] = useState<string | null>(null);
  const [job_image_url_2, setJob_image_url_2] = useState<string | null>(null);
  const [job_image_url_3, setJob_image_url_3] = useState<string | null>(null);
  const [job_image_url_4, setJob_image_url_4] = useState<string | null>(null);
  const [job_image_url_5, setJob_image_url_5] = useState<string | null>(null);

  useEffect(() => {
    getDayoffData();
    getWorklocationData();
    getOccupation();
    getIndustry();
    getJobDataFromId();
  }, []);

  const getJobDataFromId = async () => {
    if (jobid === "") {
      return;
    }

    const { data, error } = await supabase
      .from("trn_job")
      .select()
      .eq("id", jobid);
    if (error) {
      console.log(error);
      return;
    }

    console.log(data);

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

    setName(tmp_data.name);
    setDescription(tmp_data.description);
    setWork_location(tmp_data.work_location);
    setWork_location_detail(tmp_data.work_location_detail);
    setWorking_hours(tmp_data.working_hours);
    setDay_off(tmp_data.day_off);
    setDay_off_detail(tmp_data.day_off_detail);
    setEmployment_class(tmp_data.employment_class);
    setAnnual_income(tmp_data.annual_income);
    setAnnual_income_detail(tmp_data.annual_income_detail);
    setTreatment(tmp_data.treatment);
    setEmployee_benefits(tmp_data.employee_benefits);
    setQualification(tmp_data.qualification);
    setRequired_skills(tmp_data.required_skills);
    setSkills(tmp_data.skills);
    setOccupation_id(tmp_data.occupation_id);
    setIndustry_id(tmp_data.industry_id);
    setJob_image_url_1(tmp_data.job_image_url_1);
    setJob_image_url_2(tmp_data.job_image_url_2);
    setJob_image_url_3(tmp_data.job_image_url_3);
    setJob_image_url_4(tmp_data.job_image_url_4);
    setJob_image_url_5(tmp_data.job_image_url_5);
  };

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

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

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

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

  const getOccupation = 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;
    setOccupation(fixed_data);
  };

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

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

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

    let dataForUpsert = {};

    let newId = crypto.randomUUID();

    if (jobid === "") {
      dataForUpsert = {
        id: newId,
        company_uid: user.id,
        name: name,
        description: description,
        work_location: work_location,
        work_location_detail: work_location_detail,
        working_hours: working_hours,
        day_off: day_off,
        day_off_detail: day_off_detail,
        employment_class: employment_class,
        annual_income: annual_income,
        annual_income_detail: annual_income_detail,
        treatment: treatment,
        employee_benefits: employee_benefits,
        qualification: qualification,
        required_skills: required_skills,
        skills: skills,
        occupation_id: occupation_id,
        industry_id: industry_id,
        job_image_url_1: job_image_url_1,
        job_image_url_2: job_image_url_2,
        job_image_url_3: job_image_url_3,
        job_image_url_4: job_image_url_4,
        job_image_url_5: job_image_url_5,
        updated_at: timeStamp,
      };
    } else {
      dataForUpsert = {
        id: jobid,
        company_uid: user.id,
        name: name,
        description: description,
        work_location: work_location,
        work_location_detail: work_location_detail,
        working_hours: working_hours,
        day_off: day_off,
        day_off_detail: day_off_detail,
        employment_class: employment_class,
        annual_income: annual_income,
        annual_income_detail: annual_income_detail,
        treatment: treatment,
        employee_benefits: employee_benefits,
        qualification: qualification,
        required_skills: required_skills,
        skills: skills,
        occupation_id: occupation_id,
        industry_id: industry_id,
        job_image_url_1: job_image_url_1,
        job_image_url_2: job_image_url_2,
        job_image_url_3: job_image_url_3,
        job_image_url_4: job_image_url_4,
        job_image_url_5: job_image_url_5,
        updated_at: timeStamp,
      };
    }

    const { error } = await supabase.from("trn_job").upsert(dataForUpsert);
    console.log(error);

    if (error) {
      setMessage("保存に失敗しました。");
      return;
    } else {
      setMessage("");
    }
    return redirect("/jobedit?jobid=" + (jobid ? jobid : newId));
  };

  return (
    <div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
      <h1>求人の編集</h1>
      <form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
        <label className="text-md" htmlFor="name">
          求人名*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="name"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="description">
          求人説明*
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="description"
          value={description}
          rows={10}
          onChange={(e) => setDescription(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="work_location">
          勤務地*
        </label>
        <select
          name="work_location"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={work_location}
          required
          onChange={(e) => setWork_location(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {work_locationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.work_location}
            </option>
          ))}
        </select>
        <label className="text-md" htmlFor="work_location_detail">
          勤務地詳細
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="work_location_detail"
          value={work_location_detail ? work_location_detail : ""}
          rows={5}
          onChange={(e) => setWork_location_detail(e.target.value)}
        />
        <label className="text-md" htmlFor="working_hours">
          勤務時間*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="working_hours"
          value={working_hours}
          required
          onChange={(e) => setWorking_hours(e.target.value)}
        />
        <label className="text-md" htmlFor="day_off">
          休日*
        </label>
        <select
          name="day_off"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={day_off}
          required
          onChange={(e) => setDay_off(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {day_offData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.day_off}
            </option>
          ))}
        </select>
        <label className="text-md" htmlFor="day_off_detail">
          勤務地詳細
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="day_off_detail"
          value={day_off_detail ? day_off_detail : ""}
          rows={10}
          onChange={(e) => setDay_off_detail(e.target.value)}
        />
        <label className="text-md" htmlFor="employment_class">
          雇用区分*
        </label>
        <select
          name="employment_class"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={employment_class}
          required
          onChange={(e) => setEmployment_class(parseInt(e.target.value))}
        >
          <option value="">--選択してください--</option>
          <option value="1">正社員</option>
          <option value="2">契約社員</option>
          <option value="3">アルバイト</option>
        </select>
        <label className="text-md" htmlFor="annual_income">
          想定年収*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="annual_income"
          value={annual_income}
          onChange={(e) => setAnnual_income(parseInt(e.target.value))}
          required
          type="number"
        />
        <label className="text-md" htmlFor="annual_income_detail">
          想定年収詳細
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="annual_income_detail"
          value={annual_income_detail ? annual_income_detail : ""}
          rows={5}
          onChange={(e) => setAnnual_income_detail(e.target.value)}
        />
        <label className="text-md" htmlFor="treatment">
          待遇
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="treatment"
          value={treatment ? treatment : ""}
          rows={5}
          onChange={(e) => setTreatment(e.target.value)}
        />
        <label className="text-md" htmlFor="employee_benefits">
          福利厚生
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="employee_benefits"
          value={employee_benefits ? employee_benefits : ""}
          rows={5}
          onChange={(e) => setEmployee_benefits(e.target.value)}
        />
        <label className="text-md" htmlFor="qualification">
          応募資格*
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="qualification"
          value={qualification}
          rows={5}
          required
          onChange={(e) => setQualification(e.target.value)}
        />
        <label className="text-md" htmlFor="required_skills">
          必須スキル
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="required_skills"
          value={required_skills ? required_skills : ""}
          rows={5}
          onChange={(e) => setRequired_skills(e.target.value)}
        />
        <label className="text-md" htmlFor="skills">
          歓迎スキル
        </label>
        <textarea
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="skills"
          value={skills ? skills : ""}
          rows={5}
          onChange={(e) => setSkills(e.target.value)}
        />
        <label className="text-md" htmlFor="occupation_id">
          職種*
        </label>
        <select
          name="occupation_id"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={occupation_id}
          required
          onChange={(e) => setOccupation_id(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupation.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>
        <label className="text-md" htmlFor="industry_id">
          業界*
        </label>
        <select
          name="industry_id"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={industry_id}
          required
          onChange={(e) => setIndustry_id(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {industry.map((item) => (
            <option key={item.id} value={item.id}>
              {item.industry}
            </option>
          ))}
        </select>
        <label className="text-md" htmlFor="job_image_url_1">
          募集画像1のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="job_image_url_1"
          value={job_image_url_1 ? job_image_url_1 : ""}
          onChange={(e) => setJob_image_url_1(e.target.value)}
        />
        <label className="text-md" htmlFor="job_image_url_2">
          募集画像2のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="job_image_url_2"
          value={job_image_url_2 ? job_image_url_2 : ""}
          onChange={(e) => setJob_image_url_2(e.target.value)}
        />
        <label className="text-md" htmlFor="job_image_url_3">
          募集画像3のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="job_image_url_3"
          value={job_image_url_3 ? job_image_url_3 : ""}
          onChange={(e) => setJob_image_url_3(e.target.value)}
        />
        <label className="text-md" htmlFor="job_image_url_4">
          募集画像4のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="job_image_url_4"
          value={job_image_url_4 ? job_image_url_4 : ""}
          onChange={(e) => setJob_image_url_4(e.target.value)}
        />
        <label className="text-md" htmlFor="job_image_url_5">
          募集画像5のURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="job_image_url_5"
          value={job_image_url_5 ? job_image_url_5 : ""}
          onChange={(e) => setJob_image_url_5(e.target.value)}
        />

        <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>
  );
}

components/company/MyJobList

こちらも同じように求人一覧の実態のコンポーネントです。

"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";
import Link from "next/link";

/**
 * 自社の求人一覧。作成、編集につなげることができる
 */
export default function MyJobList() {
  const supabase = createClient();
  const [jobData, setJobData] = useState<
    Database["public"]["Tables"]["trn_job"]["Row"][]
  >([]);

  useEffect(() => {
    getJobData();
  }, []);

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

    const tmp_data: Database["public"]["Tables"]["trn_job"]["Row"][] =
      data as Database["public"]["Tables"]["trn_job"]["Row"][];
    setJobData(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>
      <Link
        href={"/jobedit?jobid="}
        className="bg-green-700 rounded-md w-40 px-4 py-2 text-foreground"
      >
        新規求人作成
      </Link>
      <ul>
        {jobData.map((item) => (
          <li
            key={item.id}
            className="bg-gray-200 rounded-md p-5 mt-4 hover:bg-gray-300"
          >
            <Link href={"/jobedit?jobid=" + item.id} className="block w-full">
              <h2>{item.name}</h2>
              <div className="flex">
                <div className="w-1/2">
                  <h3>雇用形態</h3>
                  <p>{item.employment_class}</p>
                </div>
                <div className="w-1/2">
                  <h3>勤務地</h3>
                  <p>{item.work_location}</p>
                </div>
              </div>
              <div className="flex">
                <div className="w-1/2">
                  <h3>応募資格</h3>
                  <p>{item.qualification}</p>
                </div>
                <div className="w-1/2">
                  <h3>給与</h3>
                  <p>{item.annual_income}</p>
                </div>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

utils/usertype.ts

新規で`getUserTypeFromEnv()`を追加しています。
こちらは環境変数をもとにユーザタイプが何であるかを返す関数です。

export enum userType {
  job_seeker,
  company,
  admin,
  none,
}

export function getUserTypeFromEnv() {
  const env_usertype = process.env.NEXT_PUBLIC_USER_TYPE;
  let header_usertype = userType.none;
  switch (env_usertype) {
    case "job_seeker":
      header_usertype = userType.job_seeker;
      break;
    case "company":
      header_usertype = userType.company;
      break;

    case "admin":
      header_usertype = userType.admin;
      break;
  }

  return header_usertype;
}

こちらで実装はOKです。確認してみましょう。

実装の確認

企業側のアプリにログインします。

企業情報はすでに入力してある前提で進めますが、`自社の求人一覧`をクリックします。
現状だと一つも求人がないため`新規作成`をクリックします。

空の求人編集画面が表示されるので適当に入力して作成しましょう。

入力して保存を押すとSupabase側の`trn_job`に作成した求人情報が追加されます。

その後、求人一覧を見るとこのように追加されていることがわかります。


求人一覧の求人をクリックすると編集画面にまた遷移することができます。

これで実装が確認できました!

その他参考資料など

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

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

お問合せ&各種リンク

presented by

よかったらシェアしてね!
  • URLをコピーしました!
目次