diff --git a/frontend/app/(WithNavbar)/pass-state/[generation]/page.tsx b/frontend/app/(WithNavbar)/pass-state/[generation]/page.tsx index 6decf046..ca48e54b 100644 --- a/frontend/app/(WithNavbar)/pass-state/[generation]/page.tsx +++ b/frontend/app/(WithNavbar)/pass-state/[generation]/page.tsx @@ -1,5 +1,6 @@ import Txt from "@/components/common/Txt.component"; import ApplicantsList from "@/components/passState/ApplicantsList"; +import BulkEmailButtons from "@/components/passState/BulkEmailButtons"; import { CURRENT_GENERATION } from "@/src/constants"; import Image from "next/image"; import Link from "next/link"; @@ -18,6 +19,8 @@ const PassStatePage = ({ searchParams: { sortedBy } }: PassStatePageProps) => { {CURRENT_GENERATION}기 지원자 합격 상태 관리 페이지
+ +
{sortedBy === "field" && ( @@ -26,7 +29,7 @@ const PassStatePage = ({ searchParams: { sortedBy } }: PassStatePageProps) => {
)} -
+
지원자 이름 @@ -49,6 +52,12 @@ const PassStatePage = ({ searchParams: { sortedBy } }: PassStatePageProps) => { 합격 상태 + + 합격/불합격 + + + 이메일 발송 +

diff --git a/frontend/components/passState/ApplicantsList.tsx b/frontend/components/passState/ApplicantsList.tsx index 4593edbd..ab440c2a 100644 --- a/frontend/components/passState/ApplicantsList.tsx +++ b/frontend/components/passState/ApplicantsList.tsx @@ -1,17 +1,21 @@ "use client"; -import { getAllApplicantsWithPassState } from "@/src/apis/passState"; +import { + getAllApplicantsWithPassState, + sendEmailToApplicant, +} from "@/src/apis/passState"; import { CURRENT_GENERATION } from "@/src/constants"; import { usePathname } from "next/navigation"; import Txt from "../common/Txt.component"; import { getApplicantPassState } from "@/src/functions/formatter"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import type { PatchApplicantPassStateParams, Answer, } from "@/src/apis/passState"; import { useOptimisticApplicantPassUpdate } from "@/src/hooks/applicant/useOptimisticApplicantPassUpdate"; + function sortApplicantsByField1(applicants: Answer[]) { const passStateOrder = { "final-passed": 0, @@ -27,7 +31,7 @@ function sortApplicantsByField1(applicants: Answer[]) { GAME: 3, }; - return applicants.sort((a, b) => { + return [...applicants].sort((a, b) => { if ( passStateOrder[a.state.passState] !== passStateOrder[b.state.passState] ) { @@ -58,30 +62,37 @@ const ApplicantsList = ({ sortedBy }: ApplicantsListProps) => { // isLoading: isUpdatingApplicantPassState, } = useOptimisticApplicantPassUpdate(selectedGeneration); - { - if (+selectedGeneration !== CURRENT_GENERATION) { - return
현재 지원중인 기수만 확인 가능합니다.
; - } + const { mutate: sendEmail } = useMutation(sendEmailToApplicant); - if (isLoading) { - return
Loading...
; - } + const onSendEmail = (name: string, applicantId: string) => { + const ok = confirm(`${name}님에게 결과 이메일을 발송하시겠습니까?`); + if (!ok) return; + sendEmail(applicantId); + }; - if (isError) { - return
Error
; - } + if (+selectedGeneration !== CURRENT_GENERATION) { + return
현재 지원중인 기수만 확인 가능합니다.
; + } - if (!allApplicants) { - return
아직은 지원자가 없습니다 🥲
; - } + if (isLoading) { + return
Loading...
; + } + + if (isError) { + return
Error
; + } + + if (!allApplicants) { + return
아직은 지원자가 없습니다 🥲
; } const onChangeApplicantsPassState = ( applicantName: string, params: PatchApplicantPassStateParams ) => { + const stateLabel = params.afterState === "pass" ? "합격" : "불합격"; const ok = confirm( - `${applicantName}님을 ${params.afterState} 처리하시겠습니까?` + `${applicantName}님을 ${stateLabel} 처리하시겠습니까?` ); if (!ok) return; updateApplicantPassState(params); @@ -93,62 +104,74 @@ const ApplicantsList = ({ sortedBy }: ApplicantsListProps) => { : allApplicants; return ( -
    - {applicants.map( - ({ state: { passState }, field, field1, field2, id, name }) => ( -
  • - - {`[${field}] ${name}`} - - {`${field1}/${field2}`} - +
      + {applicants.map( + ({ state: { passState }, field, field1, field2, id, name }) => ( +
    • - {getApplicantPassState(passState)} - -
      - + {getApplicantPassState(passState)} + +
      + + +
      -
      -
    • - ) - )} -
    +
  • + ) + )} +
+ ); }; diff --git a/frontend/components/passState/BulkEmailButtons.tsx b/frontend/components/passState/BulkEmailButtons.tsx new file mode 100644 index 00000000..57ea514b --- /dev/null +++ b/frontend/components/passState/BulkEmailButtons.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { sendEmailToAll, type EmailState } from "@/src/apis/passState"; +import { useMutation } from "@tanstack/react-query"; +import { usePathname } from "next/navigation"; + +const EMAIL_STATE_LABEL_MAP: Record = { + "first-passed": "1차 합격자", + "first-failed": "1차 불합격자", + "final-passed": "최종 합격자", + "final-failed": "최종 불합격자", +}; + +const EMAIL_STATES = Object.keys(EMAIL_STATE_LABEL_MAP) as EmailState[]; + +const BulkEmailButtons = () => { + const selectedGeneration = usePathname().split("/")[2]; + const { mutate: sendEmailAll } = useMutation(sendEmailToAll); + + const onSendEmailAll = (state: EmailState) => { + const ok = confirm( + `${EMAIL_STATE_LABEL_MAP[state]} 전체에게 결과 이메일을 발송하시겠습니까?` + ); + if (!ok) return; + sendEmailAll( + { year: Number(selectedGeneration), state }, + { + onSuccess: () => alert(`${EMAIL_STATE_LABEL_MAP[state]} 이메일 발송이 완료되었습니다.`), + onError: () => alert(`이메일 발송 중 오류가 발생했습니다. 다시 시도해주세요.`), + } + ); + }; + + return ( +
+
일괄 발송
+
+ {EMAIL_STATES.map((state) => ( + + ))} +
+
+ ); +}; + +export default BulkEmailButtons; diff --git a/frontend/src/apis/passState/index.tsx b/frontend/src/apis/passState/index.tsx index 9375d438..b0cbf7f0 100644 --- a/frontend/src/apis/passState/index.tsx +++ b/frontend/src/apis/passState/index.tsx @@ -34,3 +34,23 @@ export const patchApplicantPassState = async ({ `/applicants/${applicantId}/state?afterState=${afterState}` ); }; + +export const sendEmailToApplicant = async (applicantId: string) => { + await https.post(`/emails/${applicantId}`); +}; + +export type EmailState = + | "first-passed" + | "first-failed" + | "final-passed" + | "final-failed"; + +export interface SendEmailToAllParams { + year: number; + state: EmailState; +} +export const sendEmailToAll = async ({ year, state }: SendEmailToAllParams) => { + await https.post(`/emails/all`, undefined, { + params: { year, state }, + }); +}; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 0cd5f26d..f10b4dd9 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -174,6 +174,7 @@ export const needValidatePath = [ "/applicant", "/interview", "/kanban", + "/pass-state", ]; export const MAX_BOOLEAN_TEXT_LENGTH = 800;