diff --git a/apps/web/src/app/(main)/_components/home/Admin/AdminHomeDashboardScreen.tsx b/apps/web/src/app/(main)/_components/home/Admin/AdminHomeDashboardScreen.tsx deleted file mode 100644 index c560cdcb..00000000 --- a/apps/web/src/app/(main)/_components/home/Admin/AdminHomeDashboardScreen.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { - AnnounceCard, - AnnounceCardProps, -} from '@web/app/(main)/_components/admin/AnnouncedCard/AnnouncedCard'; -import { - DocTimeline, - DocTimelineProps, -} from '@web/app/(main)/_components/admin/DocTimeline/DocTimeline'; -import { - OverallProgress, - Task, -} from '@web/app/(main)/_components/admin/OverallProgress/OverallProgress'; -import { - PendingUsers, - PendingUsersProps, -} from '@web/app/(main)/_components/admin/PendingUsers/PendingUsers'; -import React from 'react'; -import { Flex } from '@repo/ui/Flex'; -import { AdminHomeHeader } from '@web/app/(main)/_components/admin/AdminHomeHeader/AdminHomeHeader'; - -const announceData: AnnounceCardProps = { - title: '현재 진행 중인 공고 - [한국대학생IT경영학회] 큐시즘 32기 학회원 모집', - totalCount: 100, - parts: [ - { name: '기획', color: '#FFD8A6', count: 23 }, - { name: '디자인', color: '#D0F2FF', count: 20 }, - { name: '프론트엔드', color: '#B5F5EC', count: 30 }, - { name: '백엔드', color: '#FFE3EC', count: 27 }, - ], - onViewDetail: () => { - console.log('지원 현황으로 이동'); - }, -}; - -const docTimelineData: DocTimelineProps = { - title: '서류 평가', - currentMonth: new Date(2025, 4), - deadlineDays: 3, - events: [ - { date: '2025-05-04', label: '서류 합격 발표', daysBefore: 3 }, - { date: '2025-05-14', label: '면접 평가', daysBefore: 14 }, - { date: '2025-05-23', label: '최종 발표', daysBefore: 23 }, - ], - onPrevMonth: () => console.log('이전 달'), - onNextMonth: () => console.log('다음 달'), -}; - -const tasks: Task[] = [ - { - id: 't1', - type: '서류', - title: '서류 평가', - daysBefore: 3, - total: 50, - completed: 15, - partLabel: '전체', - }, - { - id: 't2', - type: '면접', - title: '면접 평가', - daysBefore: 22, - total: 40, - completed: 10, - partLabel: '전체', - }, -]; - -const pendingUsersData: PendingUsersProps = { - deadline: new Date(2025, 4, 1, 23, 59), - remainingHours: 21, - users: [ - { - id: '1', - name: '김현호', - avatarUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTpe8Bx-lLSfBQO6Pi22c5nnBpUaJMeV0hi_s9Vf-CFB-pRlj0ezWnKCtf3b9AvkvxRmLo&usqp=CAU', - }, - { - id: '2', - name: '설정원', - avatarUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQIoLwYArbR32rJwWFNLSWkCxX_JoUSlFHByyZ181guOpXx94XHZQ6eRf9J3eq6ydHM-co&usqp=CAU', - }, - { - id: '3', - name: '이채원', - avatarUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRXUJbxoZj1I44tPmZhsdoyqImc2pwSSSULcw3hb4U-CeyF--hrVYrmOkv14DGsh5pBYCk&usqp=CAU', - }, - { - id: '4', - name: '서유빈', - avatarUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSQ2mnsIOyPS475TL65mRbwjka-xlGjqhiuQd_cwQ5h7jT_ynTLbV-kT8PeI_NYQUiW8ZI&usqp=CAU', - }, - ], - onRemind: () => { - console.log('리마인드 알림 발송'); - }, -}; - -export const AdminHomeDashboardScreen = () => ( - - - - - - - - - - - -); diff --git a/apps/web/src/app/(main)/dashboard/_components/AdminHomeDashboardScreen.tsx b/apps/web/src/app/(main)/dashboard/_components/AdminHomeDashboardScreen.tsx new file mode 100644 index 00000000..34bededf --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/_components/AdminHomeDashboardScreen.tsx @@ -0,0 +1,177 @@ +'use client'; + +import React from 'react'; +import { useRouter } from 'next/navigation'; +import { Flex } from '@repo/ui/Flex'; +import { AdminHomeHeader } from '@web/app/(main)/dashboard/_components/admin/AdminHomeHeader/AdminHomeHeader'; +import { + AnnounceCard, + AnnounceCardProps, +} from '@web/app/(main)/dashboard/_components/admin/AnnouncedCard/AnnouncedCard'; +import { + DocTimeline, + DocTimelineProps, +} from '@web/app/(main)/dashboard/_components/admin/DocTimeline/DocTimeline'; +import { + OverallProgress, + Task, +} from '@web/app/(main)/dashboard/_components/admin/OverallProgress/OverallProgress'; +import { + PendingUsers, + PendingUsersProps, +} from '@web/app/(main)/dashboard/_components/admin/PendingUsers/PendingUsers'; +import { useCurrentRecruitmentsSummaryQuery } from '@web/store/query/useCurrentRecruitmentsSummaryQuery'; +import { + useRecruitmentProgressQuery, + RecruitmentProgressDto, +} from '@web/store/query/useRecruitmentProgressQuery'; +import { useRecruitmentPendingEvaluatorsQuery } from '@web/store/query/useRecruitmentPendingEvaluatorsQuery'; +import type { Tokens } from '@web/api/types'; + +interface AdminHomeDashboardScreenProps { + recruitmentId: number; + tokens?: Tokens; +} + +export const AdminHomeDashboardScreen = ({ + recruitmentId, + tokens, +}: AdminHomeDashboardScreenProps) => { + const router = useRouter(); + + const summariesQuery = useCurrentRecruitmentsSummaryQuery(tokens); + const docProgressQuery = useRecruitmentProgressQuery( + recruitmentId, + 'DOCUMENT', + tokens + ); + const interviewProgressQuery = useRecruitmentProgressQuery( + recruitmentId, + 'INTERVIEW', + tokens + ); + const pendingQuery = useRecruitmentPendingEvaluatorsQuery( + recruitmentId, + tokens + ); + + if ( + !summariesQuery.data || + !docProgressQuery.data || + !interviewProgressQuery.data || + !pendingQuery.data + ) { + return null; + } + + const summaries = summariesQuery.data; + const docProgress = docProgressQuery.data; + const interviewProgress = interviewProgressQuery.data; + const pending = pendingQuery.data; + + console.log('summaries:', summaries); + console.log('docProgress:', docProgress); + console.log('interviewProgress:', interviewProgress); + console.log('pending:', pending); + console.log('recruitmentId:', recruitmentId); + console.log('tokens:', tokens); + + if (!summaries || summaries.length === 0) return null; + + const summary = + summaries.find((s) => s.recruitmentId === recruitmentId) ?? summaries[0]!; + const announceData: AnnounceCardProps = { + title: `현재 진행 중인 공고 - [${summary.organizationName}] ${summary.title}`, + totalCount: summary.totalApplicants, + parts: summary.positionCounts.map((p) => ({ + name: p.positionName, + count: p.count, + })), + onViewDetail: () => + // router.push( + // `/admin/recruitments/${recruitmentId}/progress?stage=DOCUMENT` + // ), + console.log('상세 페이지로 이동'), + }; + + const allEvents = summary.dDays + .filter((e) => e.date !== null) + .map((e) => ({ + date: e.date!.replace(/\//g, '-'), + label: e.label, + daysBefore: e.daysRemaining, + })); + + console.log('allEvents (filtered):', allEvents); + console.log('original dDays:', summary.dDays); + const docDeadlineEvent = allEvents.find((e) => e.label.includes('서류 마감')); + const firstDate = allEvents[0]?.date; + const currentMonth = firstDate ? new Date(firstDate) : new Date(); + + const docTimelineData: DocTimelineProps = { + title: '서류 평가', + currentMonth, + deadlineDays: docDeadlineEvent?.daysBefore ?? 0, + events: allEvents, + onPrevMonth: () => console.log('이전 달'), + onNextMonth: () => console.log('다음 달'), + }; + + const makeTasks = ( + arr: RecruitmentProgressDto[], + type: '서류' | '면접' + ): Task[] => + arr.map((d) => ({ + id: `${type}-${d.positionName}`, + type, + title: `${type} 평가`, + daysBefore: d.daysToDeadline, + total: d.totalToEvaluate, + completed: d.evaluatedCount, + partLabel: d.positionName, + })); + + const tasks: Task[] = [ + ...makeTasks(docProgress, '서류'), + ...makeTasks(interviewProgress, '면접'), + ]; + + const deadlineDate = pending.deadline + ? new Date(pending.deadline.replace(/\//g, '-')) + : new Date(); + + const pendingUsersData: PendingUsersProps = { + deadline: deadlineDate, + remainingDays: pending.daysToDeadline ?? 0, + remainingHours: pending.hoursToDeadline ?? 0, + remainingMinutes: pending.minutesToDeadline ?? 0, + users: + pending.users?.map((u) => ({ + id: u.userId.toString(), + name: u.name, + avatarUrl: u.profileImageUrl ?? '', + })) ?? [], + onRemind: () => console.log('리마인드 알림 발송'), + }; + return ( + + + + + + + + + + + + ); +}; diff --git a/apps/web/src/app/(main)/_components/home/Admin/AdminHomeEmptyScreen.tsx b/apps/web/src/app/(main)/dashboard/_components/AdminHomeEmptyScreen.tsx similarity index 58% rename from apps/web/src/app/(main)/_components/home/Admin/AdminHomeEmptyScreen.tsx rename to apps/web/src/app/(main)/dashboard/_components/AdminHomeEmptyScreen.tsx index 01766257..98c0a2cc 100644 --- a/apps/web/src/app/(main)/_components/home/Admin/AdminHomeEmptyScreen.tsx +++ b/apps/web/src/app/(main)/dashboard/_components/AdminHomeEmptyScreen.tsx @@ -1,7 +1,7 @@ import { Flex } from '@repo/ui/Flex'; -import { AdminHomeHeader } from '@web/app/(main)/_components/admin/AdminHomeHeader/AdminHomeHeader'; -import { NoAnnouncements } from '@web/app/(main)/_components/admin/EmptyState/NoAnnouncements'; -import { NoTasks } from '@web/app/(main)/_components/admin/EmptyState/NoTasks'; +import { AdminHomeHeader } from '@web/app/(main)/dashboard/_components/admin/AdminHomeHeader/AdminHomeHeader'; +import { NoAnnouncements } from '@web/app/(main)/dashboard/_components/admin/EmptyState/NoAnnouncements'; +import { NoTasks } from '@web/app/(main)/dashboard/_components/admin/EmptyState/NoTasks'; import React from 'react'; export const AdminHomeEmptyScreen = () => ( diff --git a/apps/web/src/app/(main)/_components/home/User/UserHomeDashboardScreen.tsx b/apps/web/src/app/(main)/dashboard/_components/UserHomeDashboardScreen.tsx similarity index 54% rename from apps/web/src/app/(main)/_components/home/User/UserHomeDashboardScreen.tsx rename to apps/web/src/app/(main)/dashboard/_components/UserHomeDashboardScreen.tsx index 87cf8ff4..8f03396b 100644 --- a/apps/web/src/app/(main)/_components/home/User/UserHomeDashboardScreen.tsx +++ b/apps/web/src/app/(main)/dashboard/_components/UserHomeDashboardScreen.tsx @@ -2,37 +2,78 @@ import React from 'react'; import { Flex } from '@repo/ui/Flex'; -import { TimelineEvent } from '@web/app/(main)/_components/admin/DocTimeline/DocTimeline'; +import { TimelineEvent } from '@web/app/(main)/dashboard/_components/admin/DocTimeline/DocTimeline'; import { ReviewItem, UserDocReviewList, -} from '@web/app/(main)/_components/user/UserDocReviewList/UserDocReviewList'; +} from '@web/app/(main)/dashboard/_components/user/UserDocReviewList/UserDocReviewList'; import { InterviewSlot, ReviewerRole, UserInterviewReview, -} from '@web/app/(main)/_components/user/UserInterviewReview/UserInterviewReview'; -import { UserAnnouncementProgress } from '@web/app/(main)/_components/user/UserAnnouncementProgress/UserAnnouncementProgress'; -import { UserHomeHeader } from '@web/app/(main)/_components/user/UserHomeHeader/UserHomeHeader'; +} from '@web/app/(main)/dashboard/_components/user/UserInterviewReview/UserInterviewReview'; +import { UserAnnouncementProgress } from '@web/app/(main)/dashboard/_components/user/UserAnnouncementProgress/UserAnnouncementProgress'; +import { UserHomeHeader } from '@web/app/(main)/dashboard/_components/user/UserHomeHeader/UserHomeHeader'; +import { useOrganizationsMeQuery } from '@web/store/query/useOrganizationsMeQuery'; +import { + RecruitmentSummaryDto, + useRecruitmentsCurrentSummaryQuery, +} from '@web/store/query/useRecruitmentsCurrentSummaryQuery'; +import { + MyEvaluationItemDto, + useMyDocumentEvaluationsQuery, +} from '@web/store/query/useMyDocumentEvaluationsQuery'; +import { Tokens } from '@web/api/types'; + +interface UserHomeDashboardScreenProps { + tokens: Tokens; +} +export default function UserHomeDashboardScreen({ + tokens, +}: UserHomeDashboardScreenProps) { + const orgsQuery = useOrganizationsMeQuery(tokens); + const summaryQuery = useRecruitmentsCurrentSummaryQuery( + orgsQuery.data?.[0]?.id ?? -1, + tokens + ); + + const docEvalQuery = useMyDocumentEvaluationsQuery( + summaryQuery.data?.[0]?.recruitmentId ?? -1, + tokens + ); -export const UserHomeDashboardScreen = () => { - const announcementTitle = '[한국대학생IT경영학회] 큐시즘 32기 학회원 모집'; - const timelineEvents: TimelineEvent[] = [ - { date: '2025-05-04', label: '서류 평가 마감', daysBefore: 3 }, - { date: '2025-04-07', label: '면접 평가 시작', daysBefore: 30 }, - { date: '2025-02-21', label: '최종 발표 시작', daysBefore: 70 }, - ]; + const orgs = orgsQuery.data; + if (!orgs || orgs.length === 0) return null; + const organization = orgs[0]!; - const itemsBefore: ReviewItem[] = [ - { id: '1', part: '기획', name: '장지원' }, - { id: '2', part: '디자인', name: '김하나' }, - { id: '3', part: '백엔드', name: '이영희' }, - ]; - const itemsAfter: ReviewItem[] = [ - { id: '4', part: '기획', name: '박철수' }, - { id: '5', part: '디자인', name: '최민준' }, - { id: '6', part: '백엔드', name: '최수진' }, - ]; + const summaries = summaryQuery.data; + if (!summaries || summaries.length === 0) return null; + const summary: RecruitmentSummaryDto = summaries[0]!; + + const docEvals = docEvalQuery.data; + if (!docEvals) return null; + + const timelineEvents: TimelineEvent[] = summary.dDays.map((e) => ({ + date: e.date.replace(/\//g, '-'), + label: e.label, + daysBefore: e.daysRemaining, + })); + const announcementTitle = `[${organization.name}] ${summary.title}`; + + const itemsBefore: ReviewItem[] = docEvals.pending.map( + (u: MyEvaluationItemDto) => ({ + id: u.id.toString(), + part: u.positionName, + name: u.name, + }) + ); + const itemsAfter: ReviewItem[] = docEvals.done.map( + (u: MyEvaluationItemDto) => ({ + id: u.id.toString(), + part: u.positionName, + name: u.name, + }) + ); const initialDate = new Date(2025, 4, 12); const slotsByRole: Record = { @@ -137,4 +178,4 @@ export const UserHomeDashboardScreen = () => { ); -}; +} diff --git a/apps/web/src/app/(main)/_components/home/User/UserHomeEmptyScreen.tsx b/apps/web/src/app/(main)/dashboard/_components/UserHomeEmptyScreen.tsx similarity index 59% rename from apps/web/src/app/(main)/_components/home/User/UserHomeEmptyScreen.tsx rename to apps/web/src/app/(main)/dashboard/_components/UserHomeEmptyScreen.tsx index 5a2f17dc..67a41219 100644 --- a/apps/web/src/app/(main)/_components/home/User/UserHomeEmptyScreen.tsx +++ b/apps/web/src/app/(main)/dashboard/_components/UserHomeEmptyScreen.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { Flex } from '@repo/ui/Flex'; -import { UserHomeHeader } from '@web/app/(main)/_components/user/UserHomeHeader/UserHomeHeader'; -import { NoAnnouncements } from '@web/app/(main)/_components/user/EmptyState/NoAnnouncements'; -import { NoDocs } from '@web/app/(main)/_components/user/EmptyState/NoDocs'; -import { NoInterview } from '@web/app/(main)/_components/user/EmptyState/NoInterviews'; +import { UserHomeHeader } from '@web/app/(main)/dashboard/_components/user/UserHomeHeader/UserHomeHeader'; +import { NoAnnouncements } from '@web/app/(main)/dashboard/_components/user/EmptyState/NoAnnouncements'; +import { NoDocs } from '@web/app/(main)/dashboard/_components/user/EmptyState/NoDocs'; +import { NoInterview } from '@web/app/(main)/dashboard/_components/user/EmptyState/NoInterviews'; export const UserHomeEmptyScreen: React.FC = () => { return ( diff --git a/apps/web/src/app/(main)/_components/admin/AdminHomeHeader/AdminHomeHeader.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/AdminHomeHeader/AdminHomeHeader.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/admin/AdminHomeHeader/AdminHomeHeader.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/AdminHomeHeader/AdminHomeHeader.tsx diff --git a/apps/web/src/app/(main)/_components/admin/AnnouncedCard/AnnouncedCard.css.ts b/apps/web/src/app/(main)/dashboard/_components/admin/AnnouncedCard/AnnouncedCard.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/admin/AnnouncedCard/AnnouncedCard.css.ts rename to apps/web/src/app/(main)/dashboard/_components/admin/AnnouncedCard/AnnouncedCard.css.ts diff --git a/apps/web/src/app/(main)/_components/admin/AnnouncedCard/AnnouncedCard.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/AnnouncedCard/AnnouncedCard.tsx similarity index 99% rename from apps/web/src/app/(main)/_components/admin/AnnouncedCard/AnnouncedCard.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/AnnouncedCard/AnnouncedCard.tsx index 325f1ae9..848b1115 100644 --- a/apps/web/src/app/(main)/_components/admin/AnnouncedCard/AnnouncedCard.tsx +++ b/apps/web/src/app/(main)/dashboard/_components/admin/AnnouncedCard/AnnouncedCard.tsx @@ -9,7 +9,6 @@ import { Tag } from '@repo/ui/Tag'; import { allTagColors, TagColor } from '@repo/utils'; export interface PartCount { name: string; - color: string; count: number; } diff --git a/apps/web/src/app/(main)/_components/admin/DocTimeline/DocTimeline.css.ts b/apps/web/src/app/(main)/dashboard/_components/admin/DocTimeline/DocTimeline.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/admin/DocTimeline/DocTimeline.css.ts rename to apps/web/src/app/(main)/dashboard/_components/admin/DocTimeline/DocTimeline.css.ts diff --git a/apps/web/src/app/(main)/_components/admin/DocTimeline/DocTimeline.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/DocTimeline/DocTimeline.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/admin/DocTimeline/DocTimeline.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/DocTimeline/DocTimeline.tsx diff --git a/apps/web/src/app/(main)/_components/admin/EmptyState/EmptyState.css.ts b/apps/web/src/app/(main)/dashboard/_components/admin/EmptyState/EmptyState.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/admin/EmptyState/EmptyState.css.ts rename to apps/web/src/app/(main)/dashboard/_components/admin/EmptyState/EmptyState.css.ts diff --git a/apps/web/src/app/(main)/_components/admin/EmptyState/NoAnnouncements.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/EmptyState/NoAnnouncements.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/admin/EmptyState/NoAnnouncements.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/EmptyState/NoAnnouncements.tsx diff --git a/apps/web/src/app/(main)/_components/admin/EmptyState/NoTasks.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/EmptyState/NoTasks.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/admin/EmptyState/NoTasks.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/EmptyState/NoTasks.tsx diff --git a/apps/web/src/app/(main)/_components/admin/OverallProgress/OverallProgress.css.ts b/apps/web/src/app/(main)/dashboard/_components/admin/OverallProgress/OverallProgress.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/admin/OverallProgress/OverallProgress.css.ts rename to apps/web/src/app/(main)/dashboard/_components/admin/OverallProgress/OverallProgress.css.ts diff --git a/apps/web/src/app/(main)/_components/admin/OverallProgress/OverallProgress.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/OverallProgress/OverallProgress.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/admin/OverallProgress/OverallProgress.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/OverallProgress/OverallProgress.tsx diff --git a/apps/web/src/app/(main)/_components/admin/PendingUsers/PendingUsers.css.ts b/apps/web/src/app/(main)/dashboard/_components/admin/PendingUsers/PendingUsers.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/admin/PendingUsers/PendingUsers.css.ts rename to apps/web/src/app/(main)/dashboard/_components/admin/PendingUsers/PendingUsers.css.ts diff --git a/apps/web/src/app/(main)/_components/admin/PendingUsers/PendingUsers.tsx b/apps/web/src/app/(main)/dashboard/_components/admin/PendingUsers/PendingUsers.tsx similarity index 77% rename from apps/web/src/app/(main)/_components/admin/PendingUsers/PendingUsers.tsx rename to apps/web/src/app/(main)/dashboard/_components/admin/PendingUsers/PendingUsers.tsx index f46009fd..7aa87ed2 100644 --- a/apps/web/src/app/(main)/_components/admin/PendingUsers/PendingUsers.tsx +++ b/apps/web/src/app/(main)/dashboard/_components/admin/PendingUsers/PendingUsers.tsx @@ -16,14 +16,44 @@ export interface User { export interface PendingUsersProps { deadline: Date; + remainingDays?: number; remainingHours: number; + remainingMinutes?: number; users: User[]; onRemind: () => void; } +const formatRemainingTime = ( + days?: number, + hours?: number, + minutes?: number +): string => { + const parts: string[] = []; + + if (days && days > 0) { + parts.push(`${days}일`); + } + + if (hours && hours > 0) { + parts.push(`${hours}시간`); + } + + if (minutes && minutes > 0) { + parts.push(`${minutes}분`); + } + + if (parts.length === 0) { + return '마감됨'; + } + + return `${parts.join(' ')} 남음`; +}; + export const PendingUsers = ({ deadline, + remainingDays, remainingHours, + remainingMinutes, users, onRemind, }: PendingUsersProps) => ( @@ -53,12 +83,12 @@ export const PendingUsers = ({ 평가 마감 : {deadline.getFullYear()}. {(deadline.getMonth() + 1).toString().padStart(2, '0')}. - {deadline.getDate().toString().padStart(2, '0')} + {deadline.getDate().toString().padStart(2, '0')}{' '} {deadline.getHours().toString().padStart(2, '0')}:{' '} {deadline.getMinutes().toString().padStart(2, '0')} - ({remainingHours}시간 남음) + ({formatRemainingTime(remainingDays, remainingHours, remainingMinutes)}) diff --git a/apps/web/src/app/(main)/_components/user/EmptyState/EmptyState.css.ts b/apps/web/src/app/(main)/dashboard/_components/user/EmptyState/EmptyState.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/user/EmptyState/EmptyState.css.ts rename to apps/web/src/app/(main)/dashboard/_components/user/EmptyState/EmptyState.css.ts diff --git a/apps/web/src/app/(main)/_components/user/EmptyState/NoAnnouncements.tsx b/apps/web/src/app/(main)/dashboard/_components/user/EmptyState/NoAnnouncements.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/EmptyState/NoAnnouncements.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/EmptyState/NoAnnouncements.tsx diff --git a/apps/web/src/app/(main)/_components/user/EmptyState/NoDocs.tsx b/apps/web/src/app/(main)/dashboard/_components/user/EmptyState/NoDocs.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/EmptyState/NoDocs.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/EmptyState/NoDocs.tsx diff --git a/apps/web/src/app/(main)/_components/user/EmptyState/NoInterviews.tsx b/apps/web/src/app/(main)/dashboard/_components/user/EmptyState/NoInterviews.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/EmptyState/NoInterviews.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/EmptyState/NoInterviews.tsx diff --git a/apps/web/src/app/(main)/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.css.ts b/apps/web/src/app/(main)/dashboard/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.css.ts rename to apps/web/src/app/(main)/dashboard/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.css.ts diff --git a/apps/web/src/app/(main)/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.tsx b/apps/web/src/app/(main)/dashboard/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/UserAnnouncementProgress/UserAnnouncementProgress.tsx diff --git a/apps/web/src/app/(main)/_components/user/UserDocReviewList/UserDocReviewList.css.ts b/apps/web/src/app/(main)/dashboard/_components/user/UserDocReviewList/UserDocReviewList.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserDocReviewList/UserDocReviewList.css.ts rename to apps/web/src/app/(main)/dashboard/_components/user/UserDocReviewList/UserDocReviewList.css.ts diff --git a/apps/web/src/app/(main)/_components/user/UserDocReviewList/UserDocReviewList.tsx b/apps/web/src/app/(main)/dashboard/_components/user/UserDocReviewList/UserDocReviewList.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserDocReviewList/UserDocReviewList.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/UserDocReviewList/UserDocReviewList.tsx diff --git a/apps/web/src/app/(main)/_components/user/UserHomeHeader/UserHomeHeader.tsx b/apps/web/src/app/(main)/dashboard/_components/user/UserHomeHeader/UserHomeHeader.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserHomeHeader/UserHomeHeader.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/UserHomeHeader/UserHomeHeader.tsx diff --git a/apps/web/src/app/(main)/_components/user/UserInterviewReview/UserInterviewReview.css.ts b/apps/web/src/app/(main)/dashboard/_components/user/UserInterviewReview/UserInterviewReview.css.ts similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserInterviewReview/UserInterviewReview.css.ts rename to apps/web/src/app/(main)/dashboard/_components/user/UserInterviewReview/UserInterviewReview.css.ts diff --git a/apps/web/src/app/(main)/_components/user/UserInterviewReview/UserInterviewReview.tsx b/apps/web/src/app/(main)/dashboard/_components/user/UserInterviewReview/UserInterviewReview.tsx similarity index 100% rename from apps/web/src/app/(main)/_components/user/UserInterviewReview/UserInterviewReview.tsx rename to apps/web/src/app/(main)/dashboard/_components/user/UserInterviewReview/UserInterviewReview.tsx diff --git a/apps/web/src/app/(main)/dashboard/admin/page.tsx b/apps/web/src/app/(main)/dashboard/admin/page.tsx new file mode 100644 index 00000000..c60166c7 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/admin/page.tsx @@ -0,0 +1,26 @@ +import { getServerSideTokens } from '@web/api/serverSideTokens'; +import { ServerFetchBoundary } from '@web/store/query/ServerFetchBoundary'; +import { getQueryClient } from '@web/store/query/getQueryClient'; +import { getCurrentRecruitmentsSummaryQueryOptions } from '@web/store/query/useCurrentRecruitmentsSummaryQuery'; +import { AdminHomeEmptyScreen } from '../_components/AdminHomeEmptyScreen'; +import { AdminHomeDashboardScreen } from '../_components/AdminHomeDashboardScreen'; + +export default async function AdminDashboardPage() { + const tokens = await getServerSideTokens(); + + const queryClient = getQueryClient(); + const summaryOptions = getCurrentRecruitmentsSummaryQueryOptions(tokens); + + const summaries = await queryClient.fetchQuery(summaryOptions); + + if (!summaries.length) { + return ; + } + + const recruitmentId = summaries[0]?.recruitmentId as number; + return ( + + + + ); +} diff --git a/apps/web/src/app/(main)/dashboard/user/page.tsx b/apps/web/src/app/(main)/dashboard/user/page.tsx new file mode 100644 index 00000000..e250eea6 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/user/page.tsx @@ -0,0 +1,47 @@ +import { cookies } from 'next/headers'; +import { getServerSideTokens } from '@web/api/serverSideTokens'; +import { ServerFetchBoundary } from '@web/store/query/ServerFetchBoundary'; +import { getQueryClient } from '@web/store/query/getQueryClient'; +import { getOrganizationsMeQueryOptions } from '@web/store/query/useOrganizationsMeQuery'; +import { getRecruitmentsCurrentSummaryQueryOptions } from '@web/store/query/useRecruitmentsCurrentSummaryQuery'; +import { getMyDocumentEvaluationsQueryOptions } from '@web/store/query/useMyDocumentEvaluationsQuery'; +import { UserHomeEmptyScreen } from '../_components/UserHomeEmptyScreen'; +import UserHomeDashboardScreen from '@web/app/(main)/dashboard/_components/UserHomeDashboardScreen'; + +export default async function UserDashboardPage() { + const roleRaw = (await cookies()).get('role')?.value; + + const tokens = await getServerSideTokens(); + const queryClient = getQueryClient(); + + const orgOptions = getOrganizationsMeQueryOptions(tokens); + const orgs = (await queryClient.fetchQuery(orgOptions)) ?? []; + if (!orgs.length) { + return ; + } + const organizationId = orgs[0]?.id as number; + + const summaryOptions = getRecruitmentsCurrentSummaryQueryOptions( + organizationId, + tokens + ); + const summaries = (await queryClient.fetchQuery(summaryOptions)) ?? []; + if (!summaries.length) { + return ; + } + const recruitmentId = summaries[0]?.recruitmentId as number; + + const docEvalOptions = getMyDocumentEvaluationsQueryOptions( + recruitmentId, + tokens + ); + await queryClient.fetchQuery(docEvalOptions); + + return ( + + + + ); +} diff --git a/apps/web/src/app/(main)/page.tsx b/apps/web/src/app/(main)/page.tsx index f57344b4..5e3ef3a9 100644 --- a/apps/web/src/app/(main)/page.tsx +++ b/apps/web/src/app/(main)/page.tsx @@ -1,29 +1,12 @@ -'use client'; -import { AdminHomeDashboardScreen } from '@web/app/(main)/_components/home/Admin/AdminHomeDashboardScreen'; -import { AdminHomeEmptyScreen } from '@web/app/(main)/_components/home/Admin/AdminHomeEmptyScreen'; -import { UserHomeDashboardScreen } from '@web/app/(main)/_components/home/User/UserHomeDashboardScreen'; -import { UserHomeEmptyScreen } from '@web/app/(main)/_components/home/User/UserHomeEmptyScreen'; -import { getCookie } from 'cookies-next'; -import { useMemo } from 'react'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; -export default function HomePage() { - const role = useMemo<'admin' | 'user'>(() => { - const raw = getCookie('role'); - return raw === 'ADMIN' ? 'admin' : 'user'; - }, []); +export default async function RootPage() { + const role = (await cookies()).get('role')?.value; - // 임시 값 - const adminHasData = true; - const userHasData = true; - if (role === 'admin') { - // 관리자 홈 - return adminHasData ? ( - - ) : ( - - ); + if (role === 'ADMIN') { + redirect('/dashboard/admin'); + } else { + redirect('/dashboard/user'); } - - // 일반 사용자 홈 - return userHasData ? : ; } diff --git a/apps/web/src/store/constants/queryKeys.ts b/apps/web/src/store/constants/queryKeys.ts index bf5b7100..567dcca7 100644 --- a/apps/web/src/store/constants/queryKeys.ts +++ b/apps/web/src/store/constants/queryKeys.ts @@ -42,10 +42,15 @@ export const queryKeys = { size, ] as const, }, + me: () => ['organizations', 'me'] as const, }, recruitments: { list: (keyword?: string) => ['recruitments', keyword ?? ''] as const, slug: (slug: string) => ['recruitments', 'slug', slug] as const, + currentSummary: (organizationId: number) => + ['recruitments', organizationId, 'current', 'summary'] as const, + myDocumentEvaluations: (recruitmentId: number) => + ['recruitments', recruitmentId, 'my', 'evaluations', 'documents'] as const, }, recruitment: { list: () => ['recruitment', 'list'] as const, @@ -131,4 +136,12 @@ export const queryKeys = { user: { myPage: () => ['user', 'myPage'] as const, }, + admin: { + currentRecruitmentsSummary: () => + ['admin', 'recruitments', 'current', 'summary'] as const, + recruitmentProgress: ( recruitmentId: number, stage: 'DOCUMENT' | 'INTERVIEW') => + ['admin', 'recruitments', recruitmentId, 'progress', stage] as const, + recruitmentPendingEvaluators: (recruitmentId: number) => + ['admin', 'recruitments', recruitmentId, 'pendingEvaluators'] as const, + } as const, } as const; diff --git a/apps/web/src/store/query/ServerFetchBoundary.tsx b/apps/web/src/store/query/ServerFetchBoundary.tsx index fc5f319f..93dff5a3 100644 --- a/apps/web/src/store/query/ServerFetchBoundary.tsx +++ b/apps/web/src/store/query/ServerFetchBoundary.tsx @@ -13,24 +13,26 @@ export type FetchOptions< 'queryKey' | 'queryFn' | 'staleTime' | 'gcTime' >; -type Props< - TQueryFnData = unknown, - TError = Error, - TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey, -> = { - fetchOptions: - | FetchOptions[] - | FetchOptions; +// type Props< +// TQueryFnData = unknown, +// TError = Error, +// TData = TQueryFnData, +// TQueryKey extends QueryKey = QueryKey, +// > = { +// fetchOptions: +// | FetchOptions[] +// | FetchOptions; +// children: ReactNode | ReactNode[]; +// }; + +type AnyFetchOptions = FetchOptions; + +type Props = { + fetchOptions: AnyFetchOptions | AnyFetchOptions[]; children: ReactNode | ReactNode[]; }; -export async function ServerFetchBoundary< - TQueryFnData = unknown, - TError = Error, - TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey, ->({ fetchOptions, children }: Props) { +export async function ServerFetchBoundary({ fetchOptions, children }: Props) { const queryClient = getQueryClient(); const options = Array.isArray(fetchOptions) ? fetchOptions : [fetchOptions]; diff --git a/apps/web/src/store/query/useCurrentRecruitmentsSummaryQuery.ts b/apps/web/src/store/query/useCurrentRecruitmentsSummaryQuery.ts new file mode 100644 index 00000000..c8331fbc --- /dev/null +++ b/apps/web/src/store/query/useCurrentRecruitmentsSummaryQuery.ts @@ -0,0 +1,45 @@ +import type { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { GET } from '@web/api/fetch'; +import { queryKeys } from '@web/store/constants/queryKeys'; +import type { Tokens } from '@web/api/types'; + +export interface RecruitmentSummaryDto { + recruitmentId: number; + title: string; + dDays: { + label: string; + date: string; + daysRemaining: number; + isPassed: boolean; + }[]; + organizationName: string; + totalApplicants: number; + positionCounts: { + positionName: string; + count: number; + }[]; +} + +const STALE_TIME = 1000 * 60; + +export function getCurrentRecruitmentsSummaryQueryOptions( + tokens?: Tokens +): UseSuspenseQueryOptions { + return { + queryKey: queryKeys.admin.currentRecruitmentsSummary(), + queryFn: () => + GET( + 'api/v1/admin/recruitments/current/summary', + undefined, + tokens + ).then(res => res.result), + staleTime: STALE_TIME, + }; +} + +export function useCurrentRecruitmentsSummaryQuery(tokens?: Tokens) { + return useSuspenseQuery( + getCurrentRecruitmentsSummaryQueryOptions(tokens) + ); +} \ No newline at end of file diff --git a/apps/web/src/store/query/useMyDocumentEvaluationsQuery.ts b/apps/web/src/store/query/useMyDocumentEvaluationsQuery.ts new file mode 100644 index 00000000..35fa14ef --- /dev/null +++ b/apps/web/src/store/query/useMyDocumentEvaluationsQuery.ts @@ -0,0 +1,45 @@ +import type { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { GET } from '@web/api/fetch'; +import { queryKeys } from '@web/store/constants/queryKeys'; +import type { Tokens } from '@web/api/types'; + +export interface MyEvaluationItemDto { + id: number; + name: string; + email: string; + positionName: string; + status: string; +} + +export interface MyDocumentEvaluationsDto { + pending: MyEvaluationItemDto[]; + done: MyEvaluationItemDto[]; +} + +const STALE_TIME = 1000 * 60; + +export function getMyDocumentEvaluationsQueryOptions( + recruitmentId: number, + tokens: Tokens +): UseSuspenseQueryOptions { + return { + queryKey: queryKeys.recruitments.myDocumentEvaluations(recruitmentId), + queryFn: () => + GET( + `api/v1/recruitments/${recruitmentId}/my/evaluations/documents`, + undefined, + tokens + ).then(res => res.result), + staleTime: STALE_TIME, + }; +} + +export function useMyDocumentEvaluationsQuery( + recruitmentId: number, + tokens: Tokens +) { + return useSuspenseQuery( + getMyDocumentEvaluationsQueryOptions(recruitmentId, tokens) + ); +} \ No newline at end of file diff --git a/apps/web/src/store/query/useOrganizationsMeQuery.ts b/apps/web/src/store/query/useOrganizationsMeQuery.ts new file mode 100644 index 00000000..4d728f2e --- /dev/null +++ b/apps/web/src/store/query/useOrganizationsMeQuery.ts @@ -0,0 +1,33 @@ +import type { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { GET } from '@web/api/fetch'; +import { queryKeys } from '@web/store/constants/queryKeys'; +import type { Tokens } from '@web/api/types'; + +export interface OrganizationDto { + id: number; + name: string; +} + +const STALE_TIME = 1000 * 60; + +export function getOrganizationsMeQueryOptions( + tokens: Tokens +): UseSuspenseQueryOptions { + return { + queryKey: queryKeys.organization.me(), + queryFn: () => + GET( + 'api/v1/organizations/me', + undefined, + tokens + ).then(res => res.result), + staleTime: STALE_TIME, + }; +} + +export function useOrganizationsMeQuery(tokens: Tokens) { + return useSuspenseQuery( + getOrganizationsMeQueryOptions(tokens) + ); +} \ No newline at end of file diff --git a/apps/web/src/store/query/useRecruitmentPendingEvaluatorsQuery.ts b/apps/web/src/store/query/useRecruitmentPendingEvaluatorsQuery.ts new file mode 100644 index 00000000..5c3ab1f1 --- /dev/null +++ b/apps/web/src/store/query/useRecruitmentPendingEvaluatorsQuery.ts @@ -0,0 +1,45 @@ +import type { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { GET } from '@web/api/fetch'; +import { queryKeys } from '@web/store/constants/queryKeys'; +import type { Tokens } from '@web/api/types'; + +export interface RecruitmentPendingEvaluatorsDto { + stage: 'DOCUMENT' | 'INTERVIEW'; + deadline: string; + daysToDeadline: number; + hoursToDeadline: number; + minutesToDeadline: number; + users: { + userId: number; + name: string; + profileImageUrl: string | null; + }[]; +} + +const STALE_TIME = 1000 * 60; + +export function getRecruitmentPendingEvaluatorsQueryOptions( + recruitmentId: number, + tokens?: Tokens +): UseSuspenseQueryOptions { + return { + queryKey: queryKeys.admin.recruitmentPendingEvaluators(recruitmentId), + queryFn: () => + GET( + `api/v1/admin/recruitments/${recruitmentId}/pending-evaluators`, + undefined, + tokens + ).then(res => res.result), + staleTime: STALE_TIME, + }; +} + +export function useRecruitmentPendingEvaluatorsQuery( + recruitmentId: number, + tokens?: Tokens +) { + return useSuspenseQuery( + getRecruitmentPendingEvaluatorsQueryOptions(recruitmentId, tokens) + ); +} \ No newline at end of file diff --git a/apps/web/src/store/query/useRecruitmentProgressQuery.ts b/apps/web/src/store/query/useRecruitmentProgressQuery.ts new file mode 100644 index 00000000..ac324a88 --- /dev/null +++ b/apps/web/src/store/query/useRecruitmentProgressQuery.ts @@ -0,0 +1,43 @@ +import type { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { GET } from '@web/api/fetch'; +import { queryKeys } from '@web/store/constants/queryKeys'; +import type { Tokens } from '@web/api/types'; + +export interface RecruitmentProgressDto { + positionName: string; + daysToDeadline: number; + totalToEvaluate: number; + evaluatedCount: number; + notEvaluatedCount: number; + progressPercent: number; +} + +const STALE_TIME = 1000 * 60; + +export function getRecruitmentProgressQueryOptions( + recruitmentId: number, + stage: 'DOCUMENT' | 'INTERVIEW', + tokens?: Tokens +): UseSuspenseQueryOptions { + return { + queryKey: queryKeys.admin.recruitmentProgress(recruitmentId, stage), + queryFn: () => + GET( + `api/v1/admin/recruitments/${recruitmentId}/progress`, + { stage }, + tokens + ).then(res => res.result), + staleTime: STALE_TIME, + }; +} + +export function useRecruitmentProgressQuery( + recruitmentId: number, + stage: 'DOCUMENT' | 'INTERVIEW', + tokens?: Tokens +) { + return useSuspenseQuery( + getRecruitmentProgressQueryOptions(recruitmentId, stage, tokens) + ); +} \ No newline at end of file diff --git a/apps/web/src/store/query/useRecruitmentsCurrentSummaryQuery.ts b/apps/web/src/store/query/useRecruitmentsCurrentSummaryQuery.ts new file mode 100644 index 00000000..133c8be0 --- /dev/null +++ b/apps/web/src/store/query/useRecruitmentsCurrentSummaryQuery.ts @@ -0,0 +1,49 @@ +import type { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { GET } from '@web/api/fetch'; +import { queryKeys } from '@web/store/constants/queryKeys'; +import type { Tokens } from '@web/api/types'; + +export interface RecruitmentSummaryDto { + recruitmentId: number; + title: string; + dDays: { + label: string; + date: string; + daysRemaining: number; + isPassed: boolean; + }[]; + organizationName: string; + totalApplicants: number; + positionCounts: { + positionName: string; + count: number; + }[]; +} + +const STALE_TIME = 1000 * 60; + +export function getRecruitmentsCurrentSummaryQueryOptions( + organizationId: number, + tokens: Tokens +): UseSuspenseQueryOptions { + return { + queryKey: queryKeys.recruitments.currentSummary(organizationId), + queryFn: () => + GET( + `api/v1/recruitments/${organizationId}/current/summary`, + undefined, + tokens + ).then(res => res.result), + staleTime: STALE_TIME, + }; +} + +export function useRecruitmentsCurrentSummaryQuery( + organizationId: number, + tokens: Tokens +) { + return useSuspenseQuery( + getRecruitmentsCurrentSummaryQueryOptions(organizationId, tokens) + ); +} \ No newline at end of file