diff --git a/src/api/admin/cardinal/postCardinal.ts b/src/api/admin/cardinal/postCardinal.ts index 13f00869..93407846 100644 --- a/src/api/admin/cardinal/postCardinal.ts +++ b/src/api/admin/cardinal/postCardinal.ts @@ -3,17 +3,19 @@ import axios from 'axios'; const BASE_URL = import.meta.env.VITE_API_URL; const PATH = '/api/v1/admin/cardinals'; +// 새로운 기수 등록 const postCardinalApi = async ( cardinalNumber: number, year: number, semester: number, + inProgress: boolean, ) => { const accessToken = localStorage.getItem('accessToken'); try { const response = await axios.post( `${BASE_URL}${PATH}`, - { cardinalNumber, year, semester }, + { cardinalNumber, year, semester, inProgress }, { headers: { Authorization: `Bearer ${accessToken}`, @@ -27,4 +29,30 @@ const postCardinalApi = async ( } }; -export default postCardinalApi; +// 기수 수정 +const patchCardinalApi = async ( + id: number, + year: number, + semester: number, + inProgress: boolean, +) => { + const accessToken = localStorage.getItem('accessToken'); + + try { + const response = await axios.patch( + `${BASE_URL}${PATH}`, + { id, year, semester, inProgress }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + return response.data; + } catch (error: any) { + throw new Error(error.response?.data?.message || '기수 수정 실패'); + } +}; + +export { patchCardinalApi, postCardinalApi }; diff --git a/src/api/useGetCardinals.tsx b/src/api/useGetCardinals.tsx index b82c8ff8..e856003a 100644 --- a/src/api/useGetCardinals.tsx +++ b/src/api/useGetCardinals.tsx @@ -17,7 +17,11 @@ export const getAllCardinals = async () => { export const useGetAllCardinals = () => { const [allCardinals, setAllCardinals] = useState< - { id: number; cardinalNumber: number }[] + { + status: string; // 기수 상태값 추가 ( IN_PROGRESS 이면 현재 기수 ) + id: number; + cardinalNumber: number; + }[] >([]); const [error, setError] = useState(null); diff --git a/src/components/Admin/Box.tsx b/src/components/Admin/Box.tsx index 5def1801..30d94f02 100644 --- a/src/components/Admin/Box.tsx +++ b/src/components/Admin/Box.tsx @@ -9,40 +9,70 @@ export interface BoxProps { lastColor?: string; minWidth?: string; isCardinalBox?: boolean; + onClick?: () => void; + isClick?: boolean; + isSelected?: boolean; + isIncomplete?: boolean; } export const Wrapper = styled.div<{ color: string; isCardinalBox: boolean; + isClick?: boolean; + isSelected?: boolean; + isIncomplete?: boolean; }>` width: ${({ isCardinalBox }) => (isCardinalBox ? 'none' : '234px')}; min-width: ${({ isCardinalBox }) => (isCardinalBox ? '234px' : 'none')}; height: 164px; - background-color: ${(props) => props.color}; + background-color: ${({ isIncomplete, isSelected, color }) => { + if (isIncomplete) return 'transparent'; + if (isSelected) return theme.color.gray[18]; + return color; + }}; + border: ${({ isIncomplete }) => + isIncomplete ? `1.5px dashed ${theme.color.gray[18]}` : 'none'}; display: flex; flex-direction: column; justify-content: space-between; padding: 16px; box-sizing: border-box; + cursor: ${({ isClick }) => (isClick ? 'pointer' : 'auto')}; + + ${({ isClick, isSelected, isIncomplete }) => + isClick && + !isSelected && + !isIncomplete && + ` + &:hover { + background-color: ${theme.color.gray[18]}; + } + `} `; -export const Title = styled.div<{ isHidden?: boolean }>` +export const Title = styled.div<{ + isHidden?: boolean; + isIncomplete?: boolean; +}>` font-size: 18px; font-family: ${theme.font.regular}; - color: ${theme.color.gray[100]}; + color: ${({ isIncomplete }) => + isIncomplete ? theme.color.gray[18] : theme.color.gray[100]}; `; -export const Description = styled.div` +export const Description = styled.div<{ isIncomplete?: boolean }>` font-size: 24px; font-family: ${theme.font.semiBold}; - color: ${theme.color.gray[100]}; + color: ${({ isIncomplete }) => + isIncomplete ? theme.color.gray[18] : theme.color.gray[100]}; margin-top: 20px; `; -export const Last = styled.div<{ lastColor?: string }>` +export const Last = styled.div<{ lastColor?: string; isIncomplete?: boolean }>` font-size: 18px; font-family: ${theme.font.regular}; - color: ${({ lastColor }) => lastColor || '#979797'}; + color: ${({ isIncomplete, lastColor }) => + isIncomplete ? '#909393' : lastColor || '#979797'}; `; const Box: React.FC = ({ @@ -52,12 +82,25 @@ const Box: React.FC = ({ color, lastColor, isCardinalBox = false, + onClick, + isClick = false, + isSelected = false, + isIncomplete = false, }) => { return ( - - {title && {title}} - {description} - {last} + + {title && {title}} + {description} + + {last} + ); }; diff --git a/src/components/Admin/ButtonGroup.tsx b/src/components/Admin/ButtonGroup.tsx index 63f3a6f3..1092a567 100644 --- a/src/components/Admin/ButtonGroup.tsx +++ b/src/components/Admin/ButtonGroup.tsx @@ -18,10 +18,9 @@ const ButtonGroupContainer = styled.div<{ hasEndGap?: boolean }>` justify-content: center; align-items: center; gap: 12px; - padding-right: 20px; + padding: 10px; overflow-x: auto; white-space: nowrap; - padding-left: 10px; &::-webkit-scrollbar { height: 3px; } @@ -30,7 +29,7 @@ const ButtonGroupContainer = styled.div<{ hasEndGap?: boolean }>` hasEndGap && ` & > :last-child { - margin-left:150px + margin-left:188px } @media (max-width: 900px) { diff --git a/src/components/Admin/CardinalInfo.tsx b/src/components/Admin/CardinalInfo.tsx index 222203e0..467f81e3 100644 --- a/src/components/Admin/CardinalInfo.tsx +++ b/src/components/Admin/CardinalInfo.tsx @@ -1,15 +1,30 @@ import theme from '@/styles/theme'; +import Box from '@/components/Admin/Box'; import * as S from '@/styles/admin/cardinal/CardinalInfo.styled'; import AddCardinal from '@/components/Admin/AddCardinal'; import useGetAllCardinals from '@/api/useGetCardinals'; import { useEffect, useState } from 'react'; import { useGetAdminUsers } from '@/api/admin/member/getAdminUser'; +import { useMemberContext } from '@/components/Admin/context/MemberContext'; +import CardinalModal from '@/components/Admin/Modal/CardinalModal'; const CardinalInfo: React.FC = () => { + const { selectedCardinal, setSelectedCardinal } = useMemberContext(); const { allCardinals } = useGetAllCardinals(); const { allUsers } = useGetAdminUsers(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentEditingCardinal, setCurrentEditingCardinal] = useState<{ + id: number; + cardinalNumber: number; + } | null>(null); const [cardinalList, setCardinalList] = useState< - { id: number; year?: number; semester?: number; cardinalNumber: number }[] + { + id: number; + year?: number; + semester?: number; + cardinalNumber: number; + status?: string; + }[] >([]); useEffect(() => { @@ -27,6 +42,19 @@ const CardinalInfo: React.FC = () => { const totalMembers = allUsers.length; + const handleCardinalClick = (cardinal: { + id: number; + year?: number; + semester?: number; + cardinalNumber: number; + }) => { + if (!cardinal.year || !cardinal.semester) { + setCurrentEditingCardinal(cardinal); + setIsModalOpen(true); + } else { + setSelectedCardinal(cardinal.cardinalNumber); + } + }; return ( @@ -38,6 +66,8 @@ const CardinalInfo: React.FC = () => { color={theme.color.gray[18]} lastColor="#D3D3D3" isCardinalBox + isClick + onClick={() => setSelectedCardinal(null)} /> {cardinalList.map((cardinal) => { const memberCount = getMemberCountByCardinal(cardinal.cardinalNumber); @@ -47,17 +77,35 @@ const CardinalInfo: React.FC = () => { : `노정완 외 ${memberCount}명`; return ( - handleCardinalClick(cardinal)} /> ); })} + {isModalOpen && currentEditingCardinal && ( + setIsModalOpen(false)} + initialCardinal={currentEditingCardinal} + setCardinalList={setCardinalList} + /> + )} ); diff --git a/src/components/Admin/CardinalSearchBar.tsx b/src/components/Admin/CardinalSearchBar.tsx index 7372af34..405d0dcf 100644 --- a/src/components/Admin/CardinalSearchBar.tsx +++ b/src/components/Admin/CardinalSearchBar.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useState } from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import CardinalDropDown from '@/components/Admin/Cardinal'; import SearchBar, { SearchBarWrapper } from '@/components/Admin/SearchBar'; @@ -7,17 +7,16 @@ interface CombinedSearchBarProps { setSelectedCardinal: Dispatch>; } -const CombinedSearchBar: React.FC = () => { - const [selectedCardinal, setSelectedCardinal] = useState(null); - +const CombinedSearchBar: React.FC = ({ + selectedCardinal, + setSelectedCardinal, +}) => { return (
{ - setSelectedCardinal(value); - }} + setSelectedCardinal={setSelectedCardinal} />
diff --git a/src/components/Admin/DirectCardinal.tsx b/src/components/Admin/DirectCardinal.tsx index 168a05d3..2c9da534 100644 --- a/src/components/Admin/DirectCardinal.tsx +++ b/src/components/Admin/DirectCardinal.tsx @@ -21,7 +21,7 @@ const DirectCardinalDropdown: React.FC = ({ }) => { const [isOpen, setIsOpen] = useState(false); const { allCardinals } = useGetAllCardinals(); - const [isCustomInput] = useState(false); + const [isCustomInput, setIsCustomInput] = useState(false); const sortedCardinals = [...allCardinals].reverse(); @@ -29,17 +29,18 @@ const DirectCardinalDropdown: React.FC = ({ const selectCardinal = (value: number) => { setSelectedCardinal(value, false); + setIsCustomInput(false); setIsOpen(false); }; const handleCustomInput = () => { setSelectedCardinal(0, true); + setIsCustomInput(true); setIsOpen(false); }; const getDisplayText = () => { - if (isCustomInput || selectedCardinal === 0 || selectedCardinal === null) - return '직접 입력'; + if (isCustomInput || selectedCardinal === null) return '직접 입력'; return `${selectedCardinal}기`; }; diff --git a/src/components/Admin/Modal/CardinalEditModal.tsx b/src/components/Admin/Modal/CardinalEditModal.tsx index ecdf2b73..6fcfce19 100644 --- a/src/components/Admin/Modal/CardinalEditModal.tsx +++ b/src/components/Admin/Modal/CardinalEditModal.tsx @@ -4,6 +4,7 @@ import * as S from '@/styles/admin/cardinal/CardinalModal.styled'; import CommonCardinalModal from '@/components/Admin/Modal/CommonCardinalModal'; import DirectCardinalDropdown from '@/components/Admin/DirectCardinal'; import { continueNextCardinalApi } from '@/api/admin/member/patchUserManagement'; +import useGetAllCardinals from '@/api/useGetCardinals'; interface CardinalChangeModalProps { isOpen: boolean; @@ -26,17 +27,21 @@ const CardinalEditModal: React.FC = ({ }) => { const [cardinalNumber, setCardinalNumber] = useState(''); const [isCustomInput, setIsCustomInput] = useState(false); - const [selectedCardinal] = useState(null); - + const [selectedCardinal, setSelectedCardinal] = useState(null); const inputRef = useRef(null); + const { allCardinals } = useGetAllCardinals(); + const existingCardinalNumbers = allCardinals.map((c) => c.cardinalNumber); + const handleSelectCardinal = (value: number, isCustom: boolean) => { setIsCustomInput(isCustom); if (isCustom) { setCardinalNumber(''); + setSelectedCardinal(null); setTimeout(() => inputRef.current?.focus(), 0); } else { setCardinalNumber(String(value)); + setSelectedCardinal(value); } }; @@ -47,6 +52,16 @@ const CardinalEditModal: React.FC = ({ } try { + const newCardinalNumber = Number(cardinalNumber); + + const isExistingCardinal = + existingCardinalNumbers.includes(newCardinalNumber); + + if (isCustomInput && isExistingCardinal) { + alert('이미 존재하는 기수입니다.'); + return; + } + const cardinalData = selectedUserIds.map((id) => ({ userId: id, cardinal: Number(cardinalNumber), diff --git a/src/components/Admin/Modal/CardinalModal.tsx b/src/components/Admin/Modal/CardinalModal.tsx index 834708a4..ce727db5 100644 --- a/src/components/Admin/Modal/CardinalModal.tsx +++ b/src/components/Admin/Modal/CardinalModal.tsx @@ -1,41 +1,76 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import Button from '@/components/Button/Button'; import CheckBox from '@/assets/images/ic_admin_checkbox.svg'; import UnCheckBox from '@/assets/images/ic_admin_uncheckbox.svg'; import * as S from '@/styles/admin/cardinal/CardinalModal.styled'; import CommonCardinalModal from '@/components/Admin/Modal/CommonCardinalModal'; -import postCardinalApi from '@/api/admin/cardinal/postCardinal'; +import { + patchCardinalApi, + postCardinalApi, +} from '@/api/admin/cardinal/postCardinal'; +import { + handleNumericInput, + preventNonNumeric, +} from '@/utils/admin/handleNumericInput'; interface CardinalModalProps { isOpen: boolean; onClose: () => void; + initialCardinal?: { + id: number; + cardinalNumber: number; + year?: number; + semester?: number; + status?: string; + }; + cardinalList: { + id: number; + year?: number; + semester?: number; + cardinalNumber: number; + status?: string; + }[]; + setCardinalList: React.Dispatch< + React.SetStateAction< + { + id: number; + year?: number; + semester?: number; + cardinalNumber: number; + status?: string; + }[] + > + >; } export const ModalContentWrapper = styled.div` - padding: 10px; + padding: 5px 20px; display: flex; flex-direction: column; align-items: flex-start; gap: 20px; + box-sizing: border-box; `; -const CardinalModal: React.FC = ({ isOpen, onClose }) => { +const CardinalModal: React.FC = ({ + isOpen, + onClose, + initialCardinal, + cardinalList, + setCardinalList = () => {}, +}) => { + const isEditing = Boolean(initialCardinal); + + const [, setCardinals] = useState(cardinalList); + const [formState, setFormState] = useState({ - cardinalNumber: '', - year: '', - semester: '', - isChecked: false, + cardinalNumber: initialCardinal?.cardinalNumber || '', + year: initialCardinal?.year || '', + semester: initialCardinal?.semester || '', + isChecked: initialCardinal?.status === 'IN_PROGRESS' || false, }); - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormState((prev) => ({ - ...prev, - [name]: value, - })); - }; - const handleCheckBoxClick = () => { setFormState((prev) => ({ ...prev, @@ -44,7 +79,7 @@ const CardinalModal: React.FC = ({ isOpen, onClose }) => { }; const handleClick = async () => { - const { cardinalNumber, year, semester } = formState; + const { cardinalNumber, year, semester, isChecked } = formState; if (!cardinalNumber || !year || !semester) { alert('모든 필드를 입력해주세요.'); @@ -52,20 +87,58 @@ const CardinalModal: React.FC = ({ isOpen, onClose }) => { } try { - const response = await postCardinalApi( - Number(cardinalNumber), - Number(year), - Number(semester), - ); - console.log('새로운 기수 저장 성공:', response); - alert('새로운 기수가 저장되었습니다.'); + let updatedCardinal: { + id: number; + year?: number; + semester?: number; + cardinalNumber: number; + status?: string; + }; + + if (isEditing && initialCardinal?.id) { + // 기수 수정 api 호출 + const response = await patchCardinalApi( + initialCardinal.id, + Number(year), + Number(semester), + isChecked, + ); + console.log('기수 수정 성공:', response); + alert('기수 정보가 수정되었습니다.'); + updatedCardinal = response.data; + setCardinalList((prev) => + prev.map((cardinal) => + cardinal.id === updatedCardinal.id + ? { ...cardinal, ...updatedCardinal } + : cardinal, + ), + ); + } else { + // 기수 추가 api 호출 + const response = await postCardinalApi( + Number(cardinalNumber), + Number(year), + Number(semester), + isChecked, + ); + console.log('새로운 기수 저장 성공:', response); + alert('새로운 기수가 저장되었습니다.'); + updatedCardinal = response.data; + + setCardinalList((prev) => [...prev, { ...updatedCardinal }]); + } + onClose(); } catch (error: any) { console.error('새로운 기수 저장 실패:', error.message); - alert(`새로운 기수 저장 실패: ${error.message}`); + alert(`기수 저장 실패: ${error.message}`); } }; + useEffect(() => { + setCardinals(cardinalList); + }, [cardinalList]); + return ( = ({ isOpen, onClose }) => { } > - 추가할 새로운 기수를 작성해주세요 - -
활동 시기
- + {isEditing ? '학기 정보 추가' : '새로운 기수 추가'} +
  • 추가할 새로운 기수를 작성해주세요
  • + - handleNumericInput(e, setFormState, 2)} + onKeyDown={preventNonNumeric} + readOnly={isEditing} /> + + +
  • 활동 시기
  • + + + handleNumericInput(e, setFormState, 4)} + onKeyDown={preventNonNumeric} + /> + + + + + handleNumericInput(e, setFormState, 1, ['1', '2']) + } + onKeyDown={preventNonNumeric} + /> + 학기 + = ({ void; } -const getHighestCardinal = (cardinals: string): string => - `${cardinals.split('.')[0]}기`; - const MemberDetailModal: React.FC = ({ data, onClose, }) => { const { handleAction } = useAdminActions(); + const [isCardinalModalOpen, setIsCardinalModalOpen] = useState(false); const roleChangeButton = data.role === 'ADMIN' @@ -46,9 +47,14 @@ const MemberDetailModal: React.FC = ({ onClick: () => handleAction('유저 추방', [data.id]), }, { - label: '직접 입력', - onClick: () => alert('직접 입력'), - icon: dropdownIcon, + label: '기수 변경', + onClick: () => setIsCardinalModalOpen(true), + style: { + backgroundColor: isCardinalModalOpen + ? theme.color.gray[18] + : theme.color.gray[100], + color: isCardinalModalOpen ? theme.color.gray[100] : '#000', + }, }, { label: '완료', onClick: onClose }, ]; @@ -72,66 +78,77 @@ const MemberDetailModal: React.FC = ({ ]; return ( - } - > - - - - 회원정보 - - - - {data.name}   - {getHighestCardinal(data.cardinals)} + <> + } + > + + + + 회원정보 + + + + {data.name}   + {getHighestCardinal(data.cardinals)} + + + + + + {memberInfo.map((info) => ( + {info.label} + ))} + + + {memberInfo.map((info) => ( + + {info.value} + + ))} + + + + + + 활동정보 - - - - - {memberInfo.map((info) => ( - {info.label} - ))} - - - {memberInfo.map((info) => ( - - {info.value} - - ))} - - - - - - 활동정보 - - - - {activityInfo.map((info) => ( - {info.label} - ))} - - - {activityInfo.map((info) => ( - - {info.value} - - ))} - - - - - + + + {activityInfo.map((info) => ( + {info.label} + ))} + + + {activityInfo.map((info) => ( + + {info.value} + + ))} + + + + + + {isCardinalModalOpen && ( + setIsCardinalModalOpen(false)} + selectedUserIds={[data.id]} + top="26%" + left="38%" + /> + )} + ); }; diff --git a/src/components/Admin/NavMenuList.tsx b/src/components/Admin/NavMenuList.tsx index 7151832e..5e20bd2d 100644 --- a/src/components/Admin/NavMenuList.tsx +++ b/src/components/Admin/NavMenuList.tsx @@ -43,7 +43,7 @@ const NavMenuList: React.FC = () => { { id: 'penalty', icon: , - label: '페널티 관리', + label: '패널티 관리', path: '/admin/penalty', }, { @@ -64,7 +64,7 @@ const NavMenuList: React.FC = () => { { id: 'manual', icon: , - label: '관리자 메뉴얼', + label: '관리자 매뉴얼', path: '', // 추후 수정 }, ]; diff --git a/src/components/Admin/PenaltyListTable.tsx b/src/components/Admin/PenaltyListTable.tsx index 25717a84..d019e895 100644 --- a/src/components/Admin/PenaltyListTable.tsx +++ b/src/components/Admin/PenaltyListTable.tsx @@ -26,11 +26,37 @@ const columns = [ { key: 'empty', header: '' }, ]; -const PenaltyListTable: React.FC = () => { - const { filteredMembers } = useMemberContext(); - const StatusfilteredMembers = filteredMembers.filter( - (member) => member.status === '승인 완료', - ); +interface PenaltyListTableProps { + selectedCardinal: number | null; +} + +const PenaltyListTable: React.FC = ({ + selectedCardinal, +}) => { + const { members } = useMemberContext(); + const [filteredMembers, setFilteredMembers] = useState(members); + + useEffect(() => { + const newFilteredMembers = members.filter((member) => { + const isApproved = member.status === '승인 완료'; + + if (selectedCardinal) { + let cardinalNumbers: number[] = []; + + if (typeof member.cardinals === 'string') { + cardinalNumbers = (member.cardinals as string).split('.').map(Number); + } else if (Array.isArray(member.cardinals)) { + cardinalNumbers = member.cardinals as number[]; + } + + return isApproved && cardinalNumbers.includes(selectedCardinal); + } + + return isApproved; + }); + + setFilteredMembers(newFilteredMembers); + }, [selectedCardinal, members]); const [expandedRow, setExpandedRow] = useState(null); const [isAdding, setIsAdding] = useState(false); @@ -163,7 +189,7 @@ const PenaltyListTable: React.FC = () => { - {StatusfilteredMembers.map((member) => ( + {filteredMembers.map((member) => ( ` diff --git a/src/components/Admin/SortButton.tsx b/src/components/Admin/SortButton.tsx index 05d7489d..4980e814 100644 --- a/src/components/Admin/SortButton.tsx +++ b/src/components/Admin/SortButton.tsx @@ -22,7 +22,7 @@ const SortButton: React.FC = () => { }; return ( - {sortingOrder === 'NAME_ASCENDING' ? '오름차순' : '기수 순'} + {sortingOrder === 'NAME_ASCENDING' ? '이름순' : '기수순'} sorting ); diff --git a/src/components/Admin/context/MemberContext.tsx b/src/components/Admin/context/MemberContext.tsx index f4baf098..3bfcfea1 100644 --- a/src/components/Admin/context/MemberContext.tsx +++ b/src/components/Admin/context/MemberContext.tsx @@ -1,6 +1,8 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { getAllUsers } from '@/api/admin/member/getAdminUser'; import formatDate from '@/utils/admin/dateUtils'; +import useGetAllCardinals from '@/api/useGetCardinals'; +import getHighestCardinal from '@/utils/admin/getHighestCardinal'; export type MemberData = { id: number; @@ -8,7 +10,7 @@ export type MemberData = { name: string; role: string; department: string; - cardinals: string; + cardinals: number[]; tel: string; studentId: string; position: string; @@ -18,7 +20,7 @@ export type MemberData = { LatestPenalty?: string; createdAt: string; email?: string; - membershipType?: '활동 중' | '알럼나이'; + membershipType?: '활동 중' | '알럼나이' | '상태 없음'; }; interface MemberContextProps { @@ -32,6 +34,8 @@ interface MemberContextProps { setSortingOrder: React.Dispatch< React.SetStateAction<'NAME_ASCENDING' | 'CARDINAL_DESCENDING'> >; + selectedCardinal: number | null; + setSelectedCardinal: React.Dispatch>; } // context 생성 @@ -52,12 +56,19 @@ export const MemberProvider: React.FC<{ children: React.ReactNode }> = ({ 'NAME_ASCENDING' | 'CARDINAL_DESCENDING' >('NAME_ASCENDING'); + const [selectedCardinal, setSelectedCardinal] = useState(null); + const statusMapping: Record = { ACTIVE: '승인 완료', WAITING: '대기 중', BANNED: '추방', }; + const { allCardinals } = useGetAllCardinals(); + const currentCardinal = + allCardinals.find((c) => c.status === 'IN_PROGRESS')?.cardinalNumber || + null; + useEffect(() => { const fetchMembers = async () => { try { @@ -65,16 +76,36 @@ export const MemberProvider: React.FC<{ children: React.ReactNode }> = ({ const response = await getAllUsers(sortingOrder); const fetchedMembers = response.data.data || []; console.log('API응답: ', response.data); - const mappedMembers = fetchedMembers.map((user: any) => ({ - ...user, - cardinals: - user.cardinals.length > 0 ? user.cardinals.reverse().join('.') : '', - status: statusMapping[user.status] || '추방', - attendanceCount: user.attendanceCount ?? 0, - absenceCount: user.absenceCount ?? 0, - penaltyCount: user.penaltyCount ?? 0, - createdAt: formatDate(user.createdAt), - })); + + const mappedMembers = fetchedMembers.map((user: any) => { + const highestMemberCardinalStr = getHighestCardinal(user.cardinals); + const highestMemberCardinal = parseInt(highestMemberCardinalStr, 10); + + let membershipType: '활동 중' | '알럼나이' | '상태 없음'; + + if (Number.isNaN(highestMemberCardinal)) { + membershipType = '상태 없음'; + } else if (highestMemberCardinal === currentCardinal) { + membershipType = '활동 중'; + } else { + membershipType = '알럼나이'; + } + + return { + ...user, + cardinals: + user.cardinals.length > 0 + ? user.cardinals.reverse().join('.') + : '', + status: statusMapping[user.status] || '대기 중', + attendanceCount: user.attendanceCount ?? 0, + absenceCount: user.absenceCount ?? 0, + penaltyCount: user.penaltyCount ?? 0, + createdAt: formatDate(user.createdAt), + membershipType, + }; + }); + setMembers(mappedMembers); setFilteredMembers(mappedMembers); setError(null); @@ -85,6 +116,20 @@ export const MemberProvider: React.FC<{ children: React.ReactNode }> = ({ fetchMembers(); }, [sortingOrder]); + + useEffect(() => { + if (selectedCardinal === null) { + setFilteredMembers(members); + return; + } + + const filtered = members.filter((member) => { + return member.cardinals.includes(selectedCardinal); + }); + + setFilteredMembers(filtered); + }, [selectedCardinal, members]); + const value = useMemo( () => ({ members, @@ -95,8 +140,10 @@ export const MemberProvider: React.FC<{ children: React.ReactNode }> = ({ setFilteredMembers, sortingOrder, setSortingOrder, + selectedCardinal, + setSelectedCardinal, }), - [members, selectedMembers, filteredMembers, error], + [members, selectedMembers, filteredMembers, selectedCardinal, error], ); return ( diff --git a/src/pages/admin/AdminPenalty.tsx b/src/pages/admin/AdminPenalty.tsx index 0c2673e9..f3ea64ed 100644 --- a/src/pages/admin/AdminPenalty.tsx +++ b/src/pages/admin/AdminPenalty.tsx @@ -1,8 +1,5 @@ import CardinalSearchBar from '@/components/Admin/CardinalSearchBar'; -import { - MemberProvider, - // useMemberContext, -} from '@/components/Admin/context/MemberContext'; +import { MemberProvider } from '@/components/Admin/context/MemberContext'; import NavMenu from '@/components/Admin/NavMenu'; import PenaltyListTable from '@/components/Admin/PenaltyListTable'; import TopBar from '@/components/Admin/TopBar'; @@ -16,17 +13,6 @@ import { useState } from 'react'; const AdminPenalty: React.FC = () => { const [selectedCardinal, setSelectedCardinal] = useState(null); - // const { members } = useMemberContext(); - - // const filteredMembers = selectedCardinal - // ? members.filter((member) => { - // const cardinalNumbers = Array.isArray(member.cardinals) - // ? member.cardinals.map(Number) - // : [Number(member.cardinals)]; - - // return cardinalNumbers.includes(selectedCardinal); - // }) - // : members; return ( @@ -43,8 +29,7 @@ const AdminPenalty: React.FC = () => { selectedCardinal={selectedCardinal} setSelectedCardinal={setSelectedCardinal} /> - {/* */} - + diff --git a/src/styles/admin/cardinal/CardinalInfo.styled.ts b/src/styles/admin/cardinal/CardinalInfo.styled.ts index 9fbc9b85..84669455 100644 --- a/src/styles/admin/cardinal/CardinalInfo.styled.ts +++ b/src/styles/admin/cardinal/CardinalInfo.styled.ts @@ -12,11 +12,6 @@ export const TotalBox = styled(Box)` background-color: ${theme.color.gray[18]}; `; -export const CardinalBox = styled(Box)<{ isIncomplete?: boolean }>` - ${({ isIncomplete }) => - isIncomplete ? `border: 2px dashed ${theme.color.gray[18]};` : ''} -`; - export const ScrollContainer = styled.div` display: flex; gap: 16px; diff --git a/src/styles/admin/cardinal/CardinalModal.styled.ts b/src/styles/admin/cardinal/CardinalModal.styled.ts index bef62dcc..906fe362 100644 --- a/src/styles/admin/cardinal/CardinalModal.styled.ts +++ b/src/styles/admin/cardinal/CardinalModal.styled.ts @@ -122,41 +122,73 @@ export const StyledInput = styled.input<{ flex: number; maxWidth: string }>` border-radius: 4px; font-size: 16px; padding: 12px; - &::placeholder { color: ${theme.color.gray[65]}; } // readOnly 일 때 색상 변경 ${({ readOnly }) => - readOnly && - ` - color: ${theme.color.gray[65]}; - cursor: not-allowed; - `} + readOnly + ? ` + color: ${theme.color.gray[65]}; + cursor: not-allowed; + &:focus { + outline: none + } + ` + : ` + &:focus { + outline: 2px solid #2f2f2f; + } + `} `; // CardinalModal.tsx -export const Input = styled.input` +export const InputWrapper = styled.div` + display: flex; + align-items: center; width: 100%; max-width: 100%; - padding: 15px; + padding: 10px; box-sizing: border-box; - border: 1px solid #ddd; + border: 1px solid #dedede; font-size: 16px; - font-family: ${theme.font.semiBold}; outline: none; - text-align: right; :focus::placeholder { color: transparent; } `; +export const Input = styled.input<{ readOnly?: boolean }>` + font-family: ${theme.font.semiBold}; + font-size: 18px; + flex-grow: 1; + width: 50%; + border: none; + outline: none; + text-align: right; + padding: 5px; + + ${({ readOnly }) => + readOnly && + ` + color: ${theme.color.gray[65]}; + cursor: not-allowed; + `} +`; + +export const Unit = styled.div` + font-size: 18px; + color: ${theme.color.gray[65]}; + white-space: nowrap; +`; + export const Title = styled.div` - font-weight: 500; - font-size: 16px; - color: #000; + font-weight: 700; + font-size: 24px; + margin-top: -30px; + padding-bottom: 10px; `; export const FlexRow = styled.div` diff --git a/src/utils/admin/getHighestCardinal.ts b/src/utils/admin/getHighestCardinal.ts new file mode 100644 index 00000000..5d3eff41 --- /dev/null +++ b/src/utils/admin/getHighestCardinal.ts @@ -0,0 +1,22 @@ +const getHighestCardinal = (cardinals: number[] | undefined | null): string => { + if (!cardinals) return '기수 없음'; + + let validCardinals: number[] = []; + + if (typeof cardinals === 'string') { + validCardinals = (cardinals as string) + .split('.') + .map((c) => Number(c)) + .filter((c) => !Number.isNaN(c)); + } else if (Array.isArray(cardinals)) { + validCardinals = cardinals.filter( + (c) => typeof c === 'number' && !Number.isNaN(c), + ); + } + + if (validCardinals.length === 0) return '기수 없음'; + + return `${Math.max(...validCardinals)}기`; +}; + +export default getHighestCardinal; diff --git a/src/utils/admin/handleNumericInput.ts b/src/utils/admin/handleNumericInput.ts new file mode 100644 index 00000000..831897f8 --- /dev/null +++ b/src/utils/admin/handleNumericInput.ts @@ -0,0 +1,33 @@ +// 숫자 이외의 문자 제거 +const handleNumericInput = ( + e: React.ChangeEvent, + setState: React.Dispatch>, + maxLength?: number, // 입력 가능한 최대 길이 + allowedValues?: string[], // 허용할 값 리스트 ( 학기: ['1', '2']) +) => { + const { name, value } = e.target; + + let numericValue = value.replace(/[^0-9]/g, ''); + + if (maxLength) { + numericValue = numericValue.slice(0, maxLength); + } + + if (allowedValues && !allowedValues.includes(numericValue)) { + numericValue = ''; + } + + setState((prev: any) => ({ + ...prev, + [name]: numericValue, + })); +}; + +const preventNonNumeric = (e: React.KeyboardEvent) => { + // 숫자 키, 백스페이스, 삭제키만 허용 + if (!/[0-9]/.test(e.key) && e.key !== 'Backspace' && e.key !== 'Delete') { + e.preventDefault(); + } +}; + +export { preventNonNumeric, handleNumericInput };