今回は、今まで作成してきた各種編集画面にファイルアップロードの機能を搭載し、アップロードしたファイルの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://note.com/embed/notes/n522396165049
お問合せ&各種リンク
- お問合せ:GoogleForm
- ホームページ:https://libproc.com
- 運営会社:TodoONada株式会社
- Twitter:https://twitter.com/Todoonada_corp
- Instagram:https://www.instagram.com/todoonada_corp/
- Youtube:https://www.youtube.com/@todoonada_corp/
- Tiktok:https://www.tiktok.com/@todoonada_corp
presented by