From aac55ea905484c5499740b621d39cd067a95aea2 Mon Sep 17 00:00:00 2001 From: dooohun Date: Fri, 20 Sep 2024 16:18:58 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20routeParam=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dues로 전부 페이지가 이동되는 것을 routeParam 인자값으로 이동 되도록 수정 --- src/component/YearPagination/index.tsx | 7 ++++--- src/page/DuesManagement/index.tsx | 4 ++-- src/page/EditDues/index.tsx | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/component/YearPagination/index.tsx b/src/component/YearPagination/index.tsx index 56ffb04..6604c2e 100644 --- a/src/component/YearPagination/index.tsx +++ b/src/component/YearPagination/index.tsx @@ -8,9 +8,10 @@ import * as S from './style'; interface YearPaginationProps { duesYear: number; setDuesYear: React.Dispatch>; + routeParam: string; } -export default function YearPagination({ duesYear, setDuesYear }: YearPaginationProps) { +export default function YearPagination({ duesYear, setDuesYear, routeParam }: YearPaginationProps) { const navigate = useNavigate(); const currentYear = new Date().getFullYear(); const param = useQueryParam('page'); @@ -20,14 +21,14 @@ export default function YearPagination({ duesYear, setDuesYear }: YearPagination // 재학생 회비 내역이 2021년부터 시작하므로 2021년 이전으로 이동할 수 없음 const prevYear = page ? page + 1 : 2; if (prevYear <= currentYear - 2020) { - navigate(`/dues?page=${prevYear}`); + navigate(`/${routeParam}?page=${prevYear}`); setDuesYear((prev) => prev - 1); } }; const goToNextYear = () => { if (page && page > 1) { - navigate(`/dues?page=${page - 1}`); + navigate(`/${routeParam}?page=${page - 1}`); setDuesYear((prev) => prev + 1); } }; diff --git a/src/page/DuesManagement/index.tsx b/src/page/DuesManagement/index.tsx index d33426a..8f009ad 100644 --- a/src/page/DuesManagement/index.tsx +++ b/src/page/DuesManagement/index.tsx @@ -94,7 +94,7 @@ function DefaultTable() { const updatedTrack = [...prevTrack]; updatedTrack[trackIndex] = !updatedTrack[trackIndex]; setFilteredValue(allDues.dues.filter((row) => updatedTrack[tracks.map((track) => track.name).indexOf(row.track.name)] - && members?.content.some((member) => member.memberType === 'REGULAR' && member.id === row.memberId))); + && members?.content.some((member) => member.memberType === 'REGULAR' && member.id === row.memberId))); return updatedTrack; }); }; @@ -154,7 +154,7 @@ function DefaultTable() { <>
- +
- +
{(myInfo.authority === 'ADMIN' || myInfo.authority === 'MANAGER') && ( From 5e7fff6d94113f608f1235bb09e00b8e0de8907d Mon Sep 17 00:00:00 2001 From: dooohun Date: Fri, 20 Sep 2024 19:21:36 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EC=97=91=EC=85=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=BD=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 엑셀 파일을 읽는 프로미스를 리턴함 - 엑셀 파일을 읽을 수 있도록 배열 형태로 변경하는 makeWorksheetToArray 함수 추가 --- src/page/DuesSetup/hooks/useReadExcelFile.ts | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/page/DuesSetup/hooks/useReadExcelFile.ts diff --git a/src/page/DuesSetup/hooks/useReadExcelFile.ts b/src/page/DuesSetup/hooks/useReadExcelFile.ts new file mode 100644 index 0000000..859df4b --- /dev/null +++ b/src/page/DuesSetup/hooks/useReadExcelFile.ts @@ -0,0 +1,43 @@ +import * as Excel from 'exceljs'; + +function makeWorksheetToArray(worksheet: Excel.Worksheet): Excel.CellValue[][] { + const result: Excel.CellValue[][] = []; + worksheet.eachRow((row, rowNumber) => { + result.push([]); + row.eachCell((cell) => { + if (cell.value !== null && cell.value !== undefined) { + result[rowNumber - 1].push(cell.value); + } + }); + }); + return result; +} + +export function useReadExcelFile(excelFileRef: React.RefObject): () => Promise { + const readFile: () => Promise = async () => { + const workbook = new Excel.Workbook(); + const file = excelFileRef.current?.files?.[0]; + if (!file) return null; + + const reader = new FileReader(); + return new Promise((resolve, reject) => { + reader.onload = async (e) => { + try { + const data = new Uint8Array(e.target?.result as ArrayBuffer); + await workbook.xlsx.load(data); + const worksheet = workbook.getWorksheet(1); + if (!worksheet) { + reject(new Error('worksheet is not found')); + } + resolve(worksheet ? makeWorksheetToArray(worksheet) : null); + } catch (error) { + reject(error); + } + }; + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(file); + }); + }; + + return readFile; +} From 68190220ee233dc115fcbfba8c784b96321eb702 Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 21:41:16 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20members=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=97=90=20isFeeExempt=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/member.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/model/member.ts b/src/model/member.ts index b9ab472..2d2e5c0 100644 --- a/src/model/member.ts +++ b/src/model/member.ts @@ -42,6 +42,7 @@ export interface Member { updatedAt: string; isAuthed: boolean; isDeleted: boolean; + isFeeExempt: boolean; deleteReason: string; birthday: string; } From 08a8277db1921953c88a770f73f0f5c94178c0c0 Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 21:41:39 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=EC=9D=98=20?= =?UTF-8?q?=ED=9A=8C=EB=B9=84=20=EC=A0=95=EB=A1=9C=EB=A5=BC=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DuesSetup/hooks/findMemberDuesInfo.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/page/DuesSetup/hooks/findMemberDuesInfo.ts diff --git a/src/page/DuesSetup/hooks/findMemberDuesInfo.ts b/src/page/DuesSetup/hooks/findMemberDuesInfo.ts new file mode 100644 index 0000000..c46866e --- /dev/null +++ b/src/page/DuesSetup/hooks/findMemberDuesInfo.ts @@ -0,0 +1,50 @@ +import * as Excel from 'exceljs'; +import { DuesInfo } from 'model/dues/allDues'; +import { Pagination } from 'model/page'; +import { Member } from 'model/member'; + +const NAME_ROW_NUMBER = 4; + +interface FindMemberDuesInfoProps { + worksheet: Excel.CellValue[][]; + members: Pagination; + prevYearDues: DuesInfo; + currentYearDues: DuesInfo; +} + +export type MemberDuesInfo = { id: number, name: string, notPaidMonthInfo: { year: number, month: number }[] }; + +function findUnpaidMonths(duesInfo: DuesInfo | undefined, memberId: number, year: number) { + if (!duesInfo) return []; + const memberDues = duesInfo.dues.find((dues) => dues.memberId === memberId); + return memberDues && memberDues.unpaidCount > 0 + ? memberDues.detail.filter((detail) => detail.status === 'NOT_PAID').map((detail) => ({ year, month: detail.month })) + : []; +} + +export function findMemberDuesInfo({ + worksheet, members, prevYearDues, currentYearDues, +}: FindMemberDuesInfoProps) { + const result: MemberDuesInfo[] = []; + const currentYear = new Date().getFullYear(); + + worksheet.forEach((row) => { + const memberName = row[NAME_ROW_NUMBER] as string; + const memberInfo = members?.content.find((member) => member.name === memberName); + + if (memberInfo) { + const unpaidMonthsPrevYear = findUnpaidMonths(prevYearDues, memberInfo.id, currentYear - 1); + const unpaidMonthsCurrentYear = findUnpaidMonths(currentYearDues, memberInfo.id, currentYear); + + if (unpaidMonthsPrevYear.length > 0 || unpaidMonthsCurrentYear.length > 0) { + result.push({ + id: memberInfo.id, + name: memberName, + notPaidMonthInfo: [...unpaidMonthsPrevYear, ...unpaidMonthsCurrentYear], + }); + } + } + }); + + return result; +} From 52c56091ef79eb304584188028c918f0e2eefa92 Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 21:42:44 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=ED=9A=8C=EB=B9=84=EB=A5=BC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회비 업데이트는 다음 4단계를 통해 업데이트 된다. - 미납된 회비를 납부한 경우, 미납된 회비를 납부 처리한다. - 미납된 회비가 없는 경우, 납부한 달의 회비를 납부 처리한다. - 면제 인원의 경우 면제 처리한다. - 납부한 회비가 없는 경우 미납 처리한다. --- src/page/DuesSetup/hooks/updateDues.ts | 130 +++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/page/DuesSetup/hooks/updateDues.ts diff --git a/src/page/DuesSetup/hooks/updateDues.ts b/src/page/DuesSetup/hooks/updateDues.ts new file mode 100644 index 0000000..e24f2ea --- /dev/null +++ b/src/page/DuesSetup/hooks/updateDues.ts @@ -0,0 +1,130 @@ +import * as Excel from 'exceljs'; +import { DuesInfo } from 'model/dues/allDues'; +import { Pagination } from 'model/page'; +import { Member } from 'model/member'; +import { UseMutationResult } from '@tanstack/react-query'; +import { NewDuesData } from 'api/dues'; +import { MemberDuesInfo } from './findMemberDuesInfo'; + +interface UpdateDuesProps { + worksheet: Excel.CellValue[][]; + members: Pagination; + unpaidMemberDuesInfo: MemberDuesInfo[]; + currentYearDues: DuesInfo; + putDuesMutation: UseMutationResult; + postDuesMutation: UseMutationResult; +} + +type UpdateNotPaidDuesToPaidProps = Pick & { + name: string; + depositDues: number; +}; + +type UpdateNullDuesToPaidProps = Pick & { + id: number; + name: string; + depositDues: number; +}; + +type UpdateWavierDuesProps = Pick; +type UpdateNullToNotPaidDuesProps = Pick; + +// depositDues만큼 unpaidMemberDuesInfo를 반영하여 mutate하기 +function updateNotPaidDuesToPaid({ + name, depositDues, unpaidMemberDuesInfo, putDuesMutation, +}: UpdateNotPaidDuesToPaidProps) { + let leftDepositDues = depositDues; + if (depositDues % 10000 === 0) { + Array.from({ length: depositDues }).forEach((_, index) => { + // TODO: 동명이인 처리 + const memberDuesInfo = unpaidMemberDuesInfo.find((info) => info.name === name); + if (memberDuesInfo) { + const { id, notPaidMonthInfo } = memberDuesInfo; + const { year, month } = notPaidMonthInfo[index]; + putDuesMutation.mutate({ + memberId: id, + year, + month, + status: 'PAID', + }); + leftDepositDues -= 10000; + } + }); + } + return leftDepositDues; +} + +function updateNullDuesToPaid({ id, depositDues, postDuesMutation }: UpdateNullDuesToPaidProps) { + // 회비를 매 달 1일에 정리하기 때문에 저번 달을 기준으로 처리한다. + const currentYear = new Date().getFullYear(); + const prevMonth = new Date().getMonth(); + Array.from({ length: depositDues }).forEach((_, index) => { + const year = prevMonth + index > 12 ? currentYear + 1 : currentYear; + const month = ((prevMonth + index) % 12) + 1; + postDuesMutation.mutate({ + memberId: id, + year, + month, + status: 'PAID', + }); + }); +} + +function updateWavierDues({ postDuesMutation, members }: UpdateWavierDuesProps) { + members.content.forEach((member) => { + if (member.isFeeExempted) { + postDuesMutation.mutate({ + memberId: member.id, + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + status: 'SKIP', + }); + } + }); +} + +function updateNullToNotPaidDues({ currentYearDues, postDuesMutation }: UpdateNullToNotPaidDuesProps) { + currentYearDues.dues.forEach((dues) => { + const prevMonth = new Date().getMonth(); + if (dues.detail[prevMonth].status === null) { + postDuesMutation.mutate({ + memberId: dues.memberId, + year: new Date().getFullYear(), + month: prevMonth + 1, + status: 'NOT_PAID', + }); + } + }); +} + +export function updateDues({ + worksheet, members, unpaidMemberDuesInfo, currentYearDues, putDuesMutation, postDuesMutation, +}: UpdateDuesProps) { + worksheet.forEach((row) => { + const [, , depositValue, , content, , note] = row; + const depositDues = Number(depositValue); + const name = String(content); + const id = members.content.find((member) => member.name === name)?.id; + let leftDepositDues; + + if (name && depositDues && id) { + // type note = 'X' | '-' | `id=${id}`; + if (note !== 'X') { + /** + * 1. 미납된 회비를 납부한 경우, 미납된 회비를 납부 처리한다. + * 2. 미납된 회비가 없는 경우, 납부한 달의 회비를 납부 처리한다. + * 3. 면제 인원의 경우 면제 처리한다. + * 4. 납부한 회비가 없는 경우 미납 처리한다. (1~3번 까지 처리가 끝난 후 아무값도 없는 경우 NOT_PAID 처리) + */ + leftDepositDues = updateNotPaidDuesToPaid({ + name, depositDues, unpaidMemberDuesInfo, putDuesMutation, + }); + updateNullDuesToPaid({ + id, name, depositDues: leftDepositDues, postDuesMutation, + }); + updateWavierDues({ members, postDuesMutation }); + updateNullToNotPaidDues({ postDuesMutation, currentYearDues }); + } + } + }); +} From 3c339ce92021abff0f5a4f05d1489e2fe40f5adb Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 21:43:15 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EC=97=91=EC=85=80=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=EC=8B=9C=ED=8A=B8=EC=97=90=20=EB=AF=B8=EB=82=A9=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/updateWorksheetwithDuesInfo.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/page/DuesSetup/hooks/updateWorksheetwithDuesInfo.ts diff --git a/src/page/DuesSetup/hooks/updateWorksheetwithDuesInfo.ts b/src/page/DuesSetup/hooks/updateWorksheetwithDuesInfo.ts new file mode 100644 index 0000000..4cc9329 --- /dev/null +++ b/src/page/DuesSetup/hooks/updateWorksheetwithDuesInfo.ts @@ -0,0 +1,29 @@ +import * as Excel from 'exceljs'; +import { MemberDuesInfo } from './findMemberDuesInfo'; + +const NAME_ROW_NUMBER = 4; +const UNPAID_INFO_ROW_NUMBER = 7; + +export function updateWorksheetWithDuesInfo( + worksheet: Excel.CellValue[][], + memberDuesInfo: MemberDuesInfo[], +) { + const updatedWorksheet = worksheet.filter((_, index) => index > 0); + return updatedWorksheet.map((row) => { + const memberName = row[NAME_ROW_NUMBER] as string; + const memberInfo = memberDuesInfo.find((info) => info.name === memberName); + const updatedRow = [...row]; + + if (memberInfo) { + const unpaidMonths = memberInfo.notPaidMonthInfo + .map(({ year, month }) => `${year}년 ${month}월`) + .join(', '); + + updatedRow[UNPAID_INFO_ROW_NUMBER] = unpaidMonths; + } else { + updatedRow[UNPAID_INFO_ROW_NUMBER] = ''; + } + + return updatedRow; + }); +} From 918e9e83f27b6948218308945730413792c9b9ea Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 21:44:04 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 엑셀 읽기 - 엑셀 읽은 후 엑셀 정보를 화면에 출력 - 정보에 맞게 회비 업데이트하기 --- src/page/DuesSetup/index.tsx | 490 +++-------------------------------- 1 file changed, 42 insertions(+), 448 deletions(-) diff --git a/src/page/DuesSetup/index.tsx b/src/page/DuesSetup/index.tsx index bebd5b5..6b0d27e 100644 --- a/src/page/DuesSetup/index.tsx +++ b/src/page/DuesSetup/index.tsx @@ -1,426 +1,60 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { - Button, ButtonGroup, Popover, Table, TableBody, TableCell, TableHead, TableRow, + Button, ButtonGroup, Table, TableBody, TableCell, TableHead, TableRow, } from '@mui/material'; import { - Suspense, useRef, useState, useEffect, + Suspense, useRef, useState, } from 'react'; +import LoadingSpinner from 'layout/LoadingSpinner'; import * as Excel from 'exceljs'; -import { Dues } from 'model/dues/allDues'; -import { useMutation } from '@tanstack/react-query'; -import { - NewDuesData, postDues, putDues, -} from 'api/dues'; import { useGetMe, useGetMembers } from 'query/members'; -import { useGetAllDues } from 'query/dues'; -import { useSnackBar } from 'ts/useSnackBar'; -import LoadingSpinner from 'layout/LoadingSpinner'; -import { ArrowDownward, ArrowUpward, Sort } from '@mui/icons-material'; +import { useGetAllDues, usePostDues, usePutDues } from 'query/dues'; +import { useReadExcelFile } from './hooks/useReadExcelFile'; import * as S from './style'; - -interface TableBodyData { - value: string[]; -} - -interface DatesDuesApply { - prevYearMonth: number[]; - currentYearMonth: number[]; - nextYearMonth: number[]; -} - -type Column = 'date' | 'category' | 'amount' | 'balance' | 'name' | 'note' | 'month'; -// 회비 생성 -// 매월 1일에 회비 생성 (단 한번만 하는 기능임) +import { MemberDuesInfo, findMemberDuesInfo } from './hooks/findMemberDuesInfo'; +import { updateWorksheetWithDuesInfo } from './hooks/updateWorksheetwithDuesInfo'; +import { updateDues } from './hooks/updateDues'; function DefaultTable() { const currentYear = new Date().getFullYear(); - const prevMonth = new Date().getMonth(); const excelFileRef = useRef(null); - const workbook = new Excel.Workbook(); - const tableHead = ['거래 일자', '구분', '거래 금액', '거래 후 잔액', '이름', '비고', '회비가 적용되는 날짜']; - const [tableBody, setTableBody] = useState([ - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - ]); - const [datesDuesApply, setDatesDuesApply] = useState([{ prevYearMonth: [], currentYearMonth: [], nextYearMonth: [] }]); + const tableHead = ['No', '거래 일시', '입금액', '출금액', '이름', '잔액', '비고', '현재 미납한 날짜']; + const [tableBody, setTableBody] = useState(Array.from({ length: tableHead.length }).map(() => [])); const [buttonDisabled, setButtonDisabled] = useState(true); - const [anchorEl, setAnchorEl] = useState(null); - const isSortPopoverOpen = Boolean(anchorEl); - - const handlePopoverClick = (e: React.MouseEvent) => { - setAnchorEl(e.currentTarget); - }; + let unpaidMemberDuesInfo: MemberDuesInfo[] = []; + const { data: getMe } = useGetMe(); const { data: members } = useGetMembers({ pageIndex: 0, pageSize: 1000, trackId: null }); - const { data: me } = useGetMe(); - const { data: currentYearDues } = useGetAllDues({ year: currentYear }); const { data: prevYearDues } = useGetAllDues({ year: currentYear - 1 }); - const { data: nextYearDues } = useGetAllDues({ year: currentYear + 1 }); - - const openSnackBar = useSnackBar(); - - const onMutationSuccess = () => { - openSnackBar({ type: 'success', message: '회비가 생성되었습니다.' }); - setButtonDisabled(true); - }; - - const postDuesMutation = useMutation({ - mutationKey: ['postDues'], - mutationFn: (data: NewDuesData) => { - if (me.authority === 'ADMIN') { - return postDues(data); - } - throw new Error('권한이 없습니다.'); - }, - onError: (error) => openSnackBar({ type: 'error', message: error.message }), - onSuccess: () => onMutationSuccess(), - }); - - const putDuesMutation = useMutation({ - mutationKey: ['putDues'], - mutationFn: (data: NewDuesData) => { - if (me.authority === 'ADMIN') { - return putDues(data); - } - throw new Error('권한이 없습니다.'); - }, - onError: (error) => openSnackBar({ type: 'error', message: error.message }), - onSuccess: () => onMutationSuccess(), - }); - - const columns: Column[] = ['date', 'category', 'amount', 'balance', 'name', 'note', 'month']; - const sortInAscendingOrderByName = () => { - const rowData = tableBody[4].value.map((_, index) => { - const newRowData: Record = { - date: '', - category: '', - amount: '', - balance: '', - name: '', - note: '', - month: '', - }; - columns.forEach((col, colIndex) => { - newRowData[col] = tableBody[colIndex].value[index]; - }); - return newRowData; - }); - rowData.sort((a, b) => a.name.localeCompare(b.name)); - rowData.forEach((value, index) => { - setTableBody((prev) => { - const newTableBody = [...prev]; - columns.forEach((col, colIndex) => { - newTableBody[colIndex].value[index] = value[col]; - }); - return newTableBody; - }); - }); - setAnchorEl(null); - }; - - const sortInDescendingOrderByName = () => { - const rowData = tableBody[4].value.map((_, index) => { - const newRowData: Record = { - date: '', - category: '', - amount: '', - balance: '', - name: '', - note: '', - month: '', - }; - columns.forEach((col, colIndex) => { - newRowData[col] = tableBody[colIndex].value[index]; - }); - return newRowData; - }); - rowData.sort((a, b) => b.name.localeCompare(a.name)); - rowData.forEach((value, index) => { - setTableBody((prev) => { - const newTableBody = [...prev]; - columns.forEach((col, colIndex) => { - newTableBody[colIndex].value[index] = value[col]; - }); - return newTableBody; - }); - }); - setAnchorEl(null); - }; - - const findUnpaidMonth = (dues: Dues[], name: string) => { - const unpaidPeople = dues.filter((value) => name !== '' && value.name === name && value.unpaidCount > 0); - return (unpaidPeople.map((value) => value.detail.filter((detail) => detail.status === 'NOT_PAID'))).map((value) => value.map((detail) => detail.month)); - }; - - const findStatus = (memberId: number, month: number, dues: Dues[]) => { - const memberDuesInfo = dues.filter((value) => value.memberId === memberId)[0]; - return memberDuesInfo?.detail[month - 1].status; - }; - - const findNullStatusMonth = (memberId: number, dues: Dues[]) => { - const memberDuesInfo = dues.filter((value) => value.memberId === memberId)[0]; - const result = Array.from({ length: 12 }).map((_, index) => { - if (memberDuesInfo.detail[index].status === null) { - return index + 1; - } - return null; - }); - return result.filter((value): value is number => value !== null); - }; - - const findMemberId = (name: string, index: number) => { - // 동명이인인 경우 비고에 memberId가 적혀있음(수기로 반드시 작성해야 함) 예시) memberId: 12 - const memberIdInNotes = tableBody[tableHead.indexOf('비고')].value[index]?.includes('memberId:'); - if (memberIdInNotes) { - const memberId = tableBody[tableHead.indexOf('비고')].value[index].split('memberId:')[1]; - return Number(memberId); - } - - const member = members?.content.find((value) => value.name === name); - if (member) { - return member.id; - } - - return null; - }; - - // 회비가 적용될 달을 찾아서 테이블에 추가 - const findDuesMonths = () => { - tableBody[4].value.forEach((name, index) => { - const prevResult: number[] = []; - const currentResult: number[] = []; - const nextResult: number[] = []; - const memberId = findMemberId(name, index); - const transactionAmount = Number(tableBody[2].value[index].replace(/,/g, '')); - const count = transactionAmount / 10000; - const unpaidMonthsInPrevYear = findUnpaidMonth(prevYearDues.dues, name)?.[0]; - const unpaidMonthsInCurrentYear = findUnpaidMonth(currentYearDues.dues, name)?.[0]; - // 작년에 미납된 회비가 있을 경우 - if (unpaidMonthsInPrevYear && count > 0) { - const updatedMonths = unpaidMonthsInPrevYear.slice(0, count); - prevResult.push(...updatedMonths); - } - // 작년에 미납된 회비가 null일 경우 - if (prevResult.length < count && prevMonth === 12) { - const inAdvanceMonths = Array.from({ length: count - prevResult.length }).map((_, monthIndex) => { - if (memberId) { - const prevYearDuesStatus = findStatus(memberId, prevMonth, prevYearDues.dues); - if (prevYearDuesStatus === null) { - if (prevMonth + 1 + monthIndex > 12) { - return null; - } - return prevMonth + 1 + monthIndex; - } - } - return null; - }); - prevResult.push(...inAdvanceMonths.filter((value): value is number => value !== null)); - } - // 이번 해에 미납된 회비가 있을 경우 - if (unpaidMonthsInCurrentYear && prevResult.length < count) { - const updatedMonths = unpaidMonthsInCurrentYear.slice(0, count - prevResult.length); - currentResult.push(...updatedMonths); - } - // 이번 해에 미리 납부할 회비가 있을 경우 - if (currentResult.length < count) { - const inAdvanceMonths = Array.from({ length: count - prevResult.length - currentResult.length }).map((_, monthIndex) => { - if (memberId) { - // 반복해서 null인 값을 찾아야 함 - const nullStatusMonths = findNullStatusMonth(memberId, currentYearDues.dues); - return nullStatusMonths[monthIndex]; - } - return null; - }); - currentResult.push(...inAdvanceMonths.filter((value): value is number => value !== null && value !== undefined)); - } - - // 다음 해에 미리 납부할 회비가 있을 경우 - if (currentResult.length < count) { - const inAdvanceMonths = Array.from({ length: count - prevResult.length - currentResult.length }).map((_, monthIndex) => { - if (memberId) { - const nullStatusMonths = findNullStatusMonth(memberId, nextYearDues.dues); - return nullStatusMonths[monthIndex]; - } - return null; - }); - nextResult.push(...inAdvanceMonths.filter((value): value is number => value !== null)); - } - const yearInfo: string[] = []; + const { data: currentYearDues } = useGetAllDues({ year: currentYear }); - if (prevResult.length > 0) { - yearInfo.push(`${currentYear - 1}년 ${prevResult.join('월, ')}월`); - } - if (currentResult.length > 0) { - yearInfo.push(`${currentYear}년 ${currentResult.join('월, ')}월`); - } - if (nextResult.length > 0) { - yearInfo.push(`${currentYear + 1}년 ${nextResult.join('월, ')}월`); - } + const putDuesMutation = usePutDues(); + const postDuesMutation = usePostDues(); - if (yearInfo.length > 0) { - setTableBody((prev) => { - const newTableBody = [...prev]; - newTableBody[6].value[index] = yearInfo.join(' / '); - return newTableBody; - }); - setDatesDuesApply((prev) => { - const newDatesDuesApply = [...prev]; - newDatesDuesApply[index] = { prevYearMonth: prevResult, currentYearMonth: currentResult, nextYearMonth: nextResult }; - return newDatesDuesApply; - }); - } - }); - }; + const readExcelFile = useReadExcelFile(excelFileRef); - const handleExcelFileChange = async () => { - setTableBody([ - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - { value: [] }, - ]); - const file = excelFileRef.current?.files?.[0]; - const reader = new FileReader(); - if (file) { + const handleFileUpload = async () => { + try { + const worksheet = await readExcelFile(); + if (worksheet === null) return; setButtonDisabled(false); - reader.readAsArrayBuffer(file); - reader.onload = async (e) => { - const buffer = e.target?.result; - const data = new Uint8Array(buffer as ArrayBuffer); - await workbook.xlsx.load(data); - const worksheet = workbook.getWorksheet(1); - - worksheet?.eachRow((row, rowNumber) => { - const totalRowNumber = worksheet.actualRowCount; - row.eachCell((cell, cellNumber) => { - if (cell.value !== null && rowNumber > 4 && rowNumber < totalRowNumber) { - if (cellNumber !== 6) { - setTableBody((prev) => { - const newTableBody = [...prev]; - newTableBody[cellNumber - 1].value.push(cell.text); - return newTableBody; - }); - } else { - setTableBody((prev) => { - const newTableBody = [...prev]; - newTableBody[cellNumber - 1].value[rowNumber - 5] = cell.text; - return newTableBody; - }); - } - } - }); - }); - }; + unpaidMemberDuesInfo = findMemberDuesInfo({ + worksheet, members, prevYearDues, currentYearDues, + }); + setTableBody(updateWorksheetWithDuesInfo(worksheet, unpaidMemberDuesInfo)); + } catch (error) { + console.error(error); } }; - // status가 NOT_PAID인 월을 찾아서 PAID로 변경 (PUT /dues) - // status가 없는 경우 PAID로 변경 (POST /dues) - const updateUnpaidtoPaid = (memberId: number, year: number, months: number[], dues: Dues[]) => { - months.forEach((month) => { - const monthStatus = findStatus(memberId, month, dues); - if (monthStatus === 'NOT_PAID') { - const data: NewDuesData = { - memberId, - year, - month, - status: 'PAID', - }; - putDuesMutation.mutate(data); - } else if (monthStatus === null) { - const data: NewDuesData = { - memberId, - year, - month, - status: 'PAID', - }; - postDuesMutation.mutate(data); - } - }); - }; - - // 회비 면제인 경우 null -> SKIP (POST /dues) - // authority가 manger, admin인 경우 적용 - const applyForDuesWaiver = () => { - const waiverMember = members?.content.filter((value) => value.authority === 'MANAGER' || value.authority === 'ADMIN'); - if (waiverMember) { - const waiverMembersData: NewDuesData[] = waiverMember.map((value) => { - return { - memberId: value.id, - year: prevMonth === 12 ? currentYear - 1 : currentYear, - month: prevMonth, - status: 'SKIP', - }; - }); - waiverMembersData.forEach((data, index) => { - const memberId = waiverMember[index].id; - // status null인 경우에만 면제 적용됨 - if (currentYearDues.dues.find((value) => value.memberId === memberId)?.detail[prevMonth - 1].status === null) { - postDuesMutation.mutate(data); - } + const handleCreateDues = () => { + if (getMe.authority === 'MANAGER') { + updateDues({ + worksheet: tableBody, members, unpaidMemberDuesInfo, currentYearDues, putDuesMutation, postDuesMutation, }); } }; - // status가 null인 prevMonth를 찾아서 NOT_PAID로 변경 (POST /dues) - // 오직 regular user만 적용 - const updateNullToNotPaid = () => { - const memberIds = members?.content.map((value) => value.id); - memberIds?.forEach((memberId) => { - if (members?.content.find((value) => value.id === memberId)?.memberType === 'REGULAR') { - const prevMonthStatus = findStatus(memberId, prevMonth, prevMonth === 12 ? prevYearDues.dues : currentYearDues.dues); - if (prevMonthStatus === null) { - const data: NewDuesData = { - memberId, - year: prevMonth === 12 ? currentYear - 1 : currentYear, - month: prevMonth, - status: 'NOT_PAID', - }; - postDuesMutation.mutate(data); - } - } - }); - }; - - const handleCreateDuesClick = () => { - applyForDuesWaiver(); - tableBody[4].value.forEach((name, index) => { - const memberId = findMemberId(name, index); - const prevYearDuesApplyMonth = datesDuesApply[index]?.prevYearMonth; - const currentYearDuesApplyMonth = datesDuesApply[index]?.currentYearMonth; - const nextYearDuesApplyMonth = datesDuesApply[index]?.nextYearMonth; - if (memberId) { - if (prevYearDuesApplyMonth) { - updateUnpaidtoPaid(memberId, currentYear - 1, prevYearDuesApplyMonth, prevYearDues.dues); - } - if (currentYearDuesApplyMonth) { - updateUnpaidtoPaid(memberId, currentYear, currentYearDuesApplyMonth, currentYearDues.dues); - } - if (nextYearDuesApplyMonth) { - updateUnpaidtoPaid(memberId, currentYear + 1, nextYearDuesApplyMonth, nextYearDues.dues); - } - } - }); - updateNullToNotPaid(); - }; - const mix = async () => { - handleExcelFileChange(); - }; - - useEffect(() => { - if (tableBody[4].value.length > 0) { - findDuesMonths(); - } - }, [tableBody]); - return (
@@ -433,70 +67,30 @@ function DefaultTable() { accept=".xlsx" css={S.fileUpload} ref={excelFileRef} - onChange={mix} + onChange={handleFileUpload} /> - + - {tableHead.map((head) => { - if (head === '이름') { - return ( - - {head} - - - ); - } - return ( - {head} - ); - })} + {tableHead.map((head) => ( + {head} + ))} - {tableBody[4].value.map((date, index) => { - return ( - - {tableBody.map((dues) => { - return ( - {dues.value[index]} - ); - })} - - ); - })} + {tableBody.map((row, rowIndex) => ( + + {row.map((cell) => ( + {cell as string | number} + ))} + + ))}
- setAnchorEl(null)} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - > -
-

- 이름순 정렬 -

-
- - -
-
-
); } From 75a6cfa4b26ba09014b33cdfac051d4ede575ed5 Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 22:04:04 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=EC=8B=9C=20?= =?UTF-8?q?=EC=8A=A4=EB=82=B5=EB=B0=94=EB=A5=BC=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lint 에러 수정 --- src/page/DuesSetup/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/page/DuesSetup/index.tsx b/src/page/DuesSetup/index.tsx index 6b0d27e..6ea500f 100644 --- a/src/page/DuesSetup/index.tsx +++ b/src/page/DuesSetup/index.tsx @@ -9,6 +9,7 @@ import LoadingSpinner from 'layout/LoadingSpinner'; import * as Excel from 'exceljs'; import { useGetMe, useGetMembers } from 'query/members'; import { useGetAllDues, usePostDues, usePutDues } from 'query/dues'; +import { useSnackBar } from 'ts/useSnackBar'; import { useReadExcelFile } from './hooks/useReadExcelFile'; import * as S from './style'; import { MemberDuesInfo, findMemberDuesInfo } from './hooks/findMemberDuesInfo'; @@ -30,6 +31,7 @@ function DefaultTable() { const putDuesMutation = usePutDues(); const postDuesMutation = usePostDues(); + const openSnackBar = useSnackBar(); const readExcelFile = useReadExcelFile(excelFileRef); @@ -43,7 +45,9 @@ function DefaultTable() { }); setTableBody(updateWorksheetWithDuesInfo(worksheet, unpaidMemberDuesInfo)); } catch (error) { - console.error(error); + if (error instanceof Error) { + openSnackBar({ type: 'error', message: error.message }); + } } }; From 6fd7244c9c826110ea5d7858292e14bc87494126 Mon Sep 17 00:00:00 2001 From: dooohun Date: Tue, 1 Oct 2024 22:10:08 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20YearPagination=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B9=BC=EB=A8=B9=EC=9D=80=20routeParam=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/PersonalDues/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/page/PersonalDues/index.tsx b/src/page/PersonalDues/index.tsx index 1c45b4a..08af709 100644 --- a/src/page/PersonalDues/index.tsx +++ b/src/page/PersonalDues/index.tsx @@ -27,7 +27,7 @@ export default function PersonalDues() { return (
- +