+
지원자 이름
@@ -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;