diff --git a/services/admin/public/index.html b/services/admin/public/index.html index 14fd0c65..9e674e38 100644 --- a/services/admin/public/index.html +++ b/services/admin/public/index.html @@ -9,5 +9,6 @@
+ diff --git a/services/admin/src/apis/points/index.ts b/services/admin/src/apis/points/index.ts index a7814c06..6a3c79ec 100644 --- a/services/admin/src/apis/points/index.ts +++ b/services/admin/src/apis/points/index.ts @@ -31,14 +31,12 @@ export const getStudentPointHistory = async ( page?: number, size?: number, ) => { - if (student_id) { - const { data } = await instance.get>( - `${router}/history/students/${student_id}${ - page || size ? `?page=${page}&size=${size}` : '' - }`, - ); - return data; - } + const { data } = await instance.get>( + `${router}/history/students/${student_id}${ + page || size ? `?page=${page}&size=${size}` : '' + }`, + ); + return data; }; /** 학생 상/벌점 최근 내역 조회 */ diff --git a/services/admin/src/apis/points/response.ts b/services/admin/src/apis/points/response.ts index c8bbb966..230dc19d 100644 --- a/services/admin/src/apis/points/response.ts +++ b/services/admin/src/apis/points/response.ts @@ -28,6 +28,7 @@ export interface RecentStudentPointResponse { export interface StudentPointHistoryType { point_history_id: string; + date?: string; type: PointType; score: number; name: string; diff --git a/services/admin/src/components/apply/study/SeatSetting.tsx b/services/admin/src/components/apply/study/SeatSetting.tsx index b3843c3f..f8bd9c8e 100644 --- a/services/admin/src/components/apply/study/SeatSetting.tsx +++ b/services/admin/src/components/apply/study/SeatSetting.tsx @@ -18,6 +18,8 @@ import { import { SeatType } from '@/apis/studyRooms/response'; import { SelectedModalType } from '@/context/modal'; import { useStudyRoom } from '@/hooks/useStudyRoom'; +import { SideBar } from '@/components/sidebar'; +import SideBarPortal from '@/components/sidebar/SideBarPortal'; const seatStatus = ['AVAILABLE', 'UNAVAILABLE', 'EMPTY'].map( (i: SeatStatusType) => seatStatusToKorean(i), @@ -53,14 +55,13 @@ export function SeatSetting({ }; return ( - - <_Wrapper> - <_EscapeWrapper onClick={closeSeatSetting}> - - - - 자리 설정 - + + { + !addSeat && closeSeatSetting(); + }} + > - - + + ); } diff --git a/services/admin/src/components/main/DetailBox/DetailBox.tsx b/services/admin/src/components/main/DetailBox/DetailBox.tsx index cc60f207..ac8b8385 100644 --- a/services/admin/src/components/main/DetailBox/DetailBox.tsx +++ b/services/admin/src/components/main/DetailBox/DetailBox.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import styled from 'styled-components'; import { Button, Text } from '@team-aliens/design-system'; import { GetStudentDetailResponse } from '@/apis/managers/response'; -import { PointItem } from './PointItem'; +import { StudentPointItem } from './PointItem'; import { StudentProfile } from './StudentInfo'; import { PointBox } from './PointBox'; import { PointType } from '@/apis/points'; @@ -36,11 +36,11 @@ export function DetailBox({ <_DetailBox> {availableFeature?.point_service && ( <_PointWrapper> @@ -48,13 +48,13 @@ export function DetailBox({ currentPointType={currentPointType} setCurrentPointType={setCurrentPointType} pointType="BONUS" - point={studentDetail.bonus_point} + point={studentDetail?.bonus_point} /> )} @@ -62,7 +62,7 @@ export function DetailBox({ 동일 호실 학생 <_MateList> - {studentDetail.room_mates.map((item) => ( + {studentDetail?.room_mates.map((item) => ( - <_Display> - - 전체 학생 상/벌점 - - - {data?.point_histories && data.point_histories.map((res, i) => { const { @@ -95,7 +80,7 @@ export function PointList() { return ( <> {!isSameDate && ( - + {date} )} @@ -137,10 +122,7 @@ export function PointList() { } const _Wrapper = styled.div` - width: 670px; - margin-right: 361px; - margin-left: 50px; - margin-bottom: 150px; + width: 418px; `; const _Display = styled.div` diff --git a/services/admin/src/components/main/StudentBox.tsx b/services/admin/src/components/main/StudentBox.tsx index 5293afb3..1da93504 100644 --- a/services/admin/src/components/main/StudentBox.tsx +++ b/services/admin/src/components/main/StudentBox.tsx @@ -1,11 +1,22 @@ -import { useEffect, useRef, useContext, SetStateAction, Dispatch } from 'react'; +import { + useEffect, + useRef, + useContext, + SetStateAction, + Dispatch, + ChangeEvent, + MouseEvent, +} from 'react'; import styled from 'styled-components'; import { Text, CheckBox } from '@team-aliens/design-system'; import { StudentInfo } from '@/apis/managers/response'; import { ModeType } from '@/pages/Home'; import { usePointHistoryList } from '@/hooks/usePointHistoryList'; import { Tag } from './Tag'; -import { useSelectedStudentIdStore } from '@/store/useSelectedStudentIdStore'; +import { + useClickedStudentIdStore, + useSelectedStudentIdStore, +} from '@/store/useSelectedStudentIdStore'; export interface StudentBoxProps { mode: ModeType; @@ -30,6 +41,13 @@ export function StudentBox({ state.deleteStudentId, ]); + const [clickedStudentId, setClickedStudentId, resetClickedStudentId] = + useClickedStudentIdStore((state) => [ + state.clickedStudentId, + state.setClickedStudentId, + state.resetClickedStudentId, + ]); + useEffect(() => { if (selectedStudentId.includes(studentInfo.id)) ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -44,9 +62,26 @@ export function StudentBox({ } }; + const preventCheckBoxClick = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const clickStudent = () => { + if (studentInfo.id === clickedStudentId) resetClickedStudentId(); + else if (studentInfo.id !== clickedStudentId) + setClickedStudentId(studentInfo.id); + }; + return ( - <_Wrapper ref={ref} className="studentBox"> - + <_Wrapper + isClick={clickedStudentId === studentInfo.id} + ref={ref} + className="studentBox" + onClick={clickStudent} + > +
+ +
` position: relative; z-index: 1; width: 100%; @@ -97,6 +132,8 @@ const _Wrapper = styled.li` display: flex; align-items: center; cursor: pointer; + border: ${({ theme, isClick }) => + isClick && `2px solid ${theme.color.primary}`}; > img { width: 36px; height: 36px; diff --git a/services/admin/src/components/main/StudentList.tsx b/services/admin/src/components/main/StudentList.tsx index 94ffa25c..75d67734 100644 --- a/services/admin/src/components/main/StudentList.tsx +++ b/services/admin/src/components/main/StudentList.tsx @@ -45,11 +45,17 @@ import { IsUseAbleFeature } from '@/apis/auth/response'; import { Divider } from './Divider'; import { ViewItem } from './ViewItem'; import { usePointHistoryList } from '@/hooks/usePointHistoryList'; -import { useSelectedStudentIdStore } from '@/store/useSelectedStudentIdStore'; +import { + useClickedStudentIdStore, + useSelectedStudentIdStore, +} from '@/store/useSelectedStudentIdStore'; import { usePointHistoryId } from '@/store/usePointHistoryId'; import { useQueryClient } from '@tanstack/react-query'; import { useDeleteTagIdStore } from '@/store/useDeleteTagId'; import StudentSelectModal from '../modals/StudentSelectModal'; +import SideBarPortal from '../sidebar/SideBarPortal'; +import { PointList } from './PointList'; +import { SideBar } from '../sidebar'; interface Props extends FilterState { mode: ModeType; @@ -93,12 +99,18 @@ export function StudentList({ state.deleteStudentId, ]); + const [clickedStudentId, setClickedStudentId] = useClickedStudentIdStore( + (state) => [state.clickedStudentId, state.setClickedStudentId], + ); + const [pointHistoryId] = usePointHistoryId((state) => [state.pointHistoryId]); const [tagId] = useDeleteTagIdStore((state) => [state.deleteTagId]); const { modalState, selectModal, closeModal } = useModal(); const [tagModal, setTagModal] = useState(''); const [showGiveModal, setShowGiveModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); + const [openAllPointHistorySideBar, setOpenAllPointHistorySideBar] = + useState(false); const openPointFilterModal = () => selectModal('POINT_FILTER'); const cancelPoint = useCancelPointHistory(pointHistoryId); @@ -137,13 +149,14 @@ export function StudentList({ const deleteTagAPI = useDeleteTag(selectedTag, { onSuccess: () => { refetchAllTags(); + queryClient.invalidateQueries(['studentList']); setSelectedTag(''); toastDispatch({ toastType: 'SUCCESS', actionType: 'APPEND_TOAST', message: '태그가 삭제되었습니다.', }); - selectModal(''); + selectModal('VIEW_TAG_OPTIONS'); }, onError: () => { toastDispatch({ @@ -198,7 +211,7 @@ export function StudentList({ setIsSelectAllButton((prev) => !prev); }; - const deleteStudentTag = useDeleteStudentTag(selectedStudentId[0], tagId); + const deleteStudentTag = useDeleteStudentTag(clickedStudentId, tagId); return ( <_Wrapper> @@ -209,87 +222,6 @@ export function StudentList({ onChange={onChangeSearchName} /> <_Buttons> - {/* {mode === 'POINTS' && ( - <_ChooseModalBoxWrapper> - - {selectedStudentId.filter((i) => i).length > 0 && - showGiveModal && ( - { - setShowGiveModal(false); - }} - > - <_ChooseBox> - {availableFeature?.point_service && ( - <> - <_ChooseBoxText - onClick={() => { - selectModal('GIVE_POINT'); - setShowGiveModal(false); - }} - > - 상/벌점 부여 - - <_Line /> - - )} - <_ChooseBoxText - onClick={() => { - selectModal('GIVE_TAG_OPTIONS'); - setShowGiveModal(false); - }} - > - 학생 태그 부여 - - - - )} - {!(selectedStudentId.filter((i) => i).length > 0) && - showViewModal && ( - { - if ( - !(e.target as Element).className.includes('grantPoint') - ) - setShowViewModal(false); - }} - > - <_ChooseBox> - {availableFeature?.point_service && ( - <> - <_ChooseBoxText - onClick={() => { - selectModal('POINT_OPTIONS'); - setShowViewModal(false); - }} - > - 상/벌점 항목 보기 - - <_Line /> - - )} - <_ChooseBoxText - onClick={() => { - selectModal('VIEW_TAG_OPTIONS'); - setShowViewModal(false); - }} - > - 학생 태그 항목 보기 - - - - )} - - )} */} {availableFeature?.point_service && ( {click && ( - <_Item> - + <_Item onClick={openViewPointOptionModal}> + 상/벌점 항목 보기 - <_Item> - + <_Item onClick={openViewTagOptionModal}> + 학생 태그 항목 보기 diff --git a/services/admin/src/components/modals/GivePointOptionsModal.tsx b/services/admin/src/components/modals/GivePointOptionsModal.tsx index 9057a6d1..c3adc1ec 100644 --- a/services/admin/src/components/modals/GivePointOptionsModal.tsx +++ b/services/admin/src/components/modals/GivePointOptionsModal.tsx @@ -33,6 +33,7 @@ interface PropsType { } const canClick = true; + export function GivePointOptionsModal({ allPointOptions }: PropsType) { const [newItem, setNewItem] = useState(true); const [selectedStudentId] = useSelectedStudentIdStore((store) => [ diff --git a/services/admin/src/components/modals/PointFilter.tsx b/services/admin/src/components/modals/PointFilter.tsx index feefd7b1..99541f8c 100644 --- a/services/admin/src/components/modals/PointFilter.tsx +++ b/services/admin/src/components/modals/PointFilter.tsx @@ -51,7 +51,7 @@ export function PointFilterModal({ 적용 , ]} - close={close} + close={closeModal} title="필터" > <_Buttons> diff --git a/services/admin/src/components/sidebar/SideBarPortal.ts b/services/admin/src/components/sidebar/SideBarPortal.ts new file mode 100644 index 00000000..4056af10 --- /dev/null +++ b/services/admin/src/components/sidebar/SideBarPortal.ts @@ -0,0 +1,9 @@ +import reactDom from 'react-dom'; +import React from 'react'; + +const SideBarPortal = ({ children }: { children: React.ReactNode }) => { + const el = document.getElementById('side-bar') as HTMLDivElement; + return reactDom.createPortal(children, el as HTMLDivElement); +}; + +export default SideBarPortal; diff --git a/services/admin/src/components/sidebar/index.tsx b/services/admin/src/components/sidebar/index.tsx new file mode 100644 index 00000000..0d2eae68 --- /dev/null +++ b/services/admin/src/components/sidebar/index.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components'; +import React from 'react'; +import { Escape, Text } from '@team-aliens/design-system'; +import OutsideClickHandler from 'react-outside-click-handler'; + +interface PropsType { + title?: string; + children: React.ReactNode; + close: () => void; +} + +export function SideBar({ title, children, close }: PropsType) { + return ( + + <_Wrapper> + <_EscapeWrapper onClick={close}> + + + + {title} + + {children} + + + ); +} + +const _Wrapper = styled.div` + position: fixed; + display: flex; + flex-direction: column; + overflow: scroll; + padding: 40px 40px 30px 40px; + top: 0; + right: 0; + min-width: 418px; + height: 100%; + z-index: 3; + background-color: ${({ theme }) => theme.color.gray1}; + box-shadow: 0px 2px 20px 4px rgba(0, 0, 0, 0.16); +`; + +const _EscapeWrapper = styled.div` + height: 24px; + cursor: pointer; +`; diff --git a/services/admin/src/hooks/usePointsApi.tsx b/services/admin/src/hooks/usePointsApi.tsx index 64ce1813..6cdc4447 100644 --- a/services/admin/src/hooks/usePointsApi.tsx +++ b/services/admin/src/hooks/usePointsApi.tsx @@ -41,7 +41,7 @@ export const useStudentPointHistory = ( onSuccess: (res) => { addStudentPointHistory(res?.point_histories); }, - enabled: isActive, + enabled: Boolean(student_id) && isActive, }, ); }; diff --git a/services/admin/src/hooks/useSideBar.ts b/services/admin/src/hooks/useSideBar.ts new file mode 100644 index 00000000..8d522112 --- /dev/null +++ b/services/admin/src/hooks/useSideBar.ts @@ -0,0 +1,9 @@ +import { removeCookies } from '@/utils/cookies'; +import { pagePath } from '@/utils/pagePath'; +import { useNavigate } from 'react-router-dom'; +import { useModal } from '@/hooks/useModal'; + +export const useSidebar = () => { + const navigate = useNavigate(); + const { closeModal } = useModal(); +}; diff --git a/services/admin/src/pages/Home.tsx b/services/admin/src/pages/Home.tsx index a7a6d735..cdd5c275 100644 --- a/services/admin/src/pages/Home.tsx +++ b/services/admin/src/pages/Home.tsx @@ -1,9 +1,8 @@ import styled from 'styled-components'; import { ChangeEvent, useState, useEffect } from 'react'; -import { Button, Change } from '@team-aliens/design-system'; +import { Button, Change, Text } from '@team-aliens/design-system'; import { StudentList } from '@/components/main/StudentList'; import { Divider } from '@/components/main/Divider'; -import { StudentDetail } from '@/components/main/DetailBox/StudentDetail'; import { WithNavigatorBar } from '@/components/WithNavigatorBar'; import { SortType } from '@/apis/managers'; import { useDebounce } from '@/hooks/useDebounce'; @@ -15,7 +14,14 @@ import { useStudentPointHistory } from '@/hooks/usePointsApi'; import { usePointHistoryList } from '@/hooks/usePointHistoryList'; import { TagType } from '@/apis/tags/response'; import { useAvailAbleFeatures } from '@/hooks/useSchoolsApi'; -import { useSelectedStudentIdStore } from '@/store/useSelectedStudentIdStore'; +import { + useClickedStudentIdStore, + useSelectedStudentIdStore, +} from '@/store/useSelectedStudentIdStore'; +import { SideBar } from '@/components/sidebar'; +import { DetailBox } from '@/components/main/DetailBox/DetailBox'; +import SideBarPortal from '@/components/sidebar/SideBarPortal'; +import { useModal } from '@/hooks/useModal'; export interface FilterState { name: string; @@ -66,8 +72,14 @@ export function Home() { }); const [listViewType, setListViewType] = useState('POINTS'); + const [clickedStudentId, resetClickedStudentId] = useClickedStudentIdStore( + (state) => [state.clickedStudentId, state.resetClickedStudentId], + ); + + const { modalState } = useModal(); + const { data: studentDetail, refetch: refetchStudentDetail } = - useStudentDetail(selectedStudentId[0]); + useStudentDetail(clickedStudentId); const { data: studentList, refetch: refetchSearchStudents } = useSearchStudents({ @@ -81,6 +93,9 @@ export function Home() { const { data: availableFeature } = useAvailAbleFeatures(); + const { data: studentPointHistory, refetch: refetchStudentPointHistory } = + useStudentPointHistory(clickedStudentId, availableFeature?.point_service); + const onChangeSortType = () => { const value: SortType = filter.sort === 'GCN' ? 'NAME' : 'GCN'; changeObjectValue('sort', value); @@ -146,26 +161,6 @@ export function Home() { return ( <_Wrapper> - {/* <_ModeButton - onClick={() => { - ChangeMode(); - resetStudentId(); - }} - Icon={} - > - {mode.text} - - {mode.type === 'POINTS' && availableFeature?.point_service && ( - <_PointListButton - onClick={() => { - ChangeListMode(); - }} - color="gray" - kind="outline" - > - {listViewType === 'POINTS' ? '전체 상/벌점 내역' : '학생 목록 보기'} - - )} */} {listViewType === 'POINTS' ? ( <> )} + + {clickedStudentId && ( + { + modalState.selectedModal === '' && resetClickedStudentId(); + }} + > + {clickedStudentId && ( + + )} + + )} + ); } const _Wrapper = styled.div` display: flex; - margin: 160px auto 0 auto; + margin: 70px auto 0 auto; overflow-y: scroll; `; diff --git a/services/admin/src/pages/apply/study/PatchRoom.tsx b/services/admin/src/pages/apply/study/PatchRoom.tsx index 40d03a64..0ee0354e 100644 --- a/services/admin/src/pages/apply/study/PatchRoom.tsx +++ b/services/admin/src/pages/apply/study/PatchRoom.tsx @@ -136,9 +136,9 @@ export const PatchRoom = () => { if (!west_description) changeErrorMessage('westDescription', '공백일 수 없습니다.'); if (!north_description) - changeErrorMessage('southDescription', '공백일 수 없습니다.'); - if (!south_description) changeErrorMessage('northDescription', '공백일 수 없습니다.'); + if (!south_description) + changeErrorMessage('southDescription', '공백일 수 없습니다.'); if ( floor && floor !== 0 && diff --git a/services/admin/src/store/useSelectedStudentIdStore.ts b/services/admin/src/store/useSelectedStudentIdStore.ts index 57bfab83..f34ca752 100644 --- a/services/admin/src/store/useSelectedStudentIdStore.ts +++ b/services/admin/src/store/useSelectedStudentIdStore.ts @@ -26,3 +26,23 @@ export const useSelectedStudentIdStore = create()( })), }), ); + +interface ClickedStudentIdState { + clickedStudentId: string; + resetClickedStudentId: () => void; + setClickedStudentId: (studentId: string) => void; +} + +export const useClickedStudentIdStore = create()( + (set) => ({ + clickedStudentId: '', + resetClickedStudentId: () => + set(() => ({ + clickedStudentId: '', + })), + setClickedStudentId: (studentId) => + set(() => ({ + clickedStudentId: studentId, + })), + }), +);