Next.js とSupabaseで求人マッチングアプリを作る⑨~企業、求職者情報、求人のファイルを追加できる仕組み作成~

今回は、今まで作成してきた各種編集画面にファイルアップロードの機能を搭載し、アップロードしたファイルのURLをDBに保存する仕組みを作成します。

マッチングアプリにおいて、画像アップロード機能はほぼ必須です。
求人を行う企業の雰囲気を掴んでもらうには画像が一番ですし、
応募側にも履歴書や在留資格カードをアップする機能をつければ、スムーズに採用手続きへ入ることができます。

では早速手順を見ていきましょう。

目次

SupabaseのStorage設定

今回新たにStorageを利用するため、
Supabaseのダッシュボード→`Storage`→`New bucket`から添付画像のような設定でStorageを作成します。

ポリシー設定

ポリシーの設定も必要なため、下記のように指定します。
必要になったタイミングで編集するので、設定は認証ユーザだったらなんでもできるような形でOKです。

Next.jsの実装

components/FileUploader.tsx

ファイルアップロードのための仕組みの基本部分です。
ファイル名をrandomUUIDにした上で、親のsetStateに最終的なアップロード先のURLを取得して渡します。

import { Dispatch, SetStateAction, useState } from "react";
import { createClient } from "@/utils/supabase/client";

type Props = {
  isImage: boolean;
  setUrl: Dispatch<SetStateAction<string | null>>;
};
export default function FileUploader({ isImage, setUrl }: Props) {
  const supabase = createClient();

  const [file, setFile] = useState<File>();
  const handleChangeFile = (e: any) => {
    if (e.target.files.length !== 0) {
      setFile(e.target.files[0]);
    }
  };
  const onSubmit = async () => {
    const targetFile = file;
    if (targetFile) {
      const {
        data: { user },
      } = await supabase.auth.getUser();

      const { data, error } = await supabase.storage
        .from("main_storage")
        .upload(`${user!.id}/${crypto.randomUUID()}.png`, targetFile, {
          cacheControl: "3600",
          upsert: false,
        });

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

      console.log(data);

      await getUrlFromFilePath(data.path);
    }
  };

  const getUrlFromFilePath = async (path: string) => {
    const { data } = supabase.storage.from("main_storage").getPublicUrl(path);

    setUrl(data.publicUrl);
  };

  return (
    <div className="flex">
      <input
        type="file"
        accept={isImage ? "image/*" : ""}
        onChange={(e) => {
          handleChangeFile(e);
        }}
      />
      <button
        onClick={onSubmit}
        type="button"
        className="bg-blue-700 rounded-md px-1 py-1 text-foreground"
      >
        アップロード
      </button>
    </div>
  );
}

components/common/FileUploadInput.tsx

ファイルアップロードを含むinput欄の共通要素です。

import {Dispatch, SetStateAction} from "react";
import {InputType} from "@/utils/inputType";
import FileUploader from "@/components/FileUploader";

type Props = {
    name: string,
    value: any,
    isRequired: boolean,
    labelText: string,
    placeholderText: string,
    inputType: InputType,
    isImage: boolean,
    fileSetter: Dispatch<SetStateAction<string | null>>,
};
export default function FileUploadInput({name, value, isRequired, labelText, placeholderText, inputType, isImage, fileSetter}: Props) {
    return (<>
        <label className="text-md" htmlFor={name}>
            {labelText}
        </label>
        <FileUploader
            isImage={isImage}
            setUrl={fileSetter}
        ></FileUploader>
        <input
            className="rounded-md px-4 py-2 bg-inherit border mb-6"
            name={name}
            placeholder={placeholderText}
            value={value}
            required={isRequired}
            type={InputType[inputType]}
            disabled={true}
        />
    </>)
}

components/company/MyJobEdit.tsx

企業側で行える求人の編集ページにおいて、ファイルアップロードを取り入れています。
(対象は募集画像1~5です。)
画像の場合は、アップロード対象を画像のみに縛るよう設定を入れています。

"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 {InputType} from "@/utils/inputType";
import DefaultInput from "@/components/common/DefaultInput";
import DefaultTextarea from "@/components/common/DefaultTextarea";
import {DatabaseType} from "@/utils/DatabaseType";
import DefaultSelect from "@/components/common/DefaultSelect";
import FileUploadInput from "@/components/common/FileUploadInput";

/**
 * 自社の求人編集ページ
 */
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("/myjobedit?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">
                <DefaultInput name={"name"} value={name} setter={setName} isRequired={true}
                              labelText={"求人名*"} placeholderText={""}
                              inputType={InputType.text}/>

                <DefaultTextarea name={"description"} value={description} setter={setDescription} isRequired={false}
                                 labelText={"求人説明"} placeholderText={""} rowSize={10}/>

                <DefaultSelect name={"work_location"} value={work_location}
                               setter={setWork_location} isRequired={true} labelText={"勤務地*"}
                               selectData={work_locationData} databaseType={DatabaseType.mst_work_location}/>

                <DefaultTextarea name={"work_location_detail"} value={work_location_detail ? work_location_detail : ""}
                                 setter={setWork_location_detail} isRequired={false}
                                 labelText={"勤務地詳細"} placeholderText={""} rowSize={5}/>

                <DefaultInput name={"working_hours"} value={working_hours} setter={setWorking_hours} isRequired={true}
                              labelText={"勤務時間*"} placeholderText={""}
                              inputType={InputType.text}/>

                <DefaultSelect name={"day_off"} value={day_off}
                               setter={setDay_off} isRequired={true} labelText={"休日*"}
                               selectData={day_offData} databaseType={DatabaseType.mst_day_off}/>

                <DefaultTextarea name={"day_off_detail"} value={day_off_detail ? day_off_detail : ""}
                                 setter={setDay_off_detail} isRequired={false}
                                 labelText={"休日詳細"} placeholderText={""} rowSize={10}/>

                <DefaultSelect name={"employment_class"} value={employment_class}
                               setter={setEmployment_class} isRequired={true} labelText={"雇用区分*"}
                               selectData={[]} databaseType={DatabaseType.mst_employment_class}/>

                <DefaultInput name={"annual_income"} value={annual_income} setter={setAnnual_income} isRequired={true}
                              labelText={"想定年収*"} placeholderText={""}
                              inputType={InputType.number}/>

                <DefaultTextarea name={"annual_income_detail"} value={annual_income_detail ? annual_income_detail : ""}
                                 setter={setAnnual_income_detail} isRequired={false}
                                 labelText={"想定年収詳細"} placeholderText={""} rowSize={5}/>

                <DefaultTextarea name={"treatment"} value={treatment ? treatment : ""}
                                 setter={setTreatment} isRequired={false}
                                 labelText={"待遇"} placeholderText={""} rowSize={5}/>

                <DefaultTextarea name={"employee_benefits"} value={employee_benefits ? employee_benefits : ""}
                                 setter={setEmployee_benefits} isRequired={false}
                                 labelText={"福利厚生"} placeholderText={""} rowSize={5}/>

                <DefaultTextarea name={"qualification"} value={qualification}
                                 setter={setQualification} isRequired={true}
                                 labelText={"応募資格*"} placeholderText={""} rowSize={5}/>

                <DefaultTextarea name={"required_skills"} value={required_skills ? required_skills : ""}
                                 setter={setRequired_skills} isRequired={false}
                                 labelText={"必須スキル"} placeholderText={""} rowSize={5}/>

                <DefaultTextarea name={"skills"} value={skills ? skills : ""}
                                 setter={setSkills} isRequired={false}
                                 labelText={"歓迎スキル"} placeholderText={""} rowSize={5}/>

                <DefaultSelect name={"occupation_id"} value={occupation_id}
                               setter={setOccupation_id} isRequired={true} labelText={"職種*"}
                               selectData={occupation} databaseType={DatabaseType.mst_occupation}/>

                <DefaultSelect name={"industry_id"} value={industry_id}
                               setter={setIndustry_id} isRequired={true} labelText={"業界*"}
                               selectData={industry} databaseType={DatabaseType.mst_industry}/>

                <FileUploadInput name={"job_image_url_1"} value={job_image_url_1 ? job_image_url_1 : ""}
                                 isRequired={false}
                                 labelText={"募集画像1のURL"} placeholderText={""}
                                 inputType={InputType.text} isImage={true} fileSetter={setJob_image_url_1}/>

                <FileUploadInput name={"job_image_url_2"} value={job_image_url_2 ? job_image_url_2 : ""}
                                 isRequired={false}
                                 labelText={"募集画像2のURL"} placeholderText={""}
                                 inputType={InputType.text} isImage={true} fileSetter={setJob_image_url_2}/>


                <FileUploadInput name={"job_image_url_3"} value={job_image_url_3 ? job_image_url_3 : ""}
                                 isRequired={false}
                                 labelText={"募集画像3のURL"} placeholderText={""}
                                 inputType={InputType.text} isImage={true} fileSetter={setJob_image_url_3}/>

                <FileUploadInput name={"job_image_url_4"} value={job_image_url_4 ? job_image_url_4 : ""}
                                 isRequired={false}
                                 labelText={"募集画像4のURL"} placeholderText={""}
                                 inputType={InputType.text} isImage={true} fileSetter={setJob_image_url_4}/>

                <FileUploadInput name={"job_image_url_5"} value={job_image_url_5 ? job_image_url_5 : ""}
                                 isRequired={false}
                                 labelText={"募集画像5のURL"} placeholderText={""}
                                 inputType={InputType.text} isImage={true} fileSetter={setJob_image_url_5}/>

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

ファイルアップロード部分は、添付画像のような見た目になります。
labelとinputの間にアップロードのための仕組みを挟みつつ、
input要素をdisabledにすることで直に編集できないようにしています。

components/userinfo/CompanyBasic.tsx

企業の基本情報編集ページです。
やっていることは上の求人と変わらないため説明は割愛します。

"use client";
import {createClient} from "@/utils/supabase/client";
import {useEffect, useState} from "react";
import {redirect} from "next/navigation";
import {SubmitButton} from "@/components/SubmitButton";
import {Database} from "@/types/supabase";
import DefaultInput from "@/components/common/DefaultInput";
import {InputType} from "@/utils/inputType";
import DefaultSelect from "@/components/common/DefaultSelect";
import {DatabaseType} from "@/utils/DatabaseType";
import FileUploadInput from "@/components/common/FileUploadInput";

export default function CompanyBasic() {
    const [message, setMessage] = useState("");
    const supabase = createClient();
    const [industryData, setIndustryData] = useState<
        Database["public"]["Tables"]["mst_industry"]["Row"][]
    >([]);

    // 初回のデータかどうか?
    const [isFirst, setIsFirst] = useState<boolean>(false);
    // フォームデータを保持する
    const [companyname, setCompanyname] = useState("");
    const [zipcode, setZipcode] = useState("");
    const [address1, setAddress1] = useState("");
    const [address2, setAddress2] = useState("");
    const [phone, setPhone] = useState<string | null>(null);
    const [email, setEmail] = useState("");
    const [companyurl, setCompanyurl] = useState<string | null>(null);
    const [capital, setCapital] = useState<number | null>(null);
    const [employee, setEmployee] = useState<number | null>(null);
    const [annual_turnover, setAnnual_turnover] = useState<number | null>(null);
    const [established_at, setEstablishd_at] = useState<string | null>(null);
    const [industry_id_1, setIndustry_id_1] = useState<string | null>(null);
    const [industry_id_2, setIndustry_id_2] = useState<string | null>(null);
    const [industry_id_3, setIndustry_id_3] = useState<string | null>(null);
    const [company_image_url_1, setCompany_image_url_1] = useState<string | null>(
        null
    );
    const [company_image_url_2, setCompany_image_url_2] = useState<string | null>(
        null
    );
    const [company_image_url_3, setCompany_image_url_3] = useState<string | null>(
        null
    );
    const [company_image_url_4, setCompany_image_url_4] = useState<string | null>(
        null
    );
    const [company_image_url_5, setCompany_image_url_5] = useState<string | null>(
        null
    );

    useEffect(() => {
        getIndustyData();
        setData();
    }, []);

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

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

        const {data} = await supabase
            .from("mst_company")
            .select()
            .eq("user_uid", user.id);

        if (data != null && data?.length != 0) {
            const company_data: Database["public"]["Tables"]["mst_company"]["Row"] =
                data[0];
            setCompanyname(company_data.name);
            setZipcode(company_data.zipcode);
            setAddress1(company_data.address1);
            setAddress2(company_data.address2);
            if (company_data.phone != null) {
                setPhone(company_data.phone);
            }
            setEmail(company_data.email);
            if (company_data.url != null) {
                setCompanyurl(company_data.url);
            }
            if (company_data.capital != null) {
                setCapital(company_data.capital);
            }
            if (company_data.employee != null) {
                setEmployee(company_data.employee);
            }
            if (company_data.annual_turnover != null) {
                setAnnual_turnover(company_data.annual_turnover);
            }
            if (company_data.established_at != null) {
                setEstablishd_at(company_data.established_at);
            }
            if (company_data.industry_id_1 != null) {
                setIndustry_id_1(company_data.industry_id_1);
            }
            if (company_data.industry_id_2 != null) {
                setIndustry_id_2(company_data.industry_id_2);
            }
            if (company_data.industry_id_3 != null) {
                setIndustry_id_3(company_data.industry_id_3);
            }
            if (company_data.company_image_url_1 != null) {
                setCompany_image_url_1(company_data.company_image_url_1);
            }
            if (company_data.company_image_url_2 != null) {
                setCompany_image_url_2(company_data.company_image_url_2);
            }
            if (company_data.company_image_url_3 != null) {
                setCompany_image_url_3(company_data.company_image_url_3);
            }
            if (company_data.company_image_url_4 != null) {
                setCompany_image_url_4(company_data.company_image_url_4);
            }
            if (company_data.company_image_url_5 != null) {
                setCompany_image_url_5(company_data.company_image_url_5);
            }
        } else {
            setIsFirst(true);
            setEmail(user.email!);
        }
    };

    const getIndustyData = 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;
        setIndustryData(fixed_data);
    };

    const updateEmail = async () => {
        const {data, error} = await supabase.auth.updateUser({
            email: email,
        });

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

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

        if (user.email != email) {
            await updateEmail();
        }

        const timeStamp = new Date().toISOString();

        const {error} = await supabase.from("mst_company").upsert({
            user_uid: user.id,
            name: companyname,
            zipcode: zipcode,
            address1: address1,
            address2: address2,
            phone: phone,
            email: email,
            url: companyurl,
            capital: capital,
            employee: employee,
            annual_turnover: annual_turnover,
            established_at: established_at,
            industry_id_1: industry_id_1,
            industry_id_2: industry_id_2,
            industry_id_3: industry_id_3,
            company_image_url_1: company_image_url_1,
            company_image_url_2: company_image_url_2,
            company_image_url_3: company_image_url_3,
            company_image_url_4: company_image_url_4,
            company_image_url_5: company_image_url_5,
            updated_at: timeStamp,
        });

        if (error) {
            console.log(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>
                {isFirst ? (
                    <>
                        <DefaultInput name={"companyname"} value={companyname} setter={setCompanyname} isRequired={true}
                                      labelText={"会社名*"} placeholderText={"株式会社○○"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"zipcode"} value={zipcode} setter={setZipcode} isRequired={true}
                                      labelText={"郵便番号*"}
                                      placeholderText={"0000000 ※ハイフンなしでご入力ください"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"address1"} value={address1} setter={setAddress1} isRequired={true}
                                      labelText={"住所1*"}
                                      placeholderText={"東京都港区浜松町2丁目2番15号"} inputType={InputType.text}/>
                        <DefaultInput name={"address2"} value={address2} setter={setAddress2} isRequired={true}
                                      labelText={"住所2*"} placeholderText={"浜松町ダイヤビル2F"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"email"} value={email != null ? email : undefined} setter={setEmail}
                                      isRequired={true}
                                      labelText={"メールアドレス*"} placeholderText={"email@example.com"}
                                      inputType={InputType.email}/>
                    </>
                ) : (
                    <>
                        <DefaultInput name={"companyname"} value={companyname} setter={setCompanyname} isRequired={true}
                                      labelText={"会社名*"} placeholderText={"株式会社○○"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"zipcode"} value={zipcode} setter={setZipcode} isRequired={true}
                                      labelText={"郵便番号*"}
                                      placeholderText={"0000000 ※ハイフンなしでご入力ください"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"address1"} value={address1} setter={setAddress1} isRequired={true}
                                      labelText={"住所1*"}
                                      placeholderText={"東京都港区浜松町2丁目2番15号"} inputType={InputType.text}/>
                        <DefaultInput name={"address2"} value={address2} setter={setAddress2} isRequired={true}
                                      labelText={"住所2*"} placeholderText={"浜松町ダイヤビル2F"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"phone"} value={phone != null ? phone : undefined} setter={setPhone}
                                      isRequired={false}
                                      labelText={"電話番号"}
                                      placeholderText={"00000000000 ※ハイフンなしでご入力ください"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"email"} value={email != null ? email : undefined} setter={setEmail}
                                      isRequired={true}
                                      labelText={"メールアドレス*"} placeholderText={"email@example.com"}
                                      inputType={InputType.email}/>
                        <DefaultInput name={"companyurl"} value={companyurl != null ? companyurl : undefined}
                                      setter={setCompanyurl} isRequired={false}
                                      labelText={"ホームページURL"} placeholderText={"https://example.com"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"capital"} value={capital != null ? capital : undefined} setter={setCapital}
                                      isRequired={false}
                                      labelText={"資本金"} placeholderText={"100 ※単位は万円"}
                                      inputType={InputType.number}/>
                        <DefaultInput name={"employee"} value={employee != null ? employee : undefined}
                                      setter={setEmployee} isRequired={false}
                                      labelText={"従業員数"} placeholderText={"50"} inputType={InputType.number}/>
                        <DefaultInput name={"annual_turnover"}
                                      value={annual_turnover != null ? annual_turnover : undefined}
                                      setter={setAnnual_turnover} isRequired={false}
                                      labelText={"年商"} placeholderText={"5000 ※単位は万円"}
                                      inputType={InputType.number}/>
                        <DefaultInput name={"established_at"} value={established_at ? established_at : ""}
                                      setter={setEstablishd_at} isRequired={false}
                                      labelText={"設立"} placeholderText={""} inputType={InputType.date}/>

                        <DefaultSelect name={"industry_id_1"} value={industry_id_1 ? industry_id_1 : ""}
                                       setter={setIndustry_id_1} isRequired={false} labelText={"業界1"}
                                       selectData={industryData} databaseType={DatabaseType.mst_industry}/>

                        <DefaultSelect name={"industry_id_2"} value={industry_id_2 ? industry_id_2 : ""}
                                       setter={setIndustry_id_2} isRequired={false} labelText={"業界2"}
                                       selectData={industryData} databaseType={DatabaseType.mst_industry}/>

                        <DefaultSelect name={"industry_id_3"} value={industry_id_3 ? industry_id_3 : ""}
                                       setter={setIndustry_id_3} isRequired={false} labelText={"業界3"}
                                       selectData={industryData} databaseType={DatabaseType.mst_industry}/>

                        <FileUploadInput name={"company_image_url_1"}
                                         value={company_image_url_1 != null ? company_image_url_1 : undefined}
                                         isRequired={false}
                                         labelText={"会社の画像1のURL"} placeholderText={"会社の画像1のURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_1}/>

                        <FileUploadInput name={"company_image_url_2"}
                                         value={company_image_url_2 != null ? company_image_url_2 : undefined}
                                         isRequired={false}
                                         labelText={"会社の画像2のURL"} placeholderText={"会社の画像2のURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_2}/>

                        <FileUploadInput name={"company_image_url_3"}
                                         value={company_image_url_3 != null ? company_image_url_3 : undefined}
                                         isRequired={false}
                                         labelText={"会社の画像3のURL"} placeholderText={"会社の画像3のURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_3}/>
                        <FileUploadInput name={"company_image_url_4"}
                                         value={company_image_url_4 != null ? company_image_url_4 : undefined}
                                         isRequired={false}
                                         labelText={"会社の画像4のURL"} placeholderText={"会社の画像4のURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_4}/>
                        <FileUploadInput name={"company_image_url_5"}
                                         value={company_image_url_5 != null ? company_image_url_5 : undefined}
                                         isRequired={false}
                                         labelText={"会社の画像5のURL"} placeholderText={"会社の画像5のURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_5}/>
                    </>
                )}

                <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/userinfo/JobSeekerBasic.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 {InputType} from "@/utils/inputType";
import DefaultInput from "@/components/common/DefaultInput";
import DefaultSelect from "@/components/common/DefaultSelect";
import {DatabaseType} from "@/utils/DatabaseType";
import FileUploadInput from "@/components/common/FileUploadInput";

export default function JobSeekerBasic() {
    const [message, setMessage] = useState("");
    const supabase = createClient();

    // 初回のデータ入力かどうか?
    const [isFirst, setIsFirst] = useState<boolean>(false);
    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<string | null>(null);
    const [gender, setGender] = useState("");
    const [birthday, setBirthday] = useState("");
    const [zipcode, setZipcode] = useState<string | null>(null);
    const [address1, setAddress1] = useState<string | null>(null);
    const [address2, setAddress2] = useState<string | null>(null);
    const [phone, setPhone] = useState<string | null>(null);
    const [email, setEmail] = useState("");
    const [nationality_id, setNationality_id] = useState<string | null>(null);
    const [current_annual_income, setCurrent_annual_income] = useState<
        number | null
    >(null);
    const [desired_annual_income, setDesired_annual_income] = useState<
        number | null
    >(null);
    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<string | null>(null);
    const [
        residence_qualification_back_image_url,
        setResidence_qualification_back_image_url,
    ] = useState<string | null>(null);
    const [profile_image_url, setProfile_image_url] = useState<string | null>(
        null
    );
    const [resume_file_url, setResume_file_url] = useState<string | null>(null);
    const [resume_file_name, setResume_file_name] = useState<string | null>(null);

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

    const updateEmail = async () => {
        const {data, error} = await supabase.auth.updateUser({
            email: email,
        });

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

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

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

        if (user.email != email) {
            await updateEmail();
        }

        const {data} = await supabase
            .from("mst_job_seeker")
            .select()
            .eq("user_uid", user.id);

        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);
            }
            setEmail(job_seeker_data.email);
            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);
            }
        } else {
            setIsFirst(true);
            setEmail(user.email!);
        }
    };

    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,
            email: email,
            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>
                {isFirst ? (
                    <>
                        <DefaultInput name={"lastname"} value={lastname} setter={setLastname} isRequired={true}
                                      labelText={"姓*"} placeholderText={"山田"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"firstname"} value={firstname} setter={setFirstname} isRequired={true}
                                      labelText={"名*"} placeholderText={"太郎"}
                                      inputType={InputType.text}/>
                        <DefaultSelect name={"gender"} value={gender}
                                       setter={setGender} isRequired={true} labelText={"性別*"}
                                       selectData={[]} databaseType={DatabaseType.mst_gender}/>
                        <DefaultInput name={"birthday"} value={birthday} setter={setBirthday} isRequired={true}
                                      labelText={"誕生日*"} placeholderText={""}
                                      inputType={InputType.date}/>
                        <DefaultInput name={"email"} value={email} setter={setEmail} isRequired={true}
                                      labelText={"メールアドレス*"} placeholderText={"email@example.com"}
                                      inputType={InputType.email}/>
                    </>
                ) : (
                    <>
                        <DefaultInput name={"lastname"} value={lastname} setter={setLastname} isRequired={true}
                                      labelText={"姓*"} placeholderText={"山田"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"firstname"} value={firstname} setter={setFirstname} isRequired={true}
                                      labelText={"名*"} placeholderText={"太郎"}
                                      inputType={InputType.text}/>
                        <DefaultInput name={"middlename"} value={middlename != null ? middlename : undefined}
                                      setter={setMiddlename} isRequired={false}
                                      labelText={"ミドルネーム"} placeholderText={"ミドルネーム"}
                                      inputType={InputType.text}/>
                        <DefaultSelect name={"gender"} value={gender}
                                       setter={setGender} isRequired={true} labelText={"性別*"}
                                       selectData={[]} databaseType={DatabaseType.mst_gender}/>
                        <DefaultInput name={"birthday"} value={birthday} setter={setBirthday} isRequired={true}
                                      labelText={"誕生日*"} placeholderText={""}
                                      inputType={InputType.date}/>
                        <DefaultInput name={"zipcode"} value={zipcode != null ? zipcode : undefined}
                                      setter={setZipcode} isRequired={false}
                                      labelText={"郵便番号"} placeholderText={"0000000 ※ハイフンなしでご入力ください"}
                                      inputType={InputType.text}/>

                        <DefaultInput name={"address1"} value={address1 != null ? address1 : undefined}
                                      setter={setAddress1} isRequired={false}
                                      labelText={"住所1"} placeholderText={"東京都港区浜松町2丁目2番15号"}
                                      inputType={InputType.text}/>

                        <DefaultInput name={"address2"} value={address2 != null ? address2 : undefined}
                                      setter={setAddress2} isRequired={false}
                                      labelText={"住所2"} placeholderText={"浜松町ダイヤビル2F"}
                                      inputType={InputType.text}/>

                        <DefaultInput name={"phone"} value={phone != null ? phone : undefined}
                                      setter={setPhone} isRequired={false}
                                      labelText={"電話番号"}
                                      placeholderText={"00000000000 ※ハイフンなしでご入力ください"}
                                      inputType={InputType.text}/>

                        <DefaultInput name={"email"} value={email} setter={setEmail} isRequired={true}
                                      labelText={"メールアドレス*"} placeholderText={"email@example.com"}
                                      inputType={InputType.email}/>

                        <DefaultSelect name={"nationalityId"} value={nationality_id ? nationality_id : ""}
                                       setter={setNationality_id} isRequired={false} labelText={"国籍"}
                                       selectData={nationalityData} databaseType={DatabaseType.mst_nationality}/>

                        <DefaultInput name={"current_annual_income"}
                                      value={current_annual_income != null ? current_annual_income : undefined}
                                      setter={setCurrent_annual_income} isRequired={false}
                                      labelText={"現在の年収"} placeholderText={"現在の年収"}
                                      inputType={InputType.number}/>

                        <DefaultInput name={"desired_annual_income"}
                                      value={desired_annual_income != null ? desired_annual_income : undefined}
                                      setter={setDesired_annual_income} isRequired={false}
                                      labelText={"希望年収"} placeholderText={"希望年収"}
                                      inputType={InputType.number}/>

                        <DefaultSelect name={"spouse"} value={spouse ? spouse : ""}
                                       setter={setSpouse} isRequired={false} labelText={"配偶者"}
                                       selectData={[]} databaseType={DatabaseType.mst_spouse}/>

                        <DefaultSelect name={"desired_occupation_id_1"}
                                       value={desired_occupation_id_1 ? desired_occupation_id_1 : ""}
                                       setter={setDesired_occupation_id_1} isRequired={false} labelText={"希望職種1"}
                                       selectData={occupationData} databaseType={DatabaseType.mst_occupation}/>

                        <DefaultSelect name={"desired_occupation_id_2"}
                                       value={desired_occupation_id_2 ? desired_occupation_id_2 : ""}
                                       setter={setDesired_occupation_id_2} isRequired={false} labelText={"希望職種2"}
                                       selectData={occupationData} databaseType={DatabaseType.mst_occupation}/>

                        <DefaultSelect name={"desired_occupation_id_3"}
                                       value={desired_occupation_id_3 ? desired_occupation_id_3 : ""}
                                       setter={setDesired_occupation_id_3} isRequired={false} labelText={"希望職種3"}
                                       selectData={occupationData} databaseType={DatabaseType.mst_occupation}/>

                        <DefaultInput name={"desired_change_job_date"}
                                      value={desired_change_job_date ? desired_change_job_date : ""}
                                      setter={setDesired_change_job_date} isRequired={false}
                                      labelText={"転職希望日"} placeholderText={""}
                                      inputType={InputType.date}/>

                        <DefaultSelect name={"residence_qualification_id"}
                                       value={residence_qualification_id ? residence_qualification_id : ""}
                                       setter={setResidence_qualification_id} isRequired={false} labelText={"在留資格"}
                                       selectData={residenceQualificationData}
                                       databaseType={DatabaseType.mst_residence_qualification}/>

                        <DefaultInput name={"residence_qualification_expired"}
                                      value={residence_qualification_expired != null ? residence_qualification_expired : undefined}
                                      setter={setResidence_qualification_expired} isRequired={false}
                                      labelText={"在留資格期限"} placeholderText={""}
                                      inputType={InputType.date}/>

                        <FileUploadInput name={"residence_qualification_front_image_url"}
                                         value={residence_qualification_front_image_url != null ? residence_qualification_front_image_url : undefined}
                                         isRequired={false}
                                         labelText={"在留資格カード表のファイルURL"}
                                         placeholderText={"在留資格カード表のファイルURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setResidence_qualification_front_image_url}/>

                        <FileUploadInput name={"residence_qualification_back_image_url"}
                                         value={residence_qualification_back_image_url != null ? residence_qualification_back_image_url : undefined}
                                         isRequired={false}
                                         labelText={"在留資格カード裏のファイルURL"}
                                         placeholderText={"在留資格カード裏のファイルURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setResidence_qualification_back_image_url}/>

                        <FileUploadInput name={"profile_image_url"}
                                         value={profile_image_url != null ? profile_image_url : undefined}
                                         isRequired={false}
                                         labelText={"プロフィール画像のファイルURL"}
                                         placeholderText={"プロフィール画像のファイルURL"}
                                         inputType={InputType.text} isImage={true} fileSetter={setProfile_image_url}/>

                        <FileUploadInput name={"resume_file_url"}
                                         value={resume_file_url != null ? resume_file_url : undefined}
                                         isRequired={false}
                                         labelText={"履歴書のファイルURL"}
                                         placeholderText={"履歴書のファイルURL"}
                                         inputType={InputType.text} isImage={false} fileSetter={setResume_file_url}/>

                        <DefaultInput name={"resume_file_name"}
                                      value={resume_file_name != null ? resume_file_name : undefined}
                                      setter={setResume_file_name} isRequired={false}
                                      labelText={"履歴書のファイル名"}
                                      placeholderText={"履歴書のファイル名"}
                                      inputType={InputType.text}/>
                    </>
                )}

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

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

実装の確認

求職者側のアプリにアクセスし、ログインしましょう。
ユーザページに遷移して下のほうまでスクロールすると在留資格カード~履歴書までアップロードするためのUIが表示されるかと思います。

実際にアップロードしてみましょう。



在留資格カード表をクリックし、サンプルの画像を選択してアップロードすると、入力欄へURLが表示されます。
URLをコピペして新しいタブで開けば、先ほどアップロードした画像が表示されることがわかります。

この状態で一番下の保存ボタンを押すと、こちらのURLが基本情報に適用されます。
もちろんSupabase側で見てもデータベース、Storage内ともにデータの追加が確認できます。

同じように企業の基本情報、求人の情報でも画像をアップロードして確認してみてください。
問題なく行えていればOKです!

その他参考資料など

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

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

お問合せ&各種リンク

presented by

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