Next.js とSupabaseで求人マッチングアプリを作る⑧~求人一覧、求人詳細情報~

ここまで作ってきたマッチングアプリも、だいぶ形になってきましたね。
今回は求職者一覧求職者詳細を作成します。

  • 企業一覧
  • 企業詳細
  • 求職者一覧
  • 求職者詳細
  • 求人一覧
  • 求人詳細

これも求職者一覧・求職者詳細と同じ形で作成ができるので、作り方をマスターした、という人は自力で作れるか試してみましょう。

目次

Next.jsの実装

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 { getUserTypeFromEnv, userType } from "@/utils/usertype";
import JobList from "@/components/jobseeker/JobList";

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.job_seeker) {
    return redirect("/");
  }

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

app/jobdetail/page.tsx

求人の詳細情報ページです。
求人IDをurlパラメータに渡すと、その求人の情報を子コンポーネントで取得できます。

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

export default async function JobDetailPage({
  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.job_seeker) {
    return redirect("/");
  }

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

components/jobseeker/JobList.tsx

求人一覧を表示するコンポーネントです。
取得した求人情報をすべて一覧で表示します。

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

export default function JobList() {
  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>
      <ul>
        {jobData.map((item) => (
          <li
            key={item.id}
            className="bg-gray-200 rounded-md p-5 mt-4 hover:bg-gray-300"
          >
            <Link href={"/jobdetail?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 == 1
                      ? "正社員"
                      : item.employment_class == 2
                      ? "契約社員"
                      : "アルバイト"}
                  </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>
  );
}

components/jobseeker/JobDetail.tsx

求人一覧より、リンクを通じて遷移してくる詳細ページのコンポーネントです。
現在画像アップロードの仕組みはないため、画像URLの表示はスキップしています。

 "use client";
import {createClient} from "@/utils/supabase/client";
import {useState, useEffect} from "react";
import {Database} from "@/types/supabase";
import DetailList from "@/components/common/DetailList";

type Props = {
    jobid: string | null;
};
export default function JobDetail({jobid}: Props) {
    const supabase = createClient();

    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_name, setOccupation_name] = useState<string>("");
    const [industry_name, setIndustry_name] = 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(() => {
        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);
        const work_location_name = await getWorklocationName(
            tmp_data.work_location
        );
        setWork_location(work_location_name);
        setWork_location_detail(tmp_data.work_location_detail);
        setWorking_hours(tmp_data.working_hours);
        const day_off_name = await getDayoffName(tmp_data.day_off);
        setDay_off(day_off_name);
        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);
        const occupation_name = await getOccupationName(tmp_data.occupation_id);
        setOccupation_name(occupation_name);
        const industry_name = await getIndustryName(tmp_data.industry_id);
        setIndustry_name(industry_name);
        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 getWorklocationName = async (id: string) => {
        const {data, error} = await supabase
            .from("mst_work_location")
            .select()
            .eq("id", id);
        if (error) {
            console.log(error);
            return;
        }

        return data[0].work_location;
    };

    const getDayoffName = async (id: string) => {
        const {data, error} = await supabase
            .from("mst_day_off")
            .select()
            .eq("id", id);
        if (error) {
            console.log(error);
            return;
        }

        return data[0].day_off;
    };

    const getOccupationName = async (id: string) => {
        const {data, error} = await supabase
            .from("mst_occupation")
            .select()
            .eq("id", id);
        if (error) {
            console.log(error);
            return;
        }

        return data[0].occupation;
    };

    const getIndustryName = async (id: string) => {
        const {data, error} = await supabase
            .from("mst_industry")
            .select()
            .eq("id", id);
        if (error) {
            console.log(error);
            return;
        }

        return data[0].industry;
    };

    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>
                <DetailList title={"案件名"} value={name}/>
                <DetailList title={"概要"} value={description}/>
                <DetailList title={"勤務地"} value={work_location}/>
                <DetailList title={"勤務地詳細"} value={work_location_detail}/>
                <DetailList title={"勤務時間"} value={working_hours}/>
                <DetailList title={"休日"} value={day_off}/>
                <DetailList title={"休日詳細"} value={day_off_detail}/>
                <DetailList title={"雇用区分"} value={employment_class == 1
                    ? "正社員"
                    : employment_class == 2
                    ? "契約社員"
                    : "アルバイト"}/>
                <DetailList title={"想定年収"} value={annual_income + "万円"}/>
                <DetailList title={"想定年収詳細"} value={annual_income_detail}/>
                <DetailList title={"待遇"} value={treatment}/>
                <DetailList title={"福利厚生"} value={employee_benefits}/>
                <DetailList title={"応募資格"} value={qualification}/>
                <DetailList title={"必須スキル"} value={required_skills}/>
                <DetailList title={"歓迎スキル"} value={skills}/>
                <DetailList title={"職種"} value={occupation_name}/>
                <DetailList title={"業界"} value={industry_name}/>
            </ul>
        </div>
    );
}

それでは実装を確認してみます。

実装の確認

求職者側のアプリにアクセスし、ログインしましょう。

求人一覧へのリンクがヘッダーに表示されることがわかります。
これをクリックすると、求人の一覧ページにアクセスできます。
企業側のアプリで作成した求人情報の一覧が表示されます。
(現在一つしか入れていないので一つしか表示されませんが、複数追加すれば複数表示されます。)

こちらの求人リンクをクリックすると求人の詳細情報が表示されます。

これで求人の一覧情報と詳細情報へアクセスできるようになりました。

その他参考資料など

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

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

お問合せ&各種リンク

presented by

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