From 40ed9504f04cec16a7f7bed165c1d9e23899b8d0 Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Tue, 24 Mar 2026 16:12:54 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat(graduate):=20=EC=9E=90=EA=B2=A9?= =?UTF-8?q?=EC=A6=9D=C2=B7=EB=85=BC=EB=AC=B8=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=A6=84=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=83=81=EC=84=B8=EB=B3=B4=EA=B8=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B3=B5=ED=86=B5=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../all/constants/allManagementColumns.tsx | 4 +- .../admin/pages/all/ui/AllManagementPage.tsx | 9 ++-- .../certification/constants/certColumns.ts | 11 ---- .../constants/certificationColumns.tsx | 38 ++++++++++++++ .../pages/thesis/constants/thesisColumns.ts | 19 ------- .../pages/thesis/constants/thesisColumns.tsx | 46 ++++++++++++++++ .../admin/shared/ui/DataTable/DataTable.tsx | 6 +-- .../ui/UserDetailModal}/UserDetailModal.tsx | 11 ++-- .../admin/shared/ui/UserDetailModal/index.ts | 1 + apps/graduate/src/admin/shared/ui/index.ts | 1 + .../StudentAddModal/ui/StudentAddSingle.tsx | 2 +- .../src/admin/widgets/Table/ui/Table.tsx | 52 +++++++++++++++---- 12 files changed, 148 insertions(+), 52 deletions(-) delete mode 100644 apps/graduate/src/admin/pages/certification/constants/certColumns.ts create mode 100644 apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx delete mode 100644 apps/graduate/src/admin/pages/thesis/constants/thesisColumns.ts create mode 100644 apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx rename apps/graduate/src/admin/{pages/all/ui => shared/ui/UserDetailModal}/UserDetailModal.tsx (95%) create mode 100644 apps/graduate/src/admin/shared/ui/UserDetailModal/index.ts diff --git a/apps/graduate/src/admin/pages/all/constants/allManagementColumns.tsx b/apps/graduate/src/admin/pages/all/constants/allManagementColumns.tsx index 0b4d66b9..43e86c68 100644 --- a/apps/graduate/src/admin/pages/all/constants/allManagementColumns.tsx +++ b/apps/graduate/src/admin/pages/all/constants/allManagementColumns.tsx @@ -12,7 +12,7 @@ import { vars } from '~/vars.css'; export const allManagementColumns = ( onNameClick: (id: number) => void, -): ReadonlyArray> => +): Column[] => [ { key: 'no', header: HEADER_NO, width: 56, cell: r => r.no }, { @@ -55,4 +55,4 @@ export const allManagementColumns = ( width: 140, cell: r => r.statusText, }, - ] as const; + ]; diff --git a/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx b/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx index aa7d13ba..42b9e550 100644 --- a/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx +++ b/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx @@ -4,13 +4,16 @@ import { LOADING_TEXT, TITLE_ALL_MANAGEMENT, } from '../constants/allManagementTexts'; +import { useAllManagement } from '../model/useAllManagement.ts'; import type { AllManagementRow } from '../types/allManagement'; import { extractPeriodData } from '../utils'; -import UserDetailModal from './UserDetailModal.tsx'; -import { useAllManagement } from '../model/useAllManagement.ts'; import * as style from '~/admin/shared/styles/adminPage.css'; -import { DataTable, Toolbar } from '~/admin/shared/ui/index.ts'; +import { + DataTable, + Toolbar, + UserDetailModal, +} from '~/admin/shared/ui/index.ts'; export default function AllManagementPage() { const { diff --git a/apps/graduate/src/admin/pages/certification/constants/certColumns.ts b/apps/graduate/src/admin/pages/certification/constants/certColumns.ts deleted file mode 100644 index 10da0649..00000000 --- a/apps/graduate/src/admin/pages/certification/constants/certColumns.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CertRow } from '../types/row'; - -import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; - -export const certColumns: ReadonlyArray> = [ - { key: 'no', header: '번호', width: 56, cell: r => r.no }, - { key: 'studentId', header: '학번', width: 120, cell: r => r.studentId }, - { key: 'name', header: '이름', width: 160, cell: r => r.name }, - { key: 'status', header: '상태', width: 120, cell: r => r.status }, - { key: 'approved', header: '승인 여부', width: 120, cell: r => r.approved }, -] as const; diff --git a/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx b/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx new file mode 100644 index 00000000..3752e97b --- /dev/null +++ b/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx @@ -0,0 +1,38 @@ +import type { CertRow } from '../types/row'; + +import { HEADER_NAME } from '~/admin/pages/all/constants/allManagementTexts'; +import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; +import { vars } from '~/vars.css'; + +export const certColumns = ( + onNameClick: (id: number) => void, +): Column[] => + [ + { key: 'no', header: '번호', width: 56, cell: r => r.no }, + { key: 'studentId', header: '학번', width: 120, cell: r => r.studentId }, + { + key: 'name', + header: HEADER_NAME, + width: 160, + cell: r => ( + + ), + }, + { key: 'status', header: '상태', width: 120, cell: r => r.status }, + { key: 'approved', header: '승인 여부', width: 120, cell: r => r.approved }, + ]; diff --git a/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.ts b/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.ts deleted file mode 100644 index 5dbd5247..00000000 --- a/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ThesisRow } from '../types/row'; - -import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; - -export const thesisColumns: ReadonlyArray> = [ - { key: 'no', header: '번호', width: 30, cell: r => r.no }, - { key: 'studentId', header: '학번', width: 90, cell: r => r.studentId }, - { key: 'name', header: '이름', width: 50, cell: r => r.name }, - { key: 'advisor', header: '지도교수', width: 50, cell: r => r.advisor }, - { key: 'gradTerm', header: '졸업 년도', width: 90, cell: r => r.gradTerm }, - { key: 'status', header: '상태', width: 130, cell: r => r.status }, - { - key: 'submissionStatus', - header: '제출 상태', - width: 70, - cell: r => r.submissionStatus, - }, - { key: 'approved', header: '승인 여부', width: 70, cell: r => r.approved }, -] as const; diff --git a/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx b/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx new file mode 100644 index 00000000..7d0f2f18 --- /dev/null +++ b/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx @@ -0,0 +1,46 @@ +import type { ThesisRow } from '../types/row'; + +import { HEADER_NAME } from '~/admin/pages/all/constants/allManagementTexts'; +import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; +import { vars } from '~/vars.css'; + +export const thesisColumns = ( + onNameClick: (id: number) => void, +): Column[] => + [ + { key: 'no', header: '번호', width: 30, cell: r => r.no }, + { key: 'studentId', header: '학번', width: 90, cell: r => r.studentId }, + { + key: 'name', + header: HEADER_NAME, + width: 50, + cell: r => ( + + ), + }, + { key: 'advisor', header: '지도교수', width: 50, cell: r => r.advisor }, + { key: 'gradTerm', header: '졸업 년도', width: 90, cell: r => r.gradTerm }, + { key: 'status', header: '상태', width: 130, cell: r => r.status }, + { + key: 'submissionStatus', + header: '제출 상태', + width: 70, + cell: r => r.submissionStatus, + }, + { key: 'approved', header: '승인 여부', width: 70, cell: r => r.approved }, + ]; diff --git a/apps/graduate/src/admin/shared/ui/DataTable/DataTable.tsx b/apps/graduate/src/admin/shared/ui/DataTable/DataTable.tsx index 3459d074..208b8b61 100644 --- a/apps/graduate/src/admin/shared/ui/DataTable/DataTable.tsx +++ b/apps/graduate/src/admin/shared/ui/DataTable/DataTable.tsx @@ -14,12 +14,12 @@ export type Column = { type Id = string | number; type Props = { - rows: ReadonlyArray; - columns: ReadonlyArray>; + rows: T[]; + columns: Column[]; getRowId: (row: T) => Id; allChecked?: boolean; onToggleAll?: () => void; - selectedIds?: ReadonlyArray; + selectedIds?: Id[]; onToggleOne?: (id: Id) => void; emptyText?: React.ReactNode; rowClassName?: (row: T, isSelected: boolean) => string | undefined; diff --git a/apps/graduate/src/admin/pages/all/ui/UserDetailModal.tsx b/apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx similarity index 95% rename from apps/graduate/src/admin/pages/all/ui/UserDetailModal.tsx rename to apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx index 461bb846..9c421f9a 100644 --- a/apps/graduate/src/admin/pages/all/ui/UserDetailModal.tsx +++ b/apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx @@ -3,14 +3,16 @@ import { Descriptions, Modal, Spin, Table } from 'antd'; import { Header } from '~/shared/ui'; -import type { PeriodData, StageData } from '../types/allManagement'; +import { useStudentDetail } from '~/admin/features/studentDetail'; +import type { + PeriodData, + StageData, +} from '~/admin/pages/all/types/allManagement'; import { buildStageData, formatSubmissionStatus, getStatusLabel, -} from '../utils'; - -import { useStudentDetail } from '~/admin/features/studentDetail'; +} from '~/admin/pages/all/utils'; import { Container } from '~/admin/shared/ui/Container'; import { vars } from '~/vars.css'; @@ -54,6 +56,7 @@ export default function UserDetailModal({ return record.isSubmitted ? ( - ), - }, - { - key: 'type', - header: HEADER_TYPE, - width: 120, - cell: r => r.graduationTypeLabel, - }, - { - key: 'status', - header: HEADER_STATUS, - width: 140, - cell: r => r.statusText, - }, - ]; +): Column[] => [ + { key: 'no', header: HEADER_NO, width: 56, cell: r => r.no }, + { + key: 'studentId', + header: HEADER_STUDENT_ID, + width: 100, + cell: r => r.studentId, + }, + { + key: 'name', + header: HEADER_NAME, + width: 120, + cell: r => ( + onNameClick(r.id)} /> + ), + }, + { + key: 'type', + header: HEADER_TYPE, + width: 120, + cell: r => r.graduationTypeLabel, + }, + { + key: 'status', + header: HEADER_STATUS, + width: 140, + cell: r => r.statusText, + }, +]; diff --git a/apps/graduate/src/admin/pages/all/model/useAllManagement.ts b/apps/graduate/src/admin/pages/all/model/useAllManagement.ts index 39861e33..252ee71b 100644 --- a/apps/graduate/src/admin/pages/all/model/useAllManagement.ts +++ b/apps/graduate/src/admin/pages/all/model/useAllManagement.ts @@ -3,10 +3,10 @@ import { useState, useEffect } from 'react'; import { useToast } from '~/shared/hooks'; + import { allManagementColumns } from '../constants/allManagementColumns'; import { TYPE_LABEL, TYPE_UNKNOWN } from '../constants/allManagementTexts'; import { AllManagementRow } from '../types/allManagement'; -import { getStatusLabel } from '../utils'; import { useScheduleList } from '~/admin/entities/admin-schedule/model'; import { useFetchGraduationUsers } from '~/admin/entities/graduation-users/model/useFetchGraduationUsers'; @@ -15,6 +15,7 @@ import { DELETE_ALERT, DELETE_CONFIRM_TITLE, } from '~/admin/shared/ui/Toolbar/toolbarTexts'; +import { getStatusLabel } from '~/admin/shared/utils'; import { useAdminDownload, useAdminPagination, diff --git a/apps/graduate/src/admin/pages/all/types/allManagement.ts b/apps/graduate/src/admin/pages/all/types/allManagement.ts index 1c0a00b2..b2b0072e 100644 --- a/apps/graduate/src/admin/pages/all/types/allManagement.ts +++ b/apps/graduate/src/admin/pages/all/types/allManagement.ts @@ -4,7 +4,14 @@ import type { ThesisSubmission, } from '~/shared/types/graduation'; -export type { CertificateSubmission as CertificateStatus, SubmissionStatus as StudentStatus, ThesisSubmission as ThesisStatus }; +import type { PeriodData, StageData } from '~/admin/shared/types/studentDetail'; + +export type { + CertificateSubmission as CertificateStatus, + SubmissionStatus as StudentStatus, + ThesisSubmission as ThesisStatus, +}; +export type { PeriodData, StageData }; export type AllManagementRow = { id: number; @@ -15,19 +22,3 @@ export type AllManagementRow = { graduationTypeLabel: string; statusText: string; }; - -export type PeriodData = { - certificate?: string; - midThesis?: string; - finalThesis?: string; -}; - -export interface StageData { - key: string; - stage: string; - period: string; - createdAt: string | null; - isSubmitted: boolean; - isApproved: boolean; - fileId: number | null; -} diff --git a/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx b/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx index 42b9e550..14c1f86c 100644 --- a/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx +++ b/apps/graduate/src/admin/pages/all/ui/AllManagementPage.tsx @@ -1,12 +1,12 @@ import { Header, Pagination } from '~/shared/ui/index.ts'; + import { LOADING_TEXT, TITLE_ALL_MANAGEMENT, } from '../constants/allManagementTexts'; import { useAllManagement } from '../model/useAllManagement.ts'; import type { AllManagementRow } from '../types/allManagement'; -import { extractPeriodData } from '../utils'; import * as style from '~/admin/shared/styles/adminPage.css'; import { @@ -14,6 +14,7 @@ import { Toolbar, UserDetailModal, } from '~/admin/shared/ui/index.ts'; +import { extractPeriodData } from '~/admin/shared/utils'; export default function AllManagementPage() { const { diff --git a/apps/graduate/src/admin/pages/all/utils/index.ts b/apps/graduate/src/admin/pages/all/utils/index.ts deleted file mode 100644 index ebcd51ff..00000000 --- a/apps/graduate/src/admin/pages/all/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './buildStageData'; -export * from './extractPeriodData'; -export * from './getStatusLabel'; diff --git a/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx b/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx index 3752e97b..939a9958 100644 --- a/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx +++ b/apps/graduate/src/admin/pages/certification/constants/certificationColumns.tsx @@ -1,38 +1,22 @@ import type { CertRow } from '../types/row'; import { HEADER_NAME } from '~/admin/pages/all/constants/allManagementTexts'; +import { NameCellButton } from '~/admin/shared/ui'; import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; -import { vars } from '~/vars.css'; export const certColumns = ( onNameClick: (id: number) => void, -): Column[] => - [ - { key: 'no', header: '번호', width: 56, cell: r => r.no }, - { key: 'studentId', header: '학번', width: 120, cell: r => r.studentId }, - { - key: 'name', - header: HEADER_NAME, - width: 160, - cell: r => ( - - ), - }, - { key: 'status', header: '상태', width: 120, cell: r => r.status }, - { key: 'approved', header: '승인 여부', width: 120, cell: r => r.approved }, - ]; +): Column[] => [ + { key: 'no', header: '번호', width: 56, cell: r => r.no }, + { key: 'studentId', header: '학번', width: 120, cell: r => r.studentId }, + { + key: 'name', + header: HEADER_NAME, + width: 160, + cell: r => ( + onNameClick(r.id)} /> + ), + }, + { key: 'status', header: '상태', width: 120, cell: r => r.status }, + { key: 'approved', header: '승인 여부', width: 120, cell: r => r.approved }, +]; diff --git a/apps/graduate/src/admin/pages/schedule/constants/schedule.ts b/apps/graduate/src/admin/pages/schedule/constants/schedule.ts index ac720912..ac121242 100644 --- a/apps/graduate/src/admin/pages/schedule/constants/schedule.ts +++ b/apps/graduate/src/admin/pages/schedule/constants/schedule.ts @@ -1,30 +1,6 @@ -import type { SubmissionType, SubmissionTypeLabel } from '~/shared/types'; - -export const SUBMISSION_TYPE_OPTIONS = [ - { value: 'SUBMITTED', label: '신청접수' }, - { value: 'MIDTHESIS', label: '중간보고서' }, - { value: 'FINALTHESIS', label: '최종보고서' }, - { value: 'CERTIFICATE', label: '자격증' }, - { value: 'APPROVED', label: '최종 통과' }, - { value: 'OTHER', label: '기타자격' }, -] as const; - -export function getSubmissionTypeByIndex( - index: number, -): SubmissionType | undefined { - return SUBMISSION_TYPE_OPTIONS[index - 1]?.value; -} - -export function getSubmissionTypeIndex(type: SubmissionType): number { - const index = SUBMISSION_TYPE_OPTIONS.findIndex( - option => option.value === type, - ); - return index !== -1 ? index + 1 : 0; -} - -export function getSubmissionTypeLabel( - type: SubmissionType, -): SubmissionTypeLabel { - const option = SUBMISSION_TYPE_OPTIONS.find(option => option.value === type); - return option?.label ?? '기타자격'; -} +export { + getSubmissionTypeByIndex, + getSubmissionTypeIndex, + getSubmissionTypeLabel, + SUBMISSION_TYPE_OPTIONS, +} from '~/shared/constants'; \ No newline at end of file diff --git a/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx b/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx index 7d0f2f18..d4694310 100644 --- a/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx +++ b/apps/graduate/src/admin/pages/thesis/constants/thesisColumns.tsx @@ -1,46 +1,30 @@ import type { ThesisRow } from '../types/row'; import { HEADER_NAME } from '~/admin/pages/all/constants/allManagementTexts'; +import { NameCellButton } from '~/admin/shared/ui'; import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; -import { vars } from '~/vars.css'; export const thesisColumns = ( onNameClick: (id: number) => void, -): Column[] => - [ - { key: 'no', header: '번호', width: 30, cell: r => r.no }, - { key: 'studentId', header: '학번', width: 90, cell: r => r.studentId }, - { - key: 'name', - header: HEADER_NAME, - width: 50, - cell: r => ( - - ), - }, - { key: 'advisor', header: '지도교수', width: 50, cell: r => r.advisor }, - { key: 'gradTerm', header: '졸업 년도', width: 90, cell: r => r.gradTerm }, - { key: 'status', header: '상태', width: 130, cell: r => r.status }, - { - key: 'submissionStatus', - header: '제출 상태', - width: 70, - cell: r => r.submissionStatus, - }, - { key: 'approved', header: '승인 여부', width: 70, cell: r => r.approved }, - ]; +): Column[] => [ + { key: 'no', header: '번호', width: 30, cell: r => r.no }, + { key: 'studentId', header: '학번', width: 90, cell: r => r.studentId }, + { + key: 'name', + header: HEADER_NAME, + width: 50, + cell: r => ( + onNameClick(r.id)} /> + ), + }, + { key: 'advisor', header: '지도교수', width: 50, cell: r => r.advisor }, + { key: 'gradTerm', header: '졸업 년도', width: 90, cell: r => r.gradTerm }, + { key: 'status', header: '상태', width: 130, cell: r => r.status }, + { + key: 'submissionStatus', + header: '제출 상태', + width: 70, + cell: r => r.submissionStatus, + }, + { key: 'approved', header: '승인 여부', width: 70, cell: r => r.approved }, +]; diff --git a/apps/graduate/src/admin/shared/types/studentDetail.ts b/apps/graduate/src/admin/shared/types/studentDetail.ts new file mode 100644 index 00000000..c40d8584 --- /dev/null +++ b/apps/graduate/src/admin/shared/types/studentDetail.ts @@ -0,0 +1,28 @@ +import type { + CertificateSubmission, + SubmissionStatus, + ThesisSubmission, +} from '~/shared/types/graduation'; + +export type { + CertificateSubmission as CertificateStatus, + ThesisSubmission as ThesisStatus, +}; + +export type StudentStatus = SubmissionStatus; + +export type PeriodData = { + certificate?: string; + midThesis?: string; + finalThesis?: string; +}; + +export interface StageData { + key: string; + stage: string; + period: string; + createdAt: string | null; + isSubmitted: boolean; + isApproved: boolean; + fileId: number | null; +} diff --git a/apps/graduate/src/admin/shared/ui/NameCellButton/NameCellButton.tsx b/apps/graduate/src/admin/shared/ui/NameCellButton/NameCellButton.tsx new file mode 100644 index 00000000..17505eba --- /dev/null +++ b/apps/graduate/src/admin/shared/ui/NameCellButton/NameCellButton.tsx @@ -0,0 +1,27 @@ +import { vars } from '~/vars.css'; + +interface NameCellButtonProps { + name: string; + onClick: () => void; +} + +export default function NameCellButton({ name, onClick }: NameCellButtonProps) { + return ( + + ); +} diff --git a/apps/graduate/src/admin/shared/ui/NameCellButton/index.ts b/apps/graduate/src/admin/shared/ui/NameCellButton/index.ts new file mode 100644 index 00000000..db36757a --- /dev/null +++ b/apps/graduate/src/admin/shared/ui/NameCellButton/index.ts @@ -0,0 +1 @@ +export { default as NameCellButton } from './NameCellButton'; diff --git a/apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx b/apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx index 9c421f9a..6b20b3df 100644 --- a/apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx +++ b/apps/graduate/src/admin/shared/ui/UserDetailModal/UserDetailModal.tsx @@ -4,16 +4,13 @@ import { Descriptions, Modal, Spin, Table } from 'antd'; import { Header } from '~/shared/ui'; import { useStudentDetail } from '~/admin/features/studentDetail'; -import type { - PeriodData, - StageData, -} from '~/admin/pages/all/types/allManagement'; +import type { PeriodData, StageData } from '~/admin/shared/types/studentDetail'; +import { Container } from '~/admin/shared/ui/Container'; import { buildStageData, formatSubmissionStatus, getStatusLabel, -} from '~/admin/pages/all/utils'; -import { Container } from '~/admin/shared/ui/Container'; +} from '~/admin/shared/utils'; import { vars } from '~/vars.css'; interface UserDetailModalProps { diff --git a/apps/graduate/src/admin/shared/ui/index.ts b/apps/graduate/src/admin/shared/ui/index.ts index 80d0e4f1..bc440bcb 100644 --- a/apps/graduate/src/admin/shared/ui/index.ts +++ b/apps/graduate/src/admin/shared/ui/index.ts @@ -2,3 +2,4 @@ export * from './TextEditor'; export * from './DataTable'; export * from './Toolbar'; export * from './UserDetailModal'; +export * from './NameCellButton'; diff --git a/apps/graduate/src/admin/pages/all/utils/extractPeriodData.ts b/apps/graduate/src/admin/shared/utils/extractPeriodData.ts similarity index 73% rename from apps/graduate/src/admin/pages/all/utils/extractPeriodData.ts rename to apps/graduate/src/admin/shared/utils/extractPeriodData.ts index 31f5ba8e..7d684854 100644 --- a/apps/graduate/src/admin/pages/all/utils/extractPeriodData.ts +++ b/apps/graduate/src/admin/shared/utils/extractPeriodData.ts @@ -1,10 +1,8 @@ -import { SCHEDULE } from '~/shared/constants'; -import { SubmissionTypeLabel } from '~/shared/types'; - -import { getSubmissionTypeLabel } from '../../schedule/constants'; -import type { PeriodData } from '../types/allManagement'; +import { getSubmissionTypeLabel, SCHEDULE } from '~/shared/constants'; +import type { SubmissionTypeLabel } from '~/shared/types'; import type { ScheduleItem } from '~/admin/pages/schedule/model'; +import type { PeriodData } from '~/admin/shared/types/studentDetail'; export function extractPeriodData( schedules: ScheduleItem[] | undefined, diff --git a/apps/graduate/src/admin/shared/utils/index.ts b/apps/graduate/src/admin/shared/utils/index.ts new file mode 100644 index 00000000..51436cb0 --- /dev/null +++ b/apps/graduate/src/admin/shared/utils/index.ts @@ -0,0 +1,3 @@ +export * from './extractPeriodData'; +export * from './studentDetailStage'; +export * from './studentStatus'; diff --git a/apps/graduate/src/admin/pages/all/utils/buildStageData.ts b/apps/graduate/src/admin/shared/utils/studentDetailStage.ts similarity index 52% rename from apps/graduate/src/admin/pages/all/utils/buildStageData.ts rename to apps/graduate/src/admin/shared/utils/studentDetailStage.ts index e917841b..b2c332ec 100644 --- a/apps/graduate/src/admin/pages/all/utils/buildStageData.ts +++ b/apps/graduate/src/admin/shared/utils/studentDetailStage.ts @@ -1,8 +1,8 @@ import type { + PeriodData, StageData, StudentStatus, - PeriodData, -} from '../types/allManagement'; +} from '~/admin/shared/types/studentDetail'; export function formatSubmissionStatus( createdAt: string | null, @@ -30,26 +30,26 @@ export function buildStageData( fileId: status.id, }, ]; - } else { - return [ - { - key: 'midthesis', - stage: '중간보고서', - period: period.midThesis ?? '-', - createdAt: status.midThesis.createdAt, - isSubmitted: status.midThesis.submitted, - isApproved: status.midThesis.approval, - fileId: status.midThesis.id, - }, - { - key: 'finalthesis', - stage: '최종보고서', - period: period.finalThesis ?? '-', - createdAt: status.finalThesis.createdAt, - isSubmitted: status.finalThesis.submitted, - isApproved: status.finalThesis.approval, - fileId: status.finalThesis.id, - }, - ]; } + + return [ + { + key: 'midthesis', + stage: '중간보고서', + period: period.midThesis ?? '-', + createdAt: status.midThesis.createdAt, + isSubmitted: status.midThesis.submitted, + isApproved: status.midThesis.approval, + fileId: status.midThesis.id, + }, + { + key: 'finalthesis', + stage: '최종보고서', + period: period.finalThesis ?? '-', + createdAt: status.finalThesis.createdAt, + isSubmitted: status.finalThesis.submitted, + isApproved: status.finalThesis.approval, + fileId: status.finalThesis.id, + }, + ]; } diff --git a/apps/graduate/src/admin/pages/all/utils/getStatusLabel.ts b/apps/graduate/src/admin/shared/utils/studentStatus.ts similarity index 57% rename from apps/graduate/src/admin/pages/all/utils/getStatusLabel.ts rename to apps/graduate/src/admin/shared/utils/studentStatus.ts index 7c99e177..cdff7e64 100644 --- a/apps/graduate/src/admin/pages/all/utils/getStatusLabel.ts +++ b/apps/graduate/src/admin/shared/utils/studentStatus.ts @@ -1,15 +1,17 @@ -import { - STATUS_UNKNOWN, - STATUS_CERTIFICATE_NOT_SUBMITTED, - STATUS_CERTIFICATE_SUBMITTED, - STATUS_CERTIFICATE_APPROVED, - STATUS_MID_REPORT_NOT_SUBMITTED, - STATUS_MID_REPORT_SUBMITTED, - STATUS_FINAL_REPORT_NOT_SUBMITTED, - STATUS_FINAL_REPORT_SUBMITTED, - STATUS_FINAL_REPORT_APPROVED, -} from '../constants/allManagementTexts'; -import type { StudentStatus } from '../types/allManagement'; +import type { StudentStatus } from '~/admin/shared/types/studentDetail'; + +const STATUS_UNKNOWN = '미정'; + +const STATUS_CERTIFICATE_NOT_SUBMITTED = '자격증 미제출'; +const STATUS_CERTIFICATE_SUBMITTED = '자격증 제출'; +const STATUS_CERTIFICATE_APPROVED = '자격증 승인'; + +const STATUS_MID_REPORT_NOT_SUBMITTED = '중간보고서 미제출'; +const STATUS_MID_REPORT_SUBMITTED = '중간보고서 제출'; + +const STATUS_FINAL_REPORT_NOT_SUBMITTED = '최종보고서 미제출'; +const STATUS_FINAL_REPORT_SUBMITTED = '최종보고서 제출'; +const STATUS_FINAL_REPORT_APPROVED = '최종보고서 승인'; export function getStatusLabel(status: StudentStatus | undefined): string { switch (status?.type) { diff --git a/apps/graduate/src/admin/widgets/Table/ui/Table.tsx b/apps/graduate/src/admin/widgets/Table/ui/Table.tsx index ae1267de..d9438681 100644 --- a/apps/graduate/src/admin/widgets/Table/ui/Table.tsx +++ b/apps/graduate/src/admin/widgets/Table/ui/Table.tsx @@ -4,7 +4,6 @@ import { useScheduleList } from '~/admin/entities/admin-schedule/model'; import { useGraduationApproval } from '~/admin/entities/graduation-approval/model'; import type { GraduationUserSummary } from '~/admin/entities/graduation-users/api'; import { useFetchGraduationUsers } from '~/admin/entities/graduation-users/model'; -import { extractPeriodData } from '~/admin/pages/all/utils'; import { certColumns } from '~/admin/pages/certification/constants/certificationColumns'; import type { CertRow } from '~/admin/pages/certification/types/row'; import { thesisColumns } from '~/admin/pages/thesis/constants/thesisColumns'; @@ -12,6 +11,7 @@ import type { ThesisRow } from '~/admin/pages/thesis/types/row'; import * as style from '~/admin/shared/styles/adminPage.css'; import { DataTable, Toolbar, UserDetailModal } from '~/admin/shared/ui'; import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; +import { extractPeriodData } from '~/admin/shared/utils'; import { useAdminDownload, useAdminSelection, diff --git a/apps/graduate/src/shared/constants/schedule.ts b/apps/graduate/src/shared/constants/schedule.ts index 538dec13..04a69e3b 100644 --- a/apps/graduate/src/shared/constants/schedule.ts +++ b/apps/graduate/src/shared/constants/schedule.ts @@ -1,3 +1,5 @@ +import type { SubmissionType, SubmissionTypeLabel } from '~/shared/types'; + export const SCHEDULE = { SUBMITTED: 'SUBMITTED', MIDTHESIS: 'MIDTHESIS', @@ -6,3 +8,40 @@ export const SCHEDULE = { APPROVED: 'APPROVED', OTHER: 'OTHER', } as const; + +const SUBMISSION_TYPE_LABEL: Record = { + SUBMITTED: '\uC2E0\uCCAD\uC811\uC218', + MIDTHESIS: '\uC911\uAC04\uBCF4\uACE0\uC11C', + FINALTHESIS: '\uCD5C\uC885\uBCF4\uACE0\uC11C', + CERTIFICATE: '\uC790\uACA9\uC99D', + APPROVED: '\uCD5C\uC885 \uD1B5\uACFC', + OTHER: '\uAE30\uD0C0\uC790\uACA9', +}; + +export const SUBMISSION_TYPE_OPTIONS = [ + { value: SCHEDULE.SUBMITTED, label: SUBMISSION_TYPE_LABEL.SUBMITTED }, + { value: SCHEDULE.MIDTHESIS, label: SUBMISSION_TYPE_LABEL.MIDTHESIS }, + { value: SCHEDULE.FINALTHESIS, label: SUBMISSION_TYPE_LABEL.FINALTHESIS }, + { value: SCHEDULE.CERTIFICATE, label: SUBMISSION_TYPE_LABEL.CERTIFICATE }, + { value: SCHEDULE.APPROVED, label: SUBMISSION_TYPE_LABEL.APPROVED }, + { value: SCHEDULE.OTHER, label: SUBMISSION_TYPE_LABEL.OTHER }, +] as const; + +export function getSubmissionTypeByIndex( + index: number, +): SubmissionType | undefined { + return SUBMISSION_TYPE_OPTIONS[index - 1]?.value; +} + +export function getSubmissionTypeIndex(type: SubmissionType): number { + const index = SUBMISSION_TYPE_OPTIONS.findIndex( + option => option.value === type, + ); + return index !== -1 ? index + 1 : 0; +} + +export function getSubmissionTypeLabel( + type: SubmissionType, +): SubmissionTypeLabel { + return SUBMISSION_TYPE_LABEL[type] ?? SUBMISSION_TYPE_LABEL.OTHER; +} From 87a226ce2a8e8be3d5d708c5bdd182291d818266 Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Tue, 24 Mar 2026 16:49:03 +0900 Subject: [PATCH 03/14] =?UTF-8?q?fix(graduate):=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=AC=B8=EA=B5=AC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/admin/shared/utils/studentStatus.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/graduate/src/admin/shared/utils/studentStatus.ts b/apps/graduate/src/admin/shared/utils/studentStatus.ts index cdff7e64..a9153403 100644 --- a/apps/graduate/src/admin/shared/utils/studentStatus.ts +++ b/apps/graduate/src/admin/shared/utils/studentStatus.ts @@ -2,16 +2,16 @@ import type { StudentStatus } from '~/admin/shared/types/studentDetail'; const STATUS_UNKNOWN = '미정'; -const STATUS_CERTIFICATE_NOT_SUBMITTED = '자격증 미제출'; -const STATUS_CERTIFICATE_SUBMITTED = '자격증 제출'; -const STATUS_CERTIFICATE_APPROVED = '자격증 승인'; +const STATUS_CERTIFICATE_NOT_SUBMITTED = '자격증-미제출'; +const STATUS_CERTIFICATE_SUBMITTED = '자격증-제출'; +const STATUS_CERTIFICATE_APPROVED = '자격증-승인'; -const STATUS_MID_REPORT_NOT_SUBMITTED = '중간보고서 미제출'; -const STATUS_MID_REPORT_SUBMITTED = '중간보고서 제출'; +const STATUS_MID_REPORT_NOT_SUBMITTED = '중간보고서-미제출'; +const STATUS_MID_REPORT_SUBMITTED = '중간보고서-제출'; -const STATUS_FINAL_REPORT_NOT_SUBMITTED = '최종보고서 미제출'; -const STATUS_FINAL_REPORT_SUBMITTED = '최종보고서 제출'; -const STATUS_FINAL_REPORT_APPROVED = '최종보고서 승인'; +const STATUS_FINAL_REPORT_NOT_SUBMITTED = '최종보고서-미제출'; +const STATUS_FINAL_REPORT_SUBMITTED = '최종보고서-제출'; +const STATUS_FINAL_REPORT_APPROVED = '최종보고서-승인'; export function getStatusLabel(status: StudentStatus | undefined): string { switch (status?.type) { From 0c280798c67ae9d62268d13aa71428a743250d8b Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Fri, 27 Mar 2026 20:42:37 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat(graduate):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=8A=B9=EC=9D=B8=20=EA=B1=B0=EB=B6=80=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EA=B2=B0=EA=B3=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/graduation-approval/api/index.ts | 1 + .../api/updateGraduationBatchRejection.ts | 30 +++ .../graduation-approval/model/index.ts | 1 + .../model/useGraduationApproval.ts | 3 + .../model/useGraduationRejection.ts | 195 ++++++++++++++++++ .../src/admin/shared/ui/Toolbar/Toolbar.tsx | 9 +- .../admin/shared/ui/Toolbar/toolbarTexts.ts | 18 ++ .../widgets/StudentAddModal/ui/ModeCard.tsx | 2 +- .../StudentAddModal/ui/StudentAddModal.tsx | 2 +- .../src/admin/widgets/Table/ui/Table.tsx | 53 +++-- .../graduate/src/shared/constants/endpoint.ts | 2 + apps/graduate/src/shared/hooks/useToast.ts | 6 +- 12 files changed, 299 insertions(+), 23 deletions(-) create mode 100644 apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchRejection.ts create mode 100644 apps/graduate/src/admin/entities/graduation-approval/model/useGraduationRejection.ts diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/index.ts b/apps/graduate/src/admin/entities/graduation-approval/api/index.ts index dcc49c02..108aaebf 100644 --- a/apps/graduate/src/admin/entities/graduation-approval/api/index.ts +++ b/apps/graduate/src/admin/entities/graduation-approval/api/index.ts @@ -1 +1,2 @@ export * from './updateGraduationBatchApproval'; +export * from './updateGraduationBatchRejection'; diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchRejection.ts b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchRejection.ts new file mode 100644 index 00000000..6707139c --- /dev/null +++ b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchRejection.ts @@ -0,0 +1,30 @@ +import { patch } from '~/shared/api'; +import { END_POINT } from '~/shared/constants'; + +type UpdateGraduationBatchRejectionRequest = { + ids: number[]; +}; + +type UpdateGraduationBatchRejectionResponse = { + disapprovedIds: number[]; +}; + +type UpdateGraduationBatchRejectionResult = { + rejectedIds: number[]; +}; + +export const updateGraduationBatchRejection = async ( + ids: number[], +): Promise => { + const response = await patch< + UpdateGraduationBatchRejectionResponse, + UpdateGraduationBatchRejectionRequest + >({ + request: END_POINT.ADMIN.GRADUATION_USERS_BATCH_REJECT, + data: { ids }, + }); + + return { + rejectedIds: response.data.disapprovedIds, + }; +}; diff --git a/apps/graduate/src/admin/entities/graduation-approval/model/index.ts b/apps/graduate/src/admin/entities/graduation-approval/model/index.ts index 1668bce6..c6f1b8fb 100644 --- a/apps/graduate/src/admin/entities/graduation-approval/model/index.ts +++ b/apps/graduate/src/admin/entities/graduation-approval/model/index.ts @@ -1,2 +1,3 @@ export * from './useGraduationApproval'; export * from './useGraduationBatchApproval'; +export * from './useGraduationRejection'; diff --git a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationApproval.ts b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationApproval.ts index e3de98aa..dda7efca 100644 --- a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationApproval.ts +++ b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationApproval.ts @@ -126,6 +126,7 @@ export function useGraduationApproval({ if (pending.length === 0) { info({ title: APPROVE_RESULT_TITLE, + centered: true, content: buildResultContent( [], buildNotApprovedDetails(notSubmitted, alreadyApproved), @@ -139,6 +140,7 @@ export function useGraduationApproval({ content: APPROVE_ALERT, okText: APPROVE_OK_TEXT, cancelText: APPROVE_CANCEL_TEXT, + centered: true, onOk: async () => { try { const result = await approveGraduationUsers( @@ -154,6 +156,7 @@ export function useGraduationApproval({ info({ title: APPROVE_RESULT_TITLE, + centered: true, content: buildResultContent( approved, buildNotApprovedDetails(notSubmitted, alreadyApproved, failed), diff --git a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationRejection.ts b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationRejection.ts new file mode 100644 index 00000000..fd57c9d9 --- /dev/null +++ b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationRejection.ts @@ -0,0 +1,195 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createElement } from 'react'; + +import { useToast } from '~/shared/hooks'; +import { graduationUsersKeys } from '~/shared/queries'; + +import { updateGraduationBatchRejection } from '~/admin/entities/graduation-approval/api'; +import { + REJECT_ALERT, + REJECT_CANCEL_TEXT, + REJECT_CONFIRM_TITLE, + REJECT_EMPTY, + REJECT_FAILED, + REJECT_NOTHING, + REJECT_OK_TEXT, + REJECT_REASON_FAILED, + REJECT_REASON_NOT_APPROVED, + REJECT_REASON_NOT_SUBMITTED, + REJECT_RESULT_NONE, + REJECT_RESULT_NOT_REJECTED, + REJECT_RESULT_REJECTED, + REJECT_RESULT_TITLE, + REJECT_SUCCESS, +} from '~/admin/shared/ui/Toolbar/toolbarTexts'; + +type UseRejectGraduationUsersProps = { + items: T[]; + selectedIds: number[]; + getId: (item: T) => number; + getLabel: (item: T) => string; + status: { + isSubmitted: (item: T) => boolean; + isApproved: (item: T) => boolean; + }; + onSuccess?: () => void | Promise; +}; + +type NotRejectedDetail = { + id: number; + label: string; + reason: string; +}; + +type RejectParams = { + ids: number[]; +}; + +export function useGraduationRejection({ + items, + selectedIds, + getId, + getLabel, + status, + onSuccess, +}: UseRejectGraduationUsersProps) { + const { toast, confirm, info } = useToast(); + const queryClient = useQueryClient(); + const rejectMutation = useMutation({ + mutationFn: ({ ids }: RejectParams) => updateGraduationBatchRejection(ids), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: graduationUsersKeys.all, + }); + await onSuccess?.(); + }, + }); + + const buildResultContent = ( + rejectedUsers: T[], + notRejectedDetails: NotRejectedDetail[], + ) => { + const rejectedLines = + rejectedUsers.length > 0 + ? rejectedUsers.map(user => `- ${getLabel(user)}`) + : [REJECT_RESULT_NONE]; + + const notRejectedLines = + notRejectedDetails.length > 0 + ? notRejectedDetails.map(item => `- ${item.label} (${item.reason})`) + : [REJECT_RESULT_NONE]; + + return [ + REJECT_RESULT_REJECTED, + ...rejectedLines, + REJECT_RESULT_NOT_REJECTED, + ...notRejectedLines, + ] + .filter(Boolean) + .map((line, index) => createElement('div', { key: index }, line)); + }; + + const buildNotRejectedDetails = ( + notSubmitted: T[], + notApproved: T[], + failed: T[] = [], + ) => [ + ...notSubmitted.map(user => ({ + id: getId(user), + label: getLabel(user), + reason: REJECT_REASON_NOT_SUBMITTED, + })), + ...notApproved.map(user => ({ + id: getId(user), + label: getLabel(user), + reason: REJECT_REASON_NOT_APPROVED, + })), + ...failed.map(user => ({ + id: getId(user), + label: getLabel(user), + reason: REJECT_REASON_FAILED, + })), + ]; + + const handleRejectSelected = () => { + if (selectedIds.length === 0) { + toast.warning(REJECT_EMPTY); + return; + } + + const selected = items.filter(item => selectedIds.includes(getId(item))); + const notSubmitted: T[] = []; + const notApproved: T[] = []; + const pending: T[] = []; + + selected.forEach(item => { + const submitted = status.isSubmitted(item); + const approved = status.isApproved(item); + + if (!submitted) { + notSubmitted.push(item); + return; + } + + if (!approved) { + notApproved.push(item); + return; + } + + pending.push(item); + }); + + if (pending.length === 0) { + info({ + title: REJECT_RESULT_TITLE, + centered: true, + content: buildResultContent( + [], + buildNotRejectedDetails(notSubmitted, notApproved), + ), + }); + return; + } + + confirm({ + title: REJECT_CONFIRM_TITLE, + content: REJECT_ALERT, + okText: REJECT_OK_TEXT, + cancelText: REJECT_CANCEL_TEXT, + centered: true, + onOk: async () => { + try { + const result = await rejectMutation.mutateAsync({ + ids: pending.map(item => getId(item)), + }); + const rejectedIdSet = new Set(result.rejectedIds); + const rejected = pending.filter(item => + rejectedIdSet.has(getId(item)), + ); + const failed = pending.filter( + item => !rejectedIdSet.has(getId(item)), + ); + + info({ + title: REJECT_RESULT_TITLE, + centered: true, + content: buildResultContent( + rejected, + buildNotRejectedDetails(notSubmitted, notApproved, failed), + ), + }); + + if (rejected.length > 0) { + toast.success(REJECT_SUCCESS); + } else { + toast.warning(REJECT_NOTHING); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : REJECT_FAILED); + } + }, + }); + }; + + return { handleRejectSelected }; +} diff --git a/apps/graduate/src/admin/shared/ui/Toolbar/Toolbar.tsx b/apps/graduate/src/admin/shared/ui/Toolbar/Toolbar.tsx index 6dab0feb..818edfd2 100644 --- a/apps/graduate/src/admin/shared/ui/Toolbar/Toolbar.tsx +++ b/apps/graduate/src/admin/shared/ui/Toolbar/Toolbar.tsx @@ -2,15 +2,16 @@ import { Button } from 'antd'; import { useState } from 'react'; import * as style from './Toolbar.css'; +import { REJECT_BUTTON_TEXT } from './toolbarTexts'; import { StudentAddModal } from '~/admin/widgets/StudentAddModal'; - type Props = { selectedCount: number; query: string; onQueryChange: (v: string) => void; onApprove: () => void; + onReject?: () => void; onDeleteSelected?: () => void; onDownload?: () => void; disabledApprove?: boolean; @@ -21,6 +22,7 @@ export default function Toolbar({ query, onQueryChange, onApprove, + onReject, onDeleteSelected, onDownload, disabledApprove = false, @@ -53,6 +55,11 @@ export default function Toolbar({ 승인 )} + {!disabledApprove && onReject && ( + + )} {onDeleteSelected && ( )} - {!disabledApprove && onReject && ( - )} {onDeleteSelected && ( @@ -71,25 +68,15 @@ export default function Toolbar({ )} {onDownload && ( - )} - + {onAddStudent && ( + + )}
@@ -102,8 +89,7 @@ export default function Toolbar({ 검색
- - setAddOpen(false)} /> ); } + diff --git a/apps/graduate/src/admin/widgets/Table/ui/Table.tsx b/apps/graduate/src/admin/widgets/Table/ui/Table.tsx index fecb6be6..e26dd553 100644 --- a/apps/graduate/src/admin/widgets/Table/ui/Table.tsx +++ b/apps/graduate/src/admin/widgets/Table/ui/Table.tsx @@ -162,7 +162,7 @@ export default function Table({ query={query} onQueryChange={onQueryChange} onApprove={handleApproveSelected} - onReject={handleDisapproveSelected} + onDisapprove={handleDisapproveSelected} onDownload={handleDownload} />
From 0bc7c72856090041014b70a261c1e35f9461ed3c Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Sat, 28 Mar 2026 01:03:15 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor(graduate):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=9C=84=EC=A0=AF=EA=B3=BC=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20=EC=95=A1=EC=85=98=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGraduationApproval.ts | 2 +- .../hooks/useGraduationDisapproval.ts | 2 +- .../admin/pages/all/model/useAllManagement.ts | 13 +- .../ui/CertificationAdminPage.tsx | 8 +- .../admin/pages/thesis/ui/ThesisAdminPage.tsx | 8 +- .../src/admin/shared/constants/actionTexts.ts | 35 +++ .../src/admin/shared/ui/Toolbar/Toolbar.tsx | 2 +- .../GraduationManagementTable/index.ts | 1 + .../GraduationManagementTable/model/index.ts | 3 + .../model/useAdminDownload.ts | 18 ++ .../model/useAdminPagination.ts | 35 +++ .../model/useAdminSelection.ts | 37 ++++ .../ui/GraduationManagementTable.tsx | 199 ++++++++++++++++++ .../utils/excelDownload.ts | 24 +++ 14 files changed, 369 insertions(+), 18 deletions(-) create mode 100644 apps/graduate/src/admin/shared/constants/actionTexts.ts create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/index.ts create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/model/index.ts create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminDownload.ts create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminPagination.ts create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminSelection.ts create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx create mode 100644 apps/graduate/src/admin/widgets/GraduationManagementTable/utils/excelDownload.ts diff --git a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts index 8276c552..5e5f45d9 100644 --- a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts +++ b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts @@ -19,7 +19,7 @@ import { APPROVE_RESULT_NOT_APPROVED, APPROVE_RESULT_TITLE, APPROVE_SUCCESS, -} from '~/admin/shared/ui/Toolbar/toolbarTexts'; +} from '~/admin/shared/constants/actionTexts'; type UseApproveGraduationUsersProps = { items: T[]; diff --git a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts index 12261371..b56e54d2 100644 --- a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts +++ b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts @@ -20,7 +20,7 @@ import { DISAPPROVE_RESULT_NOT_DISAPPROVED, DISAPPROVE_RESULT_TITLE, DISAPPROVE_SUCCESS, -} from '~/admin/shared/ui/Toolbar/toolbarTexts'; +} from '~/admin/shared/constants/actionTexts'; type UseDisapproveGraduationUsersProps = { items: T[]; diff --git a/apps/graduate/src/admin/pages/all/model/useAllManagement.ts b/apps/graduate/src/admin/pages/all/model/useAllManagement.ts index 252ee71b..28cce230 100644 --- a/apps/graduate/src/admin/pages/all/model/useAllManagement.ts +++ b/apps/graduate/src/admin/pages/all/model/useAllManagement.ts @@ -1,12 +1,11 @@ -import { useNavigate, useSearch } from '@tanstack/react-router'; -import { useState, useEffect } from 'react'; +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { useEffect, useState } from 'react'; import { useToast } from '~/shared/hooks'; - import { allManagementColumns } from '../constants/allManagementColumns'; import { TYPE_LABEL, TYPE_UNKNOWN } from '../constants/allManagementTexts'; -import { AllManagementRow } from '../types/allManagement'; +import type { AllManagementRow } from '../types/allManagement'; import { useScheduleList } from '~/admin/entities/admin-schedule/model'; import { useFetchGraduationUsers } from '~/admin/entities/graduation-users/model/useFetchGraduationUsers'; @@ -14,13 +13,13 @@ import { useRemoveGraduationUsers } from '~/admin/entities/graduation-users/mode import { DELETE_ALERT, DELETE_CONFIRM_TITLE, -} from '~/admin/shared/ui/Toolbar/toolbarTexts'; +} from '~/admin/shared/constants/actionTexts'; import { getStatusLabel } from '~/admin/shared/utils'; import { useAdminDownload, useAdminPagination, useAdminSelection, -} from '~/admin/widgets/Table/model'; +} from '~/admin/widgets/GraduationManagementTable/model'; export const useAllManagement = () => { const navigate = useNavigate(); @@ -102,7 +101,7 @@ export const useAllManagement = () => { await removeGraduationUsers(selectedIds); toast.success('선택한 학생을 삭제했습니다.'); } catch { - /* 실패 시 MutationCache 전역 토스트 */ + /* 실패 안내는 MutationCache 전역 토스트 사용 */ } }, }); diff --git a/apps/graduate/src/admin/pages/certification/ui/CertificationAdminPage.tsx b/apps/graduate/src/admin/pages/certification/ui/CertificationAdminPage.tsx index f2dcb521..9edd7206 100644 --- a/apps/graduate/src/admin/pages/certification/ui/CertificationAdminPage.tsx +++ b/apps/graduate/src/admin/pages/certification/ui/CertificationAdminPage.tsx @@ -1,8 +1,8 @@ -import { Header, Pagination } from '~/shared/ui'; +import { Header, Pagination } from '~/shared/ui'; import * as style from '~/admin/shared/styles/adminPage.css'; -import { Table } from '~/admin/widgets/Table'; -import { useAdminPagination } from '~/admin/widgets/Table/model'; +import { GraduationManagementTable } from '~/admin/widgets/GraduationManagementTable'; +import { useAdminPagination } from '~/admin/widgets/GraduationManagementTable/model'; export default function CertificationAdminPage() { const { @@ -18,7 +18,7 @@ export default function CertificationAdminPage() {
-
-
{ + try { + await excelDownload(graduationType); + } catch (error) { + notifyError(error, '다운로드에 실패했습니다.'); + } + }, [graduationType]); + + return { handleDownload }; +} diff --git a/apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminPagination.ts b/apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminPagination.ts new file mode 100644 index 00000000..d6942009 --- /dev/null +++ b/apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminPagination.ts @@ -0,0 +1,35 @@ +import { useState, useCallback } from 'react'; + +const DEFAULT_PAGE_SIZE = 10; + +export function useAdminPagination(defaultPageSize = DEFAULT_PAGE_SIZE) { + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(defaultPageSize); + const [query, setQuery] = useState(''); + + const resetToFirstPage = useCallback(() => { + setPage(1); + }, []); + + const handleQueryChange = useCallback((value: string) => { + setQuery(value); + setPage(1); + }, []); + + const handlePageSizeChange = useCallback((size: number) => { + setPageSize(size); + setPage(1); + }, []); + + return { + page, + setPage, + pageSize, + setPageSize, + query, + setQuery, + resetToFirstPage, + handleQueryChange, + handlePageSizeChange, + }; +} diff --git a/apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminSelection.ts b/apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminSelection.ts new file mode 100644 index 00000000..959b3ea3 --- /dev/null +++ b/apps/graduate/src/admin/widgets/GraduationManagementTable/model/useAdminSelection.ts @@ -0,0 +1,37 @@ +import { useState, useCallback } from 'react'; + +export function useAdminSelection(rows: T[]) { + const [selectedIds, setSelectedIds] = useState([]); + + const toggleAll = useCallback(() => { + const pageIds = rows.map(r => r.id); + const allChecked = + pageIds.length > 0 && pageIds.every(id => selectedIds.includes(id)); + setSelectedIds(prev => + allChecked + ? prev.filter(id => !pageIds.includes(id)) + : Array.from(new Set([...prev, ...pageIds])), + ); + }, [rows, selectedIds]); + + const toggleOne = useCallback((id: string | number) => { + const numericId = Number(id); + setSelectedIds(prev => + prev.includes(numericId) + ? prev.filter(x => x !== numericId) + : [...prev, numericId], + ); + }, []); + + const resetSelection = useCallback(() => { + setSelectedIds([]); + }, []); + + return { + selectedIds, + setSelectedIds, + toggleAll, + toggleOne, + resetSelection, + }; +} diff --git a/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx b/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx new file mode 100644 index 00000000..3bae5504 --- /dev/null +++ b/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; + +import { useScheduleList } from '~/admin/entities/admin-schedule/model'; +import type { GraduationUserSummary } from '~/admin/entities/graduation-users/api'; +import { useFetchGraduationUsers } from '~/admin/entities/graduation-users/model'; +import { + useGraduationApproval, + useGraduationDisapproval, +} from '~/admin/features/graduationApproval'; +import { certColumns } from '~/admin/pages/certification/constants/certificationColumns'; +import type { CertRow } from '~/admin/pages/certification/types/row'; +import { thesisColumns } from '~/admin/pages/thesis/constants/thesisColumns'; +import type { ThesisRow } from '~/admin/pages/thesis/types/row'; +import * as style from '~/admin/shared/styles/adminPage.css'; +import { DataTable, Toolbar, UserDetailModal } from '~/admin/shared/ui'; +import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; +import { + extractPeriodData, + getCurrentThesisStageState, + getThesisDisapprovalStageState, +} from '~/admin/shared/utils'; +import { + useAdminDownload, + useAdminSelection, +} from '~/admin/widgets/GraduationManagementTable/model'; +import { StudentAddModal } from '~/admin/widgets/StudentAddModal'; + +type GraduationType = 'THESIS' | 'CERTIFICATE'; +type AdminTableRow = CertRow | ThesisRow; + +interface GraduationManagementTableProps { + page: number; + pageSize: number; + query: string; + graduationType: GraduationType; + onQueryChange: (v: string) => void; +} + +export default function GraduationManagementTable({ + page, + pageSize, + query, + graduationType, + onQueryChange, +}: GraduationManagementTableProps) { + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedStudentId, setSelectedStudentId] = useState(); + + const { data: schedules } = useScheduleList(); + + const { data, isLoading } = useFetchGraduationUsers({ + page: page - 1, + size: pageSize, + name: query || undefined, + graduationType, + }); + + const getThesisCurrentStage = (user: GraduationUserSummary) => + user.status?.type === 'THESIS' + ? getCurrentThesisStageState(user.status) + : null; + + const getThesisDisapprovalStage = (user: GraduationUserSummary) => + user.status?.type === 'THESIS' + ? getThesisDisapprovalStageState(user.status) + : null; + + const rows: AdminTableRow[] = + graduationType === 'THESIS' && data + ? data.contents.map((user: GraduationUserSummary, idx: number) => { + const currentStage = getThesisCurrentStage(user); + return { + id: user.id, + no: (page - 1) * pageSize + idx + 1, + studentId: user.studentId, + name: user.name, + advisor: user.advisorProfessor, + gradTerm: user.graduationDate, + status: currentStage?.stage ?? '중간보고서', + submissionStatus: currentStage?.submissionStatus ?? '미제출', + approved: currentStage?.approvalStatus ?? '미승인', + }; + }) + : graduationType === 'CERTIFICATE' && data + ? data.contents.map((user: GraduationUserSummary, idx: number) => { + const cert = + user.status?.type === 'CERTIFICATE' ? user.status : null; + return { + id: user.id, + no: (page - 1) * pageSize + idx + 1, + studentId: user.studentId, + name: user.name, + status: cert?.submitted ? '제출' : '미제출', + approved: cert?.approval ? '승인' : '미승인', + }; + }) + : []; + + const { selectedIds, toggleAll, toggleOne, resetSelection } = + useAdminSelection(rows); + + const { handleDownload } = useAdminDownload(graduationType); + + const approvalStatus = + graduationType === 'THESIS' + ? { + isSubmitted: (u: GraduationUserSummary) => + Boolean(getThesisCurrentStage(u)?.isSubmitted), + isApproved: (u: GraduationUserSummary) => + Boolean(getThesisCurrentStage(u)?.isApproved), + } + : { + isSubmitted: (u: GraduationUserSummary) => + u.status?.type === 'CERTIFICATE' && u.status.submitted, + isApproved: (u: GraduationUserSummary) => + u.status?.type === 'CERTIFICATE' && u.status.approval, + }; + + const disapprovalStatus = + graduationType === 'THESIS' + ? { + isSubmitted: (u: GraduationUserSummary) => + Boolean(getThesisDisapprovalStage(u)?.isSubmitted), + isApproved: (u: GraduationUserSummary) => + Boolean(getThesisDisapprovalStage(u)?.isApproved), + } + : approvalStatus; + + const { handleApproveSelected } = useGraduationApproval({ + items: data?.contents ?? [], + selectedIds, + getId: (u: GraduationUserSummary) => u.id, + getLabel: (u: GraduationUserSummary) => `${u.studentId} ${u.name}`, + status: approvalStatus, + onSuccess: resetSelection, + }); + + const { handleDisapproveSelected } = useGraduationDisapproval({ + items: data?.contents ?? [], + selectedIds, + getId: (u: GraduationUserSummary) => u.id, + getLabel: (u: GraduationUserSummary) => `${u.studentId} ${u.name}`, + status: disapprovalStatus, + onSuccess: resetSelection, + }); + + const handleOpenModal = (id: number) => { + setSelectedStudentId(id); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setSelectedStudentId(undefined); + }; + + const columns: Column[] = + graduationType === 'THESIS' + ? (thesisColumns(handleOpenModal) as Column[]) + : (certColumns(handleOpenModal) as Column[]); + + return ( + <> + setIsAddModalOpen(true)} + /> +
+ + rows={rows} + getRowId={r => r.id} + columns={columns} + onToggleAll={toggleAll} + selectedIds={selectedIds} + onToggleOne={toggleOne} + emptyText={isLoading ? '불러오는 중...' : undefined} + /> +
+ {selectedStudentId && ( + + )} + setIsAddModalOpen(false)} + /> + + ); +} diff --git a/apps/graduate/src/admin/widgets/GraduationManagementTable/utils/excelDownload.ts b/apps/graduate/src/admin/widgets/GraduationManagementTable/utils/excelDownload.ts new file mode 100644 index 00000000..2619b8f6 --- /dev/null +++ b/apps/graduate/src/admin/widgets/GraduationManagementTable/utils/excelDownload.ts @@ -0,0 +1,24 @@ +import { GraduationType } from '~/shared/constants'; + +import { fetchGraduationUsersExcel } from '~/admin/entities/graduation-users/api'; + +const DEFAULT_FILENAME = '졸업_대상자_목록.xlsx'; + +export async function excelDownload( + graduationType?: GraduationType, + fallbackFilename = DEFAULT_FILENAME, +) { + const { blob, filename } = await fetchGraduationUsersExcel( + graduationType ? { graduationType } : undefined, + ); + const resolvedFilename = filename ?? fallbackFilename; + + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = resolvedFilename; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +} From 050e987d93214454ad5cd3f0f902f108eae8ad4f Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Sun, 29 Mar 2026 21:58:16 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat(graduate):=20=EC=A0=9C=EC=B6=9C=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=8A=B9=EC=9D=B8=20=EB=B0=8F=20=EC=8A=B9?= =?UTF-8?q?=EC=9D=B8=20=EC=B7=A8=EC=86=8C=20API=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/graduation-approval/api/index.ts | 4 +- .../api/updateGraduationApproval.ts | 25 ++++++++ .../api/updateGraduationBatchApproval.ts | 22 ------- .../api/updateGraduationBatchDisapproval.ts | 30 ---------- .../api/updateGraduationDisapproval.ts | 25 ++++++++ .../model/useGraduationBatchApproval.ts | 35 ++++++++--- .../model/useGraduationBatchDisapproval.ts | 42 +++++++++++--- .../hooks/useGraduationApproval.ts | 52 ++++++++++++----- .../hooks/useGraduationDisapproval.ts | 51 +++++++++++----- .../pages/filePreview/ui/FilePreviewPage.tsx | 33 +++++++---- .../src/admin/shared/utils/studentStatus.ts | 32 +++++----- .../ui/GraduationManagementTable.tsx | 15 ++--- .../graduate/src/shared/constants/endpoint.ts | 58 ++++++++++--------- 13 files changed, 265 insertions(+), 159 deletions(-) create mode 100644 apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationApproval.ts delete mode 100644 apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchApproval.ts delete mode 100644 apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchDisapproval.ts create mode 100644 apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationDisapproval.ts diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/index.ts b/apps/graduate/src/admin/entities/graduation-approval/api/index.ts index a6397a19..8cde91e4 100644 --- a/apps/graduate/src/admin/entities/graduation-approval/api/index.ts +++ b/apps/graduate/src/admin/entities/graduation-approval/api/index.ts @@ -1,2 +1,2 @@ -export * from './updateGraduationBatchApproval'; -export * from './updateGraduationBatchDisapproval'; +export * from './updateGraduationApproval'; +export * from './updateGraduationDisapproval'; diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationApproval.ts b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationApproval.ts new file mode 100644 index 00000000..973572b0 --- /dev/null +++ b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationApproval.ts @@ -0,0 +1,25 @@ +import { patch } from '~/shared/api'; +import { END_POINT } from '~/shared/constants'; + +export type UpdateGraduationApprovalParams = { + graduationUserId: number; + submissionId: number; +}; + +type UpdateGraduationApprovalResponse = { + id: number; +}; + +export const updateGraduationApproval = async ({ + graduationUserId, + submissionId, +}: UpdateGraduationApprovalParams) => { + const response = await patch({ + request: END_POINT.ADMIN.GRADUATION_USER_APPROVE( + graduationUserId, + submissionId, + ), + }); + + return response.data; +}; diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchApproval.ts b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchApproval.ts deleted file mode 100644 index 1e2b3c6a..00000000 --- a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchApproval.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { patch } from '~/shared/api'; -import { END_POINT } from '~/shared/constants'; - -type UpdateGraduationBatchApprovalRequest = { - ids: number[]; -}; - -type UpdateGraduationBatchApprovalResponse = { - approvedIds: number[]; -}; - -export const updateGraduationBatchApproval = async (ids: number[]) => { - const response = await patch< - UpdateGraduationBatchApprovalResponse, - UpdateGraduationBatchApprovalRequest - >({ - request: END_POINT.ADMIN.GRADUATION_USERS_BATCH_APPROVE, - data: { ids }, - }); - - return response.data; -}; diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchDisapproval.ts b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchDisapproval.ts deleted file mode 100644 index 560a4344..00000000 --- a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationBatchDisapproval.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { patch } from '~/shared/api'; -import { END_POINT } from '~/shared/constants'; - -type UpdateGraduationBatchDisapprovalRequest = { - ids: number[]; -}; - -type UpdateGraduationBatchDisapprovalResponse = { - disapprovedIds: number[]; -}; - -type UpdateGraduationBatchDisapprovalResult = { - disapprovedIds: number[]; -}; - -export const updateGraduationBatchDisapproval = async ( - ids: number[], -): Promise => { - const response = await patch< - UpdateGraduationBatchDisapprovalResponse, - UpdateGraduationBatchDisapprovalRequest - >({ - request: END_POINT.ADMIN.GRADUATION_USERS_BATCH_DISAPPROVE, - data: { ids }, - }); - - return { - disapprovedIds: response.data.disapprovedIds, - }; -}; diff --git a/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationDisapproval.ts b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationDisapproval.ts new file mode 100644 index 00000000..72c04003 --- /dev/null +++ b/apps/graduate/src/admin/entities/graduation-approval/api/updateGraduationDisapproval.ts @@ -0,0 +1,25 @@ +import { patch } from '~/shared/api'; +import { END_POINT } from '~/shared/constants'; + +export type UpdateGraduationDisapprovalParams = { + graduationUserId: number; + submissionId: number; +}; + +type UpdateGraduationDisapprovalResponse = { + id: number; +}; + +export const updateGraduationDisapproval = async ({ + graduationUserId, + submissionId, +}: UpdateGraduationDisapprovalParams) => { + const response = await patch({ + request: END_POINT.ADMIN.GRADUATION_USER_DISAPPROVE( + graduationUserId, + submissionId, + ), + }); + + return response.data; +}; diff --git a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchApproval.ts b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchApproval.ts index 59cc4a73..e8904b6d 100644 --- a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchApproval.ts +++ b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchApproval.ts @@ -2,15 +2,20 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { graduationUsersKeys, studentKeys } from '~/shared/queries'; -import { updateGraduationBatchApproval } from '../api'; +import { + type UpdateGraduationApprovalParams, + updateGraduationApproval, +} from '../api'; type Options = { onSuccess?: () => void | Promise; }; -type UpdateGraduationBatchApprovalResult = Awaited< - ReturnType ->; +type UpdateGraduationBatchApprovalResult = { + approvedIds: number[]; + successCount: number; + failureCount: number; +}; export function useGraduationBatchApproval({ onSuccess }: Options = {}) { const queryClient = useQueryClient(); @@ -18,10 +23,26 @@ export function useGraduationBatchApproval({ onSuccess }: Options = {}) { const mutation = useMutation< UpdateGraduationBatchApprovalResult, Error, - number[] + UpdateGraduationApprovalParams[] >({ - mutationFn: updateGraduationBatchApproval, - onSuccess: async () => { + mutationFn: async targets => { + const settled = await Promise.allSettled( + targets.map(target => updateGraduationApproval(target)), + ); + const approvedIds = settled.flatMap((result, index) => + result.status === 'fulfilled' ? [targets[index].graduationUserId] : [], + ); + + return { + approvedIds, + successCount: approvedIds.length, + failureCount: targets.length - approvedIds.length, + }; + }, + onSuccess: async result => { + if (result.successCount === 0) { + return; + } await queryClient.invalidateQueries({ queryKey: graduationUsersKeys.all, }); diff --git a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchDisapproval.ts b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchDisapproval.ts index 64f966f3..f1e57581 100644 --- a/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchDisapproval.ts +++ b/apps/graduate/src/admin/entities/graduation-approval/model/useGraduationBatchDisapproval.ts @@ -1,26 +1,50 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { graduationUsersKeys, studentKeys } from '~/shared/queries'; -import { updateGraduationBatchDisapproval } from '../api'; +import { + type UpdateGraduationDisapprovalParams, + updateGraduationDisapproval, +} from '../api'; type Options = { onSuccess?: () => void | Promise; }; -type UpdateGraduationBatchDisapprovalResult = Awaited< - ReturnType ->; +type UpdateGraduationBatchDisapprovalResult = { + disapprovedIds: number[]; + successCount: number; + failureCount: number; +}; export function useGraduationBatchDisapproval({ onSuccess }: Options = {}) { const queryClient = useQueryClient(); + const mutation = useMutation< UpdateGraduationBatchDisapprovalResult, Error, - number[] + UpdateGraduationDisapprovalParams[] >({ - mutationFn: updateGraduationBatchDisapproval, - onSuccess: async () => { + mutationFn: async targets => { + const settled = await Promise.allSettled( + targets.map(target => updateGraduationDisapproval(target)), + ); + const disapprovedIds = settled.flatMap((result, index) => + result.status === 'fulfilled' + ? [targets[index].graduationUserId] + : [], + ); + + return { + disapprovedIds, + successCount: disapprovedIds.length, + failureCount: targets.length - disapprovedIds.length, + }; + }, + onSuccess: async result => { + if (result.successCount === 0) { + return; + } await queryClient.invalidateQueries({ queryKey: graduationUsersKeys.all, }); @@ -30,9 +54,9 @@ export function useGraduationBatchDisapproval({ onSuccess }: Options = {}) { await onSuccess?.(); }, }); + return { disapproveGraduationUsers: mutation.mutateAsync, mutation, }; } - diff --git a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts index 5e5f45d9..d5dfbc7e 100644 --- a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts +++ b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts @@ -1,14 +1,14 @@ -import { createElement } from 'react'; +import { createElement } from 'react'; import { useToast } from '~/shared/hooks'; - import { useGraduationBatchApproval } from '~/admin/entities/graduation-approval/model'; import { APPROVE_ALERT, APPROVE_CANCEL_TEXT, APPROVE_CONFIRM_TITLE, APPROVE_EMPTY, + APPROVE_FAILED, APPROVE_NOTHING, APPROVE_OK_TEXT, APPROVE_REASON_ALREADY_APPROVED, @@ -26,6 +26,7 @@ type UseApproveGraduationUsersProps = { selectedIds: number[]; getId: (item: T) => number; getLabel: (item: T) => string; + getSubmissionId: (item: T) => number | null; status: { isSubmitted: (item: T) => boolean; isApproved: (item: T) => boolean; @@ -43,6 +44,7 @@ export function useGraduationApproval({ selectedIds, getId, getLabel, + getSubmissionId, status, onSuccess, }: UseApproveGraduationUsersProps) { @@ -61,7 +63,9 @@ export function useGraduationApproval({ : [APPROVE_RESULT_NONE]; const notApprovedLines = notApprovedDetails.length > 0 - ? notApprovedDetails.map(item => '- ' + item.label + ' (' + item.reason + ')') + ? notApprovedDetails.map( + item => '- ' + item.label + ' (' + item.reason + ')', + ) : [APPROVE_RESULT_NONE]; return [ @@ -103,6 +107,7 @@ export function useGraduationApproval({ const notSubmitted: T[] = []; const pending: T[] = []; const alreadyApproved: T[] = []; + const invalidTargets: T[] = []; selected.forEach(item => { const submitted = status.isSubmitted(item); @@ -116,6 +121,10 @@ export function useGraduationApproval({ alreadyApproved.push(item); return; } + if (getSubmissionId(item) == null) { + invalidTargets.push(item); + return; + } pending.push(item); }); @@ -125,7 +134,11 @@ export function useGraduationApproval({ centered: true, content: buildResultContent( [], - buildNotApprovedDetails(notSubmitted, alreadyApproved), + buildNotApprovedDetails( + notSubmitted, + alreadyApproved, + invalidTargets, + ), ), }); return; @@ -139,9 +152,19 @@ export function useGraduationApproval({ centered: true, onOk: async () => { try { - const result = await approveGraduationUsers( - pending.map(item => getId(item)), - ); + const approvalTargets = pending.flatMap(item => { + const submissionId = getSubmissionId(item); + return submissionId == null + ? [] + : [ + { + graduationUserId: getId(item), + submissionId, + }, + ]; + }); + + const result = await approveGraduationUsers(approvalTargets); const approvedIdSet = new Set(result.approvedIds); const approved = pending.filter(item => approvedIdSet.has(getId(item)), @@ -155,17 +178,23 @@ export function useGraduationApproval({ centered: true, content: buildResultContent( approved, - buildNotApprovedDetails(notSubmitted, alreadyApproved, failed), + buildNotApprovedDetails( + notSubmitted, + alreadyApproved, + [...invalidTargets, ...failed], + ), ), }); - if (approved.length > 0) { + if (result.successCount > 0) { toast.success(APPROVE_SUCCESS); + } else if (result.failureCount > 0) { + toast.error(APPROVE_FAILED); } else { toast.warning(APPROVE_NOTHING); } } catch { - /* 실패 안내는 MutationCache 전역 토스트 사용 */ + /* ?ㅽ뙣 ?덈궡??MutationCache ?꾩뿭 ?좎뒪???ъ슜 */ } }, }); @@ -173,6 +202,3 @@ export function useGraduationApproval({ return { handleApproveSelected }; } - - - diff --git a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts index b56e54d2..29c2dd1f 100644 --- a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts +++ b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts @@ -1,8 +1,7 @@ -import { createElement } from 'react'; +import { createElement } from 'react'; import { useToast } from '~/shared/hooks'; - import { useGraduationBatchDisapproval } from '~/admin/entities/graduation-approval/model'; import { DISAPPROVE_ALERT, @@ -27,6 +26,7 @@ type UseDisapproveGraduationUsersProps = { selectedIds: number[]; getId: (item: T) => number; getLabel: (item: T) => string; + getSubmissionId: (item: T) => number | null; status: { isSubmitted: (item: T) => boolean; isApproved: (item: T) => boolean; @@ -44,6 +44,7 @@ export function useGraduationDisapproval({ selectedIds, getId, getLabel, + getSubmissionId, status, onSuccess, }: UseDisapproveGraduationUsersProps) { @@ -60,10 +61,11 @@ export function useGraduationDisapproval({ disapprovedUsers.length > 0 ? disapprovedUsers.map(user => '- ' + getLabel(user)) : [DISAPPROVE_RESULT_NONE]; - const notDisapprovedLines = notDisapprovedDetails.length > 0 - ? notDisapprovedDetails.map(item => '- ' + item.label + ' (' + item.reason + ')') + ? notDisapprovedDetails.map( + item => '- ' + item.label + ' (' + item.reason + ')', + ) : [DISAPPROVE_RESULT_NONE]; return [ @@ -105,6 +107,7 @@ export function useGraduationDisapproval({ const notSubmitted: T[] = []; const notApproved: T[] = []; const pending: T[] = []; + const invalidTargets: T[] = []; selected.forEach(item => { const submitted = status.isSubmitted(item); @@ -114,11 +117,14 @@ export function useGraduationDisapproval({ notSubmitted.push(item); return; } - if (!approved) { notApproved.push(item); return; } + if (getSubmissionId(item) == null) { + invalidTargets.push(item); + return; + } pending.push(item); }); @@ -129,7 +135,11 @@ export function useGraduationDisapproval({ centered: true, content: buildResultContent( [], - buildNotDisapprovedDetails(notSubmitted, notApproved), + buildNotDisapprovedDetails( + notSubmitted, + notApproved, + invalidTargets, + ), ), }); return; @@ -143,9 +153,19 @@ export function useGraduationDisapproval({ centered: true, onOk: async () => { try { - const result = await disapproveGraduationUsers( - pending.map(item => getId(item)), - ); + const disapprovalTargets = pending.flatMap(item => { + const submissionId = getSubmissionId(item); + return submissionId == null + ? [] + : [ + { + graduationUserId: getId(item), + submissionId, + }, + ]; + }); + + const result = await disapproveGraduationUsers(disapprovalTargets); const disapprovedIdSet = new Set(result.disapprovedIds); const disapproved = pending.filter(item => disapprovedIdSet.has(getId(item)), @@ -159,12 +179,18 @@ export function useGraduationDisapproval({ centered: true, content: buildResultContent( disapproved, - buildNotDisapprovedDetails(notSubmitted, notApproved, failed), + buildNotDisapprovedDetails( + notSubmitted, + notApproved, + [...invalidTargets, ...failed], + ), ), }); - if (disapproved.length > 0) { + if (result.successCount > 0) { toast.success(DISAPPROVE_SUCCESS); + } else if (result.failureCount > 0) { + toast.error(DISAPPROVE_FAILED); } else { toast.warning(DISAPPROVE_NOTHING); } @@ -179,6 +205,3 @@ export function useGraduationDisapproval({ return { handleDisapproveSelected }; } - - - diff --git a/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx b/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx index 1cf28e7a..9665f5d5 100644 --- a/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx +++ b/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx @@ -15,6 +15,10 @@ import * as style from '../styles/FilePreviewPage.css'; import { useGraduationBatchApproval } from '~/admin/entities/graduation-approval/model'; import { useStudentDetail } from '~/admin/features/studentDetail'; +import { + APPROVE_FAILED, + APPROVE_SUCCESS, +} from '~/admin/shared/constants/actionTexts'; export default function FilePreviewPage() { const navigate = useNavigate(); @@ -37,7 +41,7 @@ export default function FilePreviewPage() { const { approveGraduationUsers, mutation: approvalMutation } = useGraduationBatchApproval({ onSuccess: async () => { - toast.success('승인이 완료되었습니다.'); + toast.success(APPROVE_SUCCESS); await queryClient.invalidateQueries({ queryKey: [...studentKeys.files()], }); @@ -49,7 +53,7 @@ export default function FilePreviewPage() { }); const clearHideTimeout = () => { - if (hideTimeoutRef.current) { + if (hideTimeoutRef.current !== null) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } @@ -95,21 +99,25 @@ export default function FilePreviewPage() { } const { scheduleId, file, approval } = fileData; - const { name, studentId, status } = studentDetail; const handleGoBack = () => { navigate({ to: ROUTE.ALL, - search: { graduationUserId: graduationUserId }, + search: { graduationUserId }, }); }; const handleApprove = async () => { - try { - await approveGraduationUsers([graduationUserId]); - } catch { - /* 실패 시 MutationCache 전역 토스트 */ + const result = await approveGraduationUsers([ + { + graduationUserId, + submissionId: fileId, + }, + ]); + + if (result.successCount === 0) { + toast.error(APPROVE_FAILED); } }; @@ -126,15 +134,18 @@ export default function FilePreviewPage() { to: ROUTE.FILE_PREVIEW, search: { fileId: status.finalThesis.id!, - graduationUserId: graduationUserId, + graduationUserId, }, }); - } else if (scheduleId === getSubmissionTypeIndex('FINALTHESIS')) { + return; + } + + if (scheduleId === getSubmissionTypeIndex('FINALTHESIS')) { navigate({ to: ROUTE.FILE_PREVIEW, search: { fileId: status.midThesis.id!, - graduationUserId: graduationUserId, + graduationUserId, }, }); } diff --git a/apps/graduate/src/admin/shared/utils/studentStatus.ts b/apps/graduate/src/admin/shared/utils/studentStatus.ts index db5de013..aa5111e4 100644 --- a/apps/graduate/src/admin/shared/utils/studentStatus.ts +++ b/apps/graduate/src/admin/shared/utils/studentStatus.ts @@ -24,6 +24,7 @@ export type ThesisStageState = { stage: ThesisStageLabel; submissionStatus: SubmissionLabel; approvalStatus: ApprovalLabel; + submissionId: number | null; isSubmitted: boolean; isApproved: boolean; }; @@ -32,27 +33,23 @@ export function getCurrentThesisStageState( status: ThesisStatus, ): ThesisStageState { if (!status.midThesis.approval) { - const isSubmitted = status.midThesis.submitted; - const isApproved = status.midThesis.approval; - return { stage: '중간보고서', - submissionStatus: isSubmitted ? '제출' : '미제출', - approvalStatus: isApproved ? '승인' : '미승인', - isSubmitted, - isApproved, + submissionStatus: status.midThesis.submitted ? '제출' : '미제출', + approvalStatus: status.midThesis.approval ? '승인' : '미승인', + submissionId: status.midThesis.id, + isSubmitted: status.midThesis.submitted, + isApproved: status.midThesis.approval, }; } - const isSubmitted = status.finalThesis.submitted; - const isApproved = status.finalThesis.approval; - return { stage: '최종보고서', - submissionStatus: isSubmitted ? '제출' : '미제출', - approvalStatus: isApproved ? '승인' : '미승인', - isSubmitted, - isApproved, + submissionStatus: status.finalThesis.submitted ? '제출' : '미제출', + approvalStatus: status.finalThesis.approval ? '승인' : '미승인', + submissionId: status.finalThesis.id, + isSubmitted: status.finalThesis.submitted, + isApproved: status.finalThesis.approval, }; } @@ -64,6 +61,7 @@ export function getThesisDisapprovalStageState( stage: '최종보고서', submissionStatus: '제출', approvalStatus: '승인', + submissionId: status.finalThesis.id, isSubmitted: true, isApproved: true, }; @@ -74,6 +72,7 @@ export function getThesisDisapprovalStageState( stage: '중간보고서', submissionStatus: '제출', approvalStatus: '승인', + submissionId: status.midThesis.id, isSubmitted: true, isApproved: true, }; @@ -84,6 +83,7 @@ export function getThesisDisapprovalStageState( stage: '최종보고서', submissionStatus: '제출', approvalStatus: '미승인', + submissionId: status.finalThesis.id, isSubmitted: true, isApproved: false, }; @@ -94,6 +94,7 @@ export function getThesisDisapprovalStageState( stage: '중간보고서', submissionStatus: '제출', approvalStatus: '미승인', + submissionId: status.midThesis.id, isSubmitted: true, isApproved: false, }; @@ -103,6 +104,7 @@ export function getThesisDisapprovalStageState( stage: '중간보고서', submissionStatus: '미제출', approvalStatus: '미승인', + submissionId: status.midThesis.id, isSubmitted: false, isApproved: false, }; @@ -126,14 +128,12 @@ export function getStatusLabel(status: StudentStatus | undefined): string { if (!status.midThesis.approval) { return STATUS_MID_REPORT_SUBMITTED; } - if (!status.finalThesis.submitted) { return STATUS_FINAL_REPORT_NOT_SUBMITTED; } if (!status.finalThesis.approval) { return STATUS_FINAL_REPORT_SUBMITTED; } - return STATUS_FINAL_REPORT_APPROVED; default: diff --git a/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx b/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx index 3bae5504..30ee568f 100644 --- a/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx +++ b/apps/graduate/src/admin/widgets/GraduationManagementTable/ui/GraduationManagementTable.tsx @@ -23,7 +23,6 @@ import { useAdminDownload, useAdminSelection, } from '~/admin/widgets/GraduationManagementTable/model'; -import { StudentAddModal } from '~/admin/widgets/StudentAddModal'; type GraduationType = 'THESIS' | 'CERTIFICATE'; type AdminTableRow = CertRow | ThesisRow; @@ -43,7 +42,6 @@ export default function GraduationManagementTable({ graduationType, onQueryChange, }: GraduationManagementTableProps) { - const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedStudentId, setSelectedStudentId] = useState(); @@ -109,12 +107,16 @@ export default function GraduationManagementTable({ Boolean(getThesisCurrentStage(u)?.isSubmitted), isApproved: (u: GraduationUserSummary) => Boolean(getThesisCurrentStage(u)?.isApproved), + getSubmissionId: (u: GraduationUserSummary) => + getThesisCurrentStage(u)?.submissionId ?? null, } : { isSubmitted: (u: GraduationUserSummary) => u.status?.type === 'CERTIFICATE' && u.status.submitted, isApproved: (u: GraduationUserSummary) => u.status?.type === 'CERTIFICATE' && u.status.approval, + getSubmissionId: (u: GraduationUserSummary) => + u.status?.type === 'CERTIFICATE' ? u.status.id : null, }; const disapprovalStatus = @@ -124,6 +126,8 @@ export default function GraduationManagementTable({ Boolean(getThesisDisapprovalStage(u)?.isSubmitted), isApproved: (u: GraduationUserSummary) => Boolean(getThesisDisapprovalStage(u)?.isApproved), + getSubmissionId: (u: GraduationUserSummary) => + getThesisDisapprovalStage(u)?.submissionId ?? null, } : approvalStatus; @@ -132,6 +136,7 @@ export default function GraduationManagementTable({ selectedIds, getId: (u: GraduationUserSummary) => u.id, getLabel: (u: GraduationUserSummary) => `${u.studentId} ${u.name}`, + getSubmissionId: approvalStatus.getSubmissionId, status: approvalStatus, onSuccess: resetSelection, }); @@ -141,6 +146,7 @@ export default function GraduationManagementTable({ selectedIds, getId: (u: GraduationUserSummary) => u.id, getLabel: (u: GraduationUserSummary) => `${u.studentId} ${u.name}`, + getSubmissionId: disapprovalStatus.getSubmissionId, status: disapprovalStatus, onSuccess: resetSelection, }); @@ -169,7 +175,6 @@ export default function GraduationManagementTable({ onApprove={handleApproveSelected} onDisapprove={handleDisapproveSelected} onDownload={handleDownload} - onAddStudent={() => setIsAddModalOpen(true)} />
@@ -190,10 +195,6 @@ export default function GraduationManagementTable({ period={extractPeriodData(schedules)} /> )} - setIsAddModalOpen(false)} - /> ); } diff --git a/apps/graduate/src/shared/constants/endpoint.ts b/apps/graduate/src/shared/constants/endpoint.ts index cdb4159f..649007ff 100644 --- a/apps/graduate/src/shared/constants/endpoint.ts +++ b/apps/graduate/src/shared/constants/endpoint.ts @@ -1,3 +1,7 @@ +import type { WorkflowStage } from '~/shared/types/graduation'; + +import { SubmissionType } from '../types'; + export const API_URL = import.meta.env.VITE_API_URL || 'https://aics-api.kgudevelopers.monster'; @@ -9,30 +13,8 @@ export const API_ADMIN_URL = import.meta.env.VITE_ADMIN_API_URL || 'https://aics-admin.kgudevelopers.monster'; -export type EndpointValue = T extends string - ? T - : T extends (...args: any[]) => infer R - ? R extends string - ? R - : never - : T extends Record - ? { - [K in keyof T]: EndpointValue; - }[keyof T] - : never; - -export type EndpointPath = EndpointValue; - -/** - * AUTH, ADMIN, USER 세 가지 타입의 엔드포인트를 정의합니다. - * 이떄 타입의 구분은 인스턴스(baseURL)에 따라 결정됩니다. - * 타입 아래 더한 Depth를 가지는 것은 불가능합니다. (ex. AUTH.LOGIN.DETAIL.USER_ID) - * 현재 Depth 지켜서 엔드포인트 추가해주세요. - */ -import type { WorkflowStage } from '~/shared/types/graduation'; - -import { SubmissionType } from '../types'; - +// Request path typing is used by the shared axios wrapper to select base URLs. +// Keep this object shallow and extend the current depth instead of nesting deeper. export const END_POINT = { AUTH: { LOGIN: '/api/v1/auth/login', @@ -44,10 +26,16 @@ export const END_POINT = { GRADUATION_USER: (graduationUserId: number) => `/api/v1/admin/graduation-users/${graduationUserId}`, GRADUATION_USERS_BATCH: '/api/v1/admin/graduation-users/batch', - GRADUATION_USERS_BATCH_APPROVE: - '/api/v1/admin/graduation-users/batch/approve', - GRADUATION_USERS_BATCH_DISAPPROVE: - '/api/v1/admin/graduation-users/batch/disapprove', + GRADUATION_USER_APPROVE: ( + graduationUserId: number, + submissionId: number, + ) => + `/api/v1/admin/graduation-users/approve/${graduationUserId}/${submissionId}`, + GRADUATION_USER_DISAPPROVE: ( + graduationUserId: number, + submissionId: number, + ) => + `/api/v1/admin/graduation-users/disapprove/${graduationUserId}/${submissionId}`, USERS: '/api/v1/admin/users', SCHEDULE_CREATE: '/api/v1/admin/schedules', SCHEDULE_DELETE: (scheduleId: number) => @@ -86,3 +74,17 @@ export const END_POINT = { `/api/v1/admin/graduation-users/${studentId}`, }, } as const; + +type EndpointValue = T extends string + ? T + : T extends (...args: never[]) => infer R + ? R extends string + ? R + : never + : T extends Record + ? { + [K in keyof T]: EndpointValue; + }[keyof T] + : never; + +export type EndpointPath = EndpointValue; From 8a7e9586e8ed7e64c727f44701020afc804b33b9 Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Sun, 29 Mar 2026 21:58:32 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor(graduate):=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EA=B4=80=EB=A6=AC=EC=9E=90=20Table=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=EC=9D=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduate/src/admin/widgets/Table/index.ts | 1 - .../src/admin/widgets/Table/model/index.ts | 3 - .../widgets/Table/model/useAdminDownload.ts | 18 -- .../widgets/Table/model/useAdminPagination.ts | 35 ---- .../widgets/Table/model/useAdminSelection.ts | 37 ---- .../src/admin/widgets/Table/ui/Table.tsx | 192 ------------------ .../widgets/Table/utils/excelDownload.ts | 24 --- 7 files changed, 310 deletions(-) delete mode 100644 apps/graduate/src/admin/widgets/Table/index.ts delete mode 100644 apps/graduate/src/admin/widgets/Table/model/index.ts delete mode 100644 apps/graduate/src/admin/widgets/Table/model/useAdminDownload.ts delete mode 100644 apps/graduate/src/admin/widgets/Table/model/useAdminPagination.ts delete mode 100644 apps/graduate/src/admin/widgets/Table/model/useAdminSelection.ts delete mode 100644 apps/graduate/src/admin/widgets/Table/ui/Table.tsx delete mode 100644 apps/graduate/src/admin/widgets/Table/utils/excelDownload.ts diff --git a/apps/graduate/src/admin/widgets/Table/index.ts b/apps/graduate/src/admin/widgets/Table/index.ts deleted file mode 100644 index 56dffdaa..00000000 --- a/apps/graduate/src/admin/widgets/Table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Table } from './ui/Table'; diff --git a/apps/graduate/src/admin/widgets/Table/model/index.ts b/apps/graduate/src/admin/widgets/Table/model/index.ts deleted file mode 100644 index 8cfa6680..00000000 --- a/apps/graduate/src/admin/widgets/Table/model/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { useAdminSelection } from './useAdminSelection'; -export { useAdminPagination } from './useAdminPagination'; -export { useAdminDownload } from './useAdminDownload'; diff --git a/apps/graduate/src/admin/widgets/Table/model/useAdminDownload.ts b/apps/graduate/src/admin/widgets/Table/model/useAdminDownload.ts deleted file mode 100644 index e3ebe0c3..00000000 --- a/apps/graduate/src/admin/widgets/Table/model/useAdminDownload.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useCallback } from 'react'; - -import { GraduationType } from '~/shared/constants'; -import { notifyError } from '~/shared/utils'; - -import { excelDownload } from '../utils/excelDownload'; - -export function useAdminDownload(graduationType?: GraduationType) { - const handleDownload = useCallback(async () => { - try { - await excelDownload(graduationType); - } catch (error) { - notifyError(error, '다운로드에 실패했습니다.'); - } - }, [graduationType]); - - return { handleDownload }; -} diff --git a/apps/graduate/src/admin/widgets/Table/model/useAdminPagination.ts b/apps/graduate/src/admin/widgets/Table/model/useAdminPagination.ts deleted file mode 100644 index d6942009..00000000 --- a/apps/graduate/src/admin/widgets/Table/model/useAdminPagination.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useState, useCallback } from 'react'; - -const DEFAULT_PAGE_SIZE = 10; - -export function useAdminPagination(defaultPageSize = DEFAULT_PAGE_SIZE) { - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(defaultPageSize); - const [query, setQuery] = useState(''); - - const resetToFirstPage = useCallback(() => { - setPage(1); - }, []); - - const handleQueryChange = useCallback((value: string) => { - setQuery(value); - setPage(1); - }, []); - - const handlePageSizeChange = useCallback((size: number) => { - setPageSize(size); - setPage(1); - }, []); - - return { - page, - setPage, - pageSize, - setPageSize, - query, - setQuery, - resetToFirstPage, - handleQueryChange, - handlePageSizeChange, - }; -} diff --git a/apps/graduate/src/admin/widgets/Table/model/useAdminSelection.ts b/apps/graduate/src/admin/widgets/Table/model/useAdminSelection.ts deleted file mode 100644 index 959b3ea3..00000000 --- a/apps/graduate/src/admin/widgets/Table/model/useAdminSelection.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useState, useCallback } from 'react'; - -export function useAdminSelection(rows: T[]) { - const [selectedIds, setSelectedIds] = useState([]); - - const toggleAll = useCallback(() => { - const pageIds = rows.map(r => r.id); - const allChecked = - pageIds.length > 0 && pageIds.every(id => selectedIds.includes(id)); - setSelectedIds(prev => - allChecked - ? prev.filter(id => !pageIds.includes(id)) - : Array.from(new Set([...prev, ...pageIds])), - ); - }, [rows, selectedIds]); - - const toggleOne = useCallback((id: string | number) => { - const numericId = Number(id); - setSelectedIds(prev => - prev.includes(numericId) - ? prev.filter(x => x !== numericId) - : [...prev, numericId], - ); - }, []); - - const resetSelection = useCallback(() => { - setSelectedIds([]); - }, []); - - return { - selectedIds, - setSelectedIds, - toggleAll, - toggleOne, - resetSelection, - }; -} diff --git a/apps/graduate/src/admin/widgets/Table/ui/Table.tsx b/apps/graduate/src/admin/widgets/Table/ui/Table.tsx deleted file mode 100644 index e26dd553..00000000 --- a/apps/graduate/src/admin/widgets/Table/ui/Table.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { useState } from 'react'; - -import { useScheduleList } from '~/admin/entities/admin-schedule/model'; -import type { GraduationUserSummary } from '~/admin/entities/graduation-users/api'; -import { useFetchGraduationUsers } from '~/admin/entities/graduation-users/model'; -import { - useGraduationApproval, - useGraduationDisapproval, -} from '~/admin/features/graduationApproval'; -import { certColumns } from '~/admin/pages/certification/constants/certificationColumns'; -import type { CertRow } from '~/admin/pages/certification/types/row'; -import { thesisColumns } from '~/admin/pages/thesis/constants/thesisColumns'; -import type { ThesisRow } from '~/admin/pages/thesis/types/row'; -import * as style from '~/admin/shared/styles/adminPage.css'; -import { DataTable, Toolbar, UserDetailModal } from '~/admin/shared/ui'; -import type { Column } from '~/admin/shared/ui/DataTable/DataTable'; -import { - extractPeriodData, - getCurrentThesisStageState, - getThesisDisapprovalStageState, -} from '~/admin/shared/utils'; -import { - useAdminDownload, - useAdminSelection, -} from '~/admin/widgets/Table/model'; - -type GraduationType = 'THESIS' | 'CERTIFICATE'; -type AdminTableRow = CertRow | ThesisRow; - -interface TableProps { - page: number; - pageSize: number; - query: string; - graduationType: GraduationType; - onQueryChange: (v: string) => void; -} - -export default function Table({ - page, - pageSize, - query, - graduationType, - onQueryChange, -}: TableProps) { - const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedStudentId, setSelectedStudentId] = useState(); - - const { data: schedules } = useScheduleList(); - - const { data, isLoading } = useFetchGraduationUsers({ - page: page - 1, - size: pageSize, - name: query || undefined, - graduationType, - }); - - const rows: AdminTableRow[] = - graduationType === 'THESIS' && data - ? data.contents.map((user: GraduationUserSummary, idx: number) => { - const thesis = user.status?.type === 'THESIS' ? user.status : null; - const currentStage = thesis - ? getCurrentThesisStageState(thesis) - : null; - return { - id: user.id, - no: (page - 1) * pageSize + idx + 1, - studentId: user.studentId, - name: user.name, - advisor: user.advisorProfessor, - gradTerm: user.graduationDate, - status: currentStage?.stage ?? '중간보고서', - submissionStatus: currentStage?.submissionStatus ?? '미제출', - approved: currentStage?.approvalStatus ?? '미승인', - }; - }) - : graduationType === 'CERTIFICATE' && data - ? data.contents.map((user: GraduationUserSummary, idx: number) => { - const cert = - user.status?.type === 'CERTIFICATE' ? user.status : null; - return { - id: user.id, - no: (page - 1) * pageSize + idx + 1, - studentId: user.studentId, - name: user.name, - status: cert?.submitted ? '제출' : '미제출', - approved: cert?.approval ? '승인' : '미승인', - }; - }) - : []; - - const { selectedIds, toggleAll, toggleOne, resetSelection } = - useAdminSelection(rows); - - const { handleDownload } = useAdminDownload(graduationType); - - const approvalStatus = - graduationType === 'THESIS' - ? { - isSubmitted: (u: GraduationUserSummary) => - u.status?.type === 'THESIS' && - getCurrentThesisStageState(u.status).isSubmitted, - isApproved: (u: GraduationUserSummary) => - u.status?.type === 'THESIS' && - getCurrentThesisStageState(u.status).isApproved, - } - : { - isSubmitted: (u: GraduationUserSummary) => - u.status?.type === 'CERTIFICATE' && u.status.submitted, - isApproved: (u: GraduationUserSummary) => - u.status?.type === 'CERTIFICATE' && u.status.approval, - }; - - const disapprovalStatus = - graduationType === 'THESIS' - ? { - isSubmitted: (u: GraduationUserSummary) => - u.status?.type === 'THESIS' && - getThesisDisapprovalStageState(u.status).isSubmitted, - isApproved: (u: GraduationUserSummary) => - u.status?.type === 'THESIS' && - getThesisDisapprovalStageState(u.status).isApproved, - } - : approvalStatus; - - const { handleApproveSelected } = useGraduationApproval({ - items: data?.contents ?? [], - selectedIds, - getId: (u: GraduationUserSummary) => u.id, - getLabel: (u: GraduationUserSummary) => `${u.studentId} ${u.name}`, - status: approvalStatus, - onSuccess: resetSelection, - }); - - const { handleDisapproveSelected } = useGraduationDisapproval({ - items: data?.contents ?? [], - selectedIds, - getId: (u: GraduationUserSummary) => u.id, - getLabel: (u: GraduationUserSummary) => `${u.studentId} ${u.name}`, - status: disapprovalStatus, - onSuccess: resetSelection, - }); - - const handleOpenModal = (id: number) => { - setSelectedStudentId(id); - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setIsModalOpen(false); - setSelectedStudentId(undefined); - }; - - const columns: Column[] = - graduationType === 'THESIS' - ? (thesisColumns(handleOpenModal) as Column[]) - : (certColumns(handleOpenModal) as Column[]); - - return ( - <> - -
- - rows={rows} - getRowId={r => r.id} - columns={columns} - onToggleAll={toggleAll} - selectedIds={selectedIds} - onToggleOne={toggleOne} - emptyText={isLoading ? '불러오는 중...' : undefined} - /> -
- {selectedStudentId && ( - - )} - - ); -} - - - diff --git a/apps/graduate/src/admin/widgets/Table/utils/excelDownload.ts b/apps/graduate/src/admin/widgets/Table/utils/excelDownload.ts deleted file mode 100644 index 2619b8f6..00000000 --- a/apps/graduate/src/admin/widgets/Table/utils/excelDownload.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { GraduationType } from '~/shared/constants'; - -import { fetchGraduationUsersExcel } from '~/admin/entities/graduation-users/api'; - -const DEFAULT_FILENAME = '졸업_대상자_목록.xlsx'; - -export async function excelDownload( - graduationType?: GraduationType, - fallbackFilename = DEFAULT_FILENAME, -) { - const { blob, filename } = await fetchGraduationUsersExcel( - graduationType ? { graduationType } : undefined, - ); - const resolvedFilename = filename ?? fallbackFilename; - - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = resolvedFilename; - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); -} From ca9e18d26d5082ea9e1c6c62a273a7c06fa35cfe Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Sun, 29 Mar 2026 22:28:38 +0900 Subject: [PATCH 11/14] =?UTF-8?q?fix(graduate):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=83=81=ED=83=9C=20=EB=AC=B8=EA=B5=AC=20=EA=B9=A8?= =?UTF-8?q?=EC=A7=90=20=EB=B0=8F=20=ED=91=9C=EC=8B=9C=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/pages/certification/types/row.ts | 4 +- .../pages/filePreview/ui/FilePreviewPage.tsx | 4 +- .../src/admin/pages/thesis/types/row.ts | 4 +- .../src/admin/shared/utils/studentStatus.ts | 66 +++++++++---------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/apps/graduate/src/admin/pages/certification/types/row.ts b/apps/graduate/src/admin/pages/certification/types/row.ts index a85822b1..041773bc 100644 --- a/apps/graduate/src/admin/pages/certification/types/row.ts +++ b/apps/graduate/src/admin/pages/certification/types/row.ts @@ -3,6 +3,6 @@ export type CertRow = { no: number; studentId: string; name: string; - status: '제출' | '미제출'; - approved: '승인' | '미승인'; + status: string; + approved: string; }; diff --git a/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx b/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx index 9665f5d5..d75fe115 100644 --- a/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx +++ b/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx @@ -92,7 +92,7 @@ export default function FilePreviewPage() { return (
- 데이터를 불러올 수 없습니다. + ???? ??? ? ????.
); @@ -156,7 +156,7 @@ export default function FilePreviewPage() {
diff --git a/apps/graduate/src/admin/pages/thesis/types/row.ts b/apps/graduate/src/admin/pages/thesis/types/row.ts index 30728f9b..770dcc30 100644 --- a/apps/graduate/src/admin/pages/thesis/types/row.ts +++ b/apps/graduate/src/admin/pages/thesis/types/row.ts @@ -6,6 +6,6 @@ export type ThesisRow = { advisor: string; gradTerm: string; status: string; - submissionStatus: '제출' | '미제출'; - approved: '승인' | '미승인'; + submissionStatus: string; + approved: string; }; diff --git a/apps/graduate/src/admin/shared/utils/studentStatus.ts b/apps/graduate/src/admin/shared/utils/studentStatus.ts index aa5111e4..41c36237 100644 --- a/apps/graduate/src/admin/shared/utils/studentStatus.ts +++ b/apps/graduate/src/admin/shared/utils/studentStatus.ts @@ -3,22 +3,22 @@ import type { ThesisStatus, } from '~/admin/shared/types/studentDetail'; -const STATUS_UNKNOWN = '미정'; +const STATUS_UNKNOWN = '??'; -const STATUS_CERTIFICATE_NOT_SUBMITTED = '자격증-미제출'; -const STATUS_CERTIFICATE_SUBMITTED = '자격증-제출'; -const STATUS_CERTIFICATE_APPROVED = '자격증-승인'; +const STATUS_CERTIFICATE_NOT_SUBMITTED = '???-???'; +const STATUS_CERTIFICATE_SUBMITTED = '???-??'; +const STATUS_CERTIFICATE_APPROVED = '???-??'; -const STATUS_MID_REPORT_NOT_SUBMITTED = '중간보고서-미제출'; -const STATUS_MID_REPORT_SUBMITTED = '중간보고서-제출'; +const STATUS_MID_REPORT_NOT_SUBMITTED = '?????-???'; +const STATUS_MID_REPORT_SUBMITTED = '?????-??'; -const STATUS_FINAL_REPORT_NOT_SUBMITTED = '최종보고서-미제출'; -const STATUS_FINAL_REPORT_SUBMITTED = '최종보고서-제출'; -const STATUS_FINAL_REPORT_APPROVED = '최종보고서-승인'; +const STATUS_FINAL_REPORT_NOT_SUBMITTED = '?????-???'; +const STATUS_FINAL_REPORT_SUBMITTED = '?????-??'; +const STATUS_FINAL_REPORT_APPROVED = '?????-??'; -type ThesisStageLabel = '중간보고서' | '최종보고서'; -type SubmissionLabel = '제출' | '미제출'; -type ApprovalLabel = '승인' | '미승인'; +type ThesisStageLabel = '?????' | '?????'; +type SubmissionLabel = '??' | '???'; +type ApprovalLabel = '??' | '???'; export type ThesisStageState = { stage: ThesisStageLabel; @@ -34,9 +34,9 @@ export function getCurrentThesisStageState( ): ThesisStageState { if (!status.midThesis.approval) { return { - stage: '중간보고서', - submissionStatus: status.midThesis.submitted ? '제출' : '미제출', - approvalStatus: status.midThesis.approval ? '승인' : '미승인', + stage: '?????', + submissionStatus: status.midThesis.submitted ? '??' : '???', + approvalStatus: status.midThesis.approval ? '??' : '???', submissionId: status.midThesis.id, isSubmitted: status.midThesis.submitted, isApproved: status.midThesis.approval, @@ -44,9 +44,9 @@ export function getCurrentThesisStageState( } return { - stage: '최종보고서', - submissionStatus: status.finalThesis.submitted ? '제출' : '미제출', - approvalStatus: status.finalThesis.approval ? '승인' : '미승인', + stage: '?????', + submissionStatus: status.finalThesis.submitted ? '??' : '???', + approvalStatus: status.finalThesis.approval ? '??' : '???', submissionId: status.finalThesis.id, isSubmitted: status.finalThesis.submitted, isApproved: status.finalThesis.approval, @@ -58,9 +58,9 @@ export function getThesisDisapprovalStageState( ): ThesisStageState { if (status.finalThesis.approval) { return { - stage: '최종보고서', - submissionStatus: '제출', - approvalStatus: '승인', + stage: '?????', + submissionStatus: '??', + approvalStatus: '??', submissionId: status.finalThesis.id, isSubmitted: true, isApproved: true, @@ -69,9 +69,9 @@ export function getThesisDisapprovalStageState( if (status.midThesis.approval) { return { - stage: '중간보고서', - submissionStatus: '제출', - approvalStatus: '승인', + stage: '?????', + submissionStatus: '??', + approvalStatus: '??', submissionId: status.midThesis.id, isSubmitted: true, isApproved: true, @@ -80,9 +80,9 @@ export function getThesisDisapprovalStageState( if (status.finalThesis.submitted) { return { - stage: '최종보고서', - submissionStatus: '제출', - approvalStatus: '미승인', + stage: '?????', + submissionStatus: '??', + approvalStatus: '???', submissionId: status.finalThesis.id, isSubmitted: true, isApproved: false, @@ -91,9 +91,9 @@ export function getThesisDisapprovalStageState( if (status.midThesis.submitted) { return { - stage: '중간보고서', - submissionStatus: '제출', - approvalStatus: '미승인', + stage: '?????', + submissionStatus: '??', + approvalStatus: '???', submissionId: status.midThesis.id, isSubmitted: true, isApproved: false, @@ -101,9 +101,9 @@ export function getThesisDisapprovalStageState( } return { - stage: '중간보고서', - submissionStatus: '미제출', - approvalStatus: '미승인', + stage: '?????', + submissionStatus: '???', + approvalStatus: '???', submissionId: status.midThesis.id, isSubmitted: false, isApproved: false, From 7a4dc59c29f381044830e32757f2a739566bb027 Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Sun, 29 Mar 2026 23:34:48 +0900 Subject: [PATCH 12/14] =?UTF-8?q?fix(graduate):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=83=81=ED=83=9C=20=EB=AC=B8=EA=B5=AC=20=EA=B9=A8?= =?UTF-8?q?=EC=A7=90=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/filePreview/ui/FilePreviewPage.tsx | 4 +- .../src/admin/shared/utils/studentStatus.ts | 66 +++++++++---------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx b/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx index d75fe115..9665f5d5 100644 --- a/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx +++ b/apps/graduate/src/admin/pages/filePreview/ui/FilePreviewPage.tsx @@ -92,7 +92,7 @@ export default function FilePreviewPage() { return (
- ???? ??? ? ????. + 데이터를 불러올 수 없습니다.
); @@ -156,7 +156,7 @@ export default function FilePreviewPage() {
diff --git a/apps/graduate/src/admin/shared/utils/studentStatus.ts b/apps/graduate/src/admin/shared/utils/studentStatus.ts index 41c36237..aa5111e4 100644 --- a/apps/graduate/src/admin/shared/utils/studentStatus.ts +++ b/apps/graduate/src/admin/shared/utils/studentStatus.ts @@ -3,22 +3,22 @@ import type { ThesisStatus, } from '~/admin/shared/types/studentDetail'; -const STATUS_UNKNOWN = '??'; +const STATUS_UNKNOWN = '미정'; -const STATUS_CERTIFICATE_NOT_SUBMITTED = '???-???'; -const STATUS_CERTIFICATE_SUBMITTED = '???-??'; -const STATUS_CERTIFICATE_APPROVED = '???-??'; +const STATUS_CERTIFICATE_NOT_SUBMITTED = '자격증-미제출'; +const STATUS_CERTIFICATE_SUBMITTED = '자격증-제출'; +const STATUS_CERTIFICATE_APPROVED = '자격증-승인'; -const STATUS_MID_REPORT_NOT_SUBMITTED = '?????-???'; -const STATUS_MID_REPORT_SUBMITTED = '?????-??'; +const STATUS_MID_REPORT_NOT_SUBMITTED = '중간보고서-미제출'; +const STATUS_MID_REPORT_SUBMITTED = '중간보고서-제출'; -const STATUS_FINAL_REPORT_NOT_SUBMITTED = '?????-???'; -const STATUS_FINAL_REPORT_SUBMITTED = '?????-??'; -const STATUS_FINAL_REPORT_APPROVED = '?????-??'; +const STATUS_FINAL_REPORT_NOT_SUBMITTED = '최종보고서-미제출'; +const STATUS_FINAL_REPORT_SUBMITTED = '최종보고서-제출'; +const STATUS_FINAL_REPORT_APPROVED = '최종보고서-승인'; -type ThesisStageLabel = '?????' | '?????'; -type SubmissionLabel = '??' | '???'; -type ApprovalLabel = '??' | '???'; +type ThesisStageLabel = '중간보고서' | '최종보고서'; +type SubmissionLabel = '제출' | '미제출'; +type ApprovalLabel = '승인' | '미승인'; export type ThesisStageState = { stage: ThesisStageLabel; @@ -34,9 +34,9 @@ export function getCurrentThesisStageState( ): ThesisStageState { if (!status.midThesis.approval) { return { - stage: '?????', - submissionStatus: status.midThesis.submitted ? '??' : '???', - approvalStatus: status.midThesis.approval ? '??' : '???', + stage: '중간보고서', + submissionStatus: status.midThesis.submitted ? '제출' : '미제출', + approvalStatus: status.midThesis.approval ? '승인' : '미승인', submissionId: status.midThesis.id, isSubmitted: status.midThesis.submitted, isApproved: status.midThesis.approval, @@ -44,9 +44,9 @@ export function getCurrentThesisStageState( } return { - stage: '?????', - submissionStatus: status.finalThesis.submitted ? '??' : '???', - approvalStatus: status.finalThesis.approval ? '??' : '???', + stage: '최종보고서', + submissionStatus: status.finalThesis.submitted ? '제출' : '미제출', + approvalStatus: status.finalThesis.approval ? '승인' : '미승인', submissionId: status.finalThesis.id, isSubmitted: status.finalThesis.submitted, isApproved: status.finalThesis.approval, @@ -58,9 +58,9 @@ export function getThesisDisapprovalStageState( ): ThesisStageState { if (status.finalThesis.approval) { return { - stage: '?????', - submissionStatus: '??', - approvalStatus: '??', + stage: '최종보고서', + submissionStatus: '제출', + approvalStatus: '승인', submissionId: status.finalThesis.id, isSubmitted: true, isApproved: true, @@ -69,9 +69,9 @@ export function getThesisDisapprovalStageState( if (status.midThesis.approval) { return { - stage: '?????', - submissionStatus: '??', - approvalStatus: '??', + stage: '중간보고서', + submissionStatus: '제출', + approvalStatus: '승인', submissionId: status.midThesis.id, isSubmitted: true, isApproved: true, @@ -80,9 +80,9 @@ export function getThesisDisapprovalStageState( if (status.finalThesis.submitted) { return { - stage: '?????', - submissionStatus: '??', - approvalStatus: '???', + stage: '최종보고서', + submissionStatus: '제출', + approvalStatus: '미승인', submissionId: status.finalThesis.id, isSubmitted: true, isApproved: false, @@ -91,9 +91,9 @@ export function getThesisDisapprovalStageState( if (status.midThesis.submitted) { return { - stage: '?????', - submissionStatus: '??', - approvalStatus: '???', + stage: '중간보고서', + submissionStatus: '제출', + approvalStatus: '미승인', submissionId: status.midThesis.id, isSubmitted: true, isApproved: false, @@ -101,9 +101,9 @@ export function getThesisDisapprovalStageState( } return { - stage: '?????', - submissionStatus: '???', - approvalStatus: '???', + stage: '중간보고서', + submissionStatus: '미제출', + approvalStatus: '미승인', submissionId: status.midThesis.id, isSubmitted: false, isApproved: false, From 76493e19f3bbac9efc594af654631f1ae3089bcf Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Mon, 30 Mar 2026 00:09:54 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fix(graduate)=20:=20=20lint=20,=20build?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/admin/shared/ui/Toolbar/toolbarTexts.ts | Bin 5208 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/graduate/src/admin/shared/ui/Toolbar/toolbarTexts.ts diff --git a/apps/graduate/src/admin/shared/ui/Toolbar/toolbarTexts.ts b/apps/graduate/src/admin/shared/ui/Toolbar/toolbarTexts.ts deleted file mode 100644 index 63a07af0e72e814dcbc04823b611b55d5da6865b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5208 zcmcIo&ubGw6nBOO4RvV4BV_B~T}Lu%Heyj6+1O=h;jfLDk3C~Q86VF4V~^weMo%pHqu+&x zQjr?;EcL^%D!7kLsK}iql{x0Gv-eT{_-eRgEyUViWrk8y0DOsLW>BW|mUp6~>1~h9 zh^xB?U5uDw-ou~Hhj{i8=ey;%@%;GWID};zm8r{&tFAZ*b1@NcG2Zx51iOiu*nu6y zZ^C_set`8;ntwPOVEs%d;nBCHI$eyvlUO~p5&@rwzlZtvmt*tY;aZixsy#2tq4c7j zW7o0ax;$0uq5QfEt|qFd^J+S21k@;LRL1P2F4o73_PdYIUD(3vpk79`X0L#DJuG&^ zLp@nBSF!rsdJSfmrvgZ`qKvL8Q8l8`(>p&!`~a_t)MyJeB6XB<@s6bS*{?5g+;qR{ zVKopo`4*x3_kPZj-TA{;;O_e>X05i{W8<+F@BJJxx$|p9L;FAH$nO5}z0A|8IhQ-T zDMvz_faAM;z-SiY2AqYSNVRk?pcVN#LK?n3W_LJ~_vmomzF?#EWp6Vp(OuP^<6&}Y&+A2BQbk(BH-=p#&>n%6tE$`RBo+Kp6OpDb*vq(Ssy*z?NNLWl4ova zd(?%5&SNv_8E_>2&aw67e_l{_2$^!ZUsm{||w)x#kwSq<-b3`sKP?7(jC`$lB-&|0Bhv5x zpU8}>+W0R24OPSVlwOU?ch{rvdH3q#;;!S)nDwaQZx)Zqu6E_KY3kUs)&G9~0JsVK AI{*Lx From 675bf359a9d453c9d4b060cacc268588d9479f2e Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Mon, 30 Mar 2026 14:51:29 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor(graduate):=20=EC=8A=B9=EC=9D=B8/?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EC=B7=A8=EC=86=8C=20=ED=9B=85=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=ED=91=9C=ED=98=84=EA=B3=BC=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGraduationApproval.ts | 9 ++++----- .../hooks/useGraduationDisapproval.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts index d5dfbc7e..11413efa 100644 --- a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts +++ b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationApproval.ts @@ -59,13 +59,11 @@ export function useGraduationApproval({ ) => { const approvedLines = approvedUsers.length > 0 - ? approvedUsers.map(user => '- ' + getLabel(user)) + ? approvedUsers.map(user => `- ${getLabel(user)}`) : [APPROVE_RESULT_NONE]; const notApprovedLines = notApprovedDetails.length > 0 - ? notApprovedDetails.map( - item => '- ' + item.label + ' (' + item.reason + ')', - ) + ? notApprovedDetails.map(item => `- ${item.label} (${item.reason})`) : [APPROVE_RESULT_NONE]; return [ @@ -194,7 +192,8 @@ export function useGraduationApproval({ toast.warning(APPROVE_NOTHING); } } catch { - /* ?ㅽ뙣 ?덈궡??MutationCache ?꾩뿭 ?좎뒪???ъ슜 */ + /* 실패 시 MutationCache 전역 토스트로 안내 */ + return; } }, }); diff --git a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts index 29c2dd1f..51b5af21 100644 --- a/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts +++ b/apps/graduate/src/admin/features/graduationApproval/hooks/useGraduationDisapproval.ts @@ -59,13 +59,11 @@ export function useGraduationDisapproval({ ) => { const disapprovedLines = disapprovedUsers.length > 0 - ? disapprovedUsers.map(user => '- ' + getLabel(user)) + ? disapprovedUsers.map(user => `- ${getLabel(user)}`) : [DISAPPROVE_RESULT_NONE]; const notDisapprovedLines = notDisapprovedDetails.length > 0 - ? notDisapprovedDetails.map( - item => '- ' + item.label + ' (' + item.reason + ')', - ) + ? notDisapprovedDetails.map(item => `- ${item.label} (${item.reason})`) : [DISAPPROVE_RESULT_NONE]; return [ @@ -194,10 +192,8 @@ export function useGraduationDisapproval({ } else { toast.warning(DISAPPROVE_NOTHING); } - } catch (error) { - toast.error( - error instanceof Error ? error.message : DISAPPROVE_FAILED, - ); + } catch { + /* 실패 시 MutationCache 전역 토스트로 안내 */ } }, });