-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 합/불 상태관리 페이지 추가 #205
Changes from 21 commits
6f7ace8
96a5372
410a1c9
289604c
023a3d4
bd64439
38890af
0c4a80c
9738a23
7cae5fa
192cb56
7ec8cc1
10af5ee
c6a4cd3
ebb1b9a
26a4913
325aa41
8d852bb
c522277
836ca3d
e266f6a
46c03d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import Txt from "@/components/common/Txt.component"; | ||
import ApplicantsList from "@/components/passState/ApplicantsList"; | ||
import { CURRENT_GENERATION } from "@/src/constants"; | ||
import Image from "next/image"; | ||
import Link from "next/link"; | ||
|
||
interface PassStatePageProps { | ||
searchParams: { | ||
sortedBy?: "field"; | ||
}; | ||
} | ||
|
||
const PassStatePage = ({ searchParams: { sortedBy } }: PassStatePageProps) => { | ||
return ( | ||
<div className="flex-1 ml-32 min-w-[46rem] mb-12"> | ||
<div className="mt-20" /> | ||
<Txt typography="h3"> | ||
{CURRENT_GENERATION}기 지원자 합격 상태 관리 페이지 | ||
</Txt> | ||
<div className="mt-8" /> | ||
{sortedBy === "field" && ( | ||
<Link href={`/pass-state/${CURRENT_GENERATION}`}> | ||
<Txt typography="h5" className="text-secondary-200"> | ||
현재 분야별로 정렬되어 있습니다. (WEB - APP - AI - GAME) | ||
</Txt> | ||
<div className="mt-8" /> | ||
</Link> | ||
)} | ||
<div className="grid grid-cols-[8fr_8fr_4fr_3fr] gap-4"> | ||
<Txt typography="h6" className="text-left text-secondary-100"> | ||
지원자 이름 | ||
</Txt> | ||
<Link | ||
href={`/pass-state/${CURRENT_GENERATION}?${new URLSearchParams( | ||
"sortedBy=field" | ||
)}`} | ||
className="text-start flex justify-between pr-4 items-center" | ||
> | ||
<Txt typography="h6" className="text-left text-secondary-100"> | ||
분야 | ||
</Txt> | ||
<Image | ||
src={"/icons/chevron-down.svg"} | ||
alt="sort by field" | ||
width={16} | ||
height={16} | ||
/> | ||
</Link> | ||
<Txt typography="h6" className="text-left text-secondary-100"> | ||
합격 상태 | ||
</Txt> | ||
</div> | ||
<div className="mt-2" /> | ||
<hr /> | ||
<div className="mt-4" /> | ||
<ApplicantsList sortedBy={sortedBy} /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default PassStatePage; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import { NavbarOperation } from "./NavbarOperation"; | |
import Link from "next/link"; | ||
import { usePathname } from "next/navigation"; | ||
import { NavbarGenerationToggle } from "./NavbarGenerationToggle"; | ||
import { NavbarManagePassState } from "./NavbarManagePassState"; | ||
|
||
const CommonNavbar = () => { | ||
const [_, currentType, generation] = usePathname().split("/"); | ||
|
@@ -31,7 +32,8 @@ const CommonNavbar = () => { | |
/> | ||
))} | ||
<NavbarGenerationToggle generation={generation} isShort={isShort} /> | ||
<NavbarOperation currentType={currentType} /> | ||
<NavbarManagePassState currentType={currentType} isShort={isShort} /> | ||
<NavbarOperation currentType={currentType} isShort={isShort} /> | ||
Comment on lines
34
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 죄송합니다.. 제가 isShort라는 괴물을 키웠군요... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @loopy-lim ㅋㅋㅋㅋㅋㅋㅋ |
||
</div> | ||
<NavbarUserInfo /> | ||
</nav> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"use client"; | ||
|
||
import { useQuery } from "@tanstack/react-query"; | ||
import CommonNavbarCell from "./NavbarCell"; | ||
import { getMyInfo } from "@/src/apis/interview"; | ||
import { usePathname } from "next/navigation"; | ||
|
||
type NavbarManagePassStateProps = { | ||
isShort: boolean; | ||
currentType: string; | ||
}; | ||
export const NavbarManagePassState = ({ | ||
currentType, | ||
isShort, | ||
}: NavbarManagePassStateProps) => { | ||
const currentPath = usePathname(); | ||
const generation = currentPath.split("/")[2]; | ||
const { data: userData, isLoading } = useQuery(["user"], getMyInfo); | ||
if (isLoading || !userData) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
if (userData.role !== "ROLE_OPERATION") { | ||
return <div>관리자만 접근이 가능합니다.</div>; | ||
} | ||
|
||
return ( | ||
<CommonNavbarCell | ||
currentType={currentType} | ||
isShort={isShort} | ||
href={`/pass-state/${generation}`} | ||
short_title="합/불 관리" | ||
title="합/불 상태 관리" | ||
target="_self" | ||
type="pass-state" | ||
/> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
"use client"; | ||
|
||
import { | ||
getAllApplicantsWithPassState, | ||
postApplicantPassState, | ||
} 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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
import type { | ||
PostApplicantPassStateParams, | ||
Answer, | ||
} from "@/src/apis/passState"; | ||
|
||
function sortApplicantsByField1(applicants: Answer[]) { | ||
const passStateOrder = { | ||
"final-passed": 0, | ||
"first-passed": 1, | ||
"non-passed": 2, | ||
"non-processed": 3, | ||
}; | ||
|
||
const field1Order = { | ||
WEB: 0, | ||
APP: 1, | ||
AI: 2, | ||
GAME: 3, | ||
}; | ||
|
||
return applicants.sort((a, b) => { | ||
if ( | ||
passStateOrder[a.state.passState] !== passStateOrder[b.state.passState] | ||
) { | ||
return ( | ||
passStateOrder[a.state.passState] - passStateOrder[b.state.passState] | ||
); | ||
} | ||
return field1Order[a.field1] - field1Order[b.field1]; | ||
}); | ||
} | ||
Comment on lines
+17
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 나중에 시간이 되신다면 함수형으로 다시 작성해보는 것도 이쁘지 않을까 라는 생각이 듭니다. 특히 filter에 대한 좋은 방법론들이 많기 때문에 해보는 것을 추천드립니다.(먼가 힘들다 싶으면 backend친구들에게 요청해보세요. 그들은 filter전문가들이에요) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 해당 부분도 고려해보겠습니다!! |
||
|
||
interface ApplicantsListProps { | ||
sortedBy?: "position" | "field"; | ||
} | ||
const ApplicantsList = ({ sortedBy }: ApplicantsListProps) => { | ||
const selectedGeneration = usePathname().split("/")[2]; | ||
const queryClient = useQueryClient(); | ||
|
||
const { | ||
data: allApplicants, | ||
isLoading, | ||
isError, | ||
} = useQuery(["allApplicantsWithPassState", selectedGeneration], () => | ||
getAllApplicantsWithPassState(selectedGeneration) | ||
); | ||
|
||
const { mutate: updateApplicantPassState } = useMutation({ | ||
mutationFn: (params: PostApplicantPassStateParams) => | ||
postApplicantPassState(params), | ||
onSuccess: () => { | ||
queryClient.invalidateQueries({ | ||
queryKey: ["allApplicantsWithPassState", selectedGeneration], | ||
}); | ||
}, | ||
}); | ||
|
||
{ | ||
if (+selectedGeneration !== CURRENT_GENERATION) { | ||
return <div>현재 지원중인 기수만 확인 가능합니다.</div>; | ||
} | ||
|
||
if (isLoading) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
if (isError) { | ||
return <div>Error</div>; | ||
} | ||
|
||
if (!allApplicants) { | ||
return <div>아직은 지원자가 없습니다 🥲</div>; | ||
} | ||
} | ||
|
||
const onChangeApplicantsPassState = ( | ||
applicantName: string, | ||
params: PostApplicantPassStateParams | ||
) => { | ||
const ok = confirm( | ||
`${applicantName}님을 ${params.afterState} 처리하시겠습니까?` | ||
); | ||
if (!ok) return; | ||
updateApplicantPassState(params); | ||
}; | ||
|
||
const applicants = | ||
sortedBy === "field" | ||
? sortApplicantsByField1(allApplicants) | ||
: allApplicants; | ||
|
||
return ( | ||
<ul className="flex flex-col gap-4"> | ||
{applicants.map( | ||
({ state: { passState }, field, field1, field2, id, name }) => ( | ||
<li | ||
key={id} | ||
className="grid grid-cols-[8fr_8fr_4fr_3fr] gap-4 items-center" | ||
> | ||
Comment on lines
+107
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 먼가 마스터한 자의 css가 보이는 듯 하다....ㄷㄷㄷ |
||
<Txt typography="h6" className="text-left truncate"> | ||
{`[${field}] ${name}`} | ||
</Txt> | ||
<Txt | ||
className="text-left truncate" | ||
color="gray" | ||
>{`${field1}/${field2}`}</Txt> | ||
<Txt | ||
className="text-left truncate" | ||
color={ | ||
passState === "non-passed" | ||
? "red" | ||
: passState === "final-passed" | ||
? "blue" | ||
: "black" | ||
} | ||
> | ||
{getApplicantPassState(passState)} | ||
</Txt> | ||
<div className="flex justify-between"> | ||
<button | ||
className="border px-4 py-2 rounded-lg truncate hover:bg-primary-100" | ||
onClick={() => | ||
onChangeApplicantsPassState(name, { | ||
applicantsId: id, | ||
afterState: "pass", | ||
}) | ||
} | ||
> | ||
합격 | ||
</button> | ||
<button | ||
className="border px-4 rounded-lg truncate hover:bg-primary-100" | ||
onClick={() => | ||
onChangeApplicantsPassState(name, { | ||
applicantsId: id, | ||
afterState: "non-pass", | ||
}) | ||
} | ||
> | ||
불합격 | ||
</button> | ||
</div> | ||
</li> | ||
) | ||
)} | ||
</ul> | ||
); | ||
}; | ||
|
||
export default ApplicantsList; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
next를 쓰고 있다는게 새삼스럽게 느껴지네요 ㅋㅋㅋ