diff --git a/package.json b/package.json
index ec6ffe48..411cfa16 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
- "@sopt-makers/colors": "^2.2.0",
+ "@sopt-makers/colors": "^3.0.0",
+ "@sopt-makers/fonts": "^1.0.0",
"@types/axios": "^0.14.0",
"@types/node": "18.15.3",
"@types/react": "18.0.28",
diff --git a/public/favicon.ico b/public/favicon.ico
index 718d6fea..3a5553fe 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/src/assets/icons/IcAlarmMenu.svg b/src/assets/icons/IcAlarmMenu.svg
new file mode 100644
index 00000000..93f3dde6
--- /dev/null
+++ b/src/assets/icons/IcAlarmMenu.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/IcAttendanceMenu.svg b/src/assets/icons/IcAttendanceMenu.svg
new file mode 100644
index 00000000..93e1d120
--- /dev/null
+++ b/src/assets/icons/IcAttendanceMenu.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/IcDate.svg b/src/assets/icons/IcDate.svg
new file mode 100644
index 00000000..d30f45eb
--- /dev/null
+++ b/src/assets/icons/IcDate.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/icons/IcDeleteFile.svg b/src/assets/icons/IcDeleteFile.svg
new file mode 100644
index 00000000..90b1f22d
--- /dev/null
+++ b/src/assets/icons/IcDeleteFile.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/IcDropdownCheck.svg b/src/assets/icons/IcDropdownCheck.svg
new file mode 100644
index 00000000..e2f49cc5
--- /dev/null
+++ b/src/assets/icons/IcDropdownCheck.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/IcModalClose.svg b/src/assets/icons/IcModalClose.svg
index 7866f77f..e2daaac7 100644
--- a/src/assets/icons/IcModalClose.svg
+++ b/src/assets/icons/IcModalClose.svg
@@ -1,3 +1,4 @@
-
+
\ No newline at end of file
diff --git a/src/assets/icons/IcMore.svg b/src/assets/icons/IcMore.svg
new file mode 100644
index 00000000..41cf151b
--- /dev/null
+++ b/src/assets/icons/IcMore.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/icons/IcNavMenu.svg b/src/assets/icons/IcNavMenu.svg
deleted file mode 100644
index fefaaafb..00000000
--- a/src/assets/icons/IcNavMenu.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
diff --git a/src/assets/icons/IcNewDropdown.svg b/src/assets/icons/IcNewDropdown.svg
new file mode 100644
index 00000000..879c9903
--- /dev/null
+++ b/src/assets/icons/IcNewDropdown.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/IcOrgMenu.svg b/src/assets/icons/IcOrgMenu.svg
new file mode 100644
index 00000000..407d7e06
--- /dev/null
+++ b/src/assets/icons/IcOrgMenu.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/IcPlace.svg b/src/assets/icons/IcPlace.svg
new file mode 100644
index 00000000..e437d92d
--- /dev/null
+++ b/src/assets/icons/IcPlace.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/icons/IcUpload.svg b/src/assets/icons/IcUpload.svg
new file mode 100644
index 00000000..9d0cb1bd
--- /dev/null
+++ b/src/assets/icons/IcUpload.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/SoptLogos/DoSoptLogo.svg b/src/assets/icons/SoptLogos/DoSoptLogo.svg
new file mode 100644
index 00000000..2a0a1e03
--- /dev/null
+++ b/src/assets/icons/SoptLogos/DoSoptLogo.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icons/SoptLogos/GoSoptLogo.svg b/src/assets/icons/SoptLogos/GoSoptLogo.svg
new file mode 100644
index 00000000..6b3a8758
--- /dev/null
+++ b/src/assets/icons/SoptLogos/GoSoptLogo.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/SoptLogos/SoptMainLogo.svg b/src/assets/icons/SoptLogos/SoptMainLogo.svg
new file mode 100644
index 00000000..a4494ade
--- /dev/null
+++ b/src/assets/icons/SoptLogos/SoptMainLogo.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/SoptLogos/index.ts b/src/assets/icons/SoptLogos/index.ts
new file mode 100644
index 00000000..78400f8d
--- /dev/null
+++ b/src/assets/icons/SoptLogos/index.ts
@@ -0,0 +1,3 @@
+export { default as DoSoptLogo } from './DoSoptLogo.svg';
+export { default as GoSoptLogo } from './GoSoptLogo.svg';
+export { default as SoptMainLogo } from './SoptMainLogo.svg';
diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts
index 99aaf718..6eca17b6 100644
--- a/src/assets/icons/index.ts
+++ b/src/assets/icons/index.ts
@@ -1,4 +1,9 @@
+export { default as IcAlarmMenu } from './IcAlarmMenu.svg';
+export { default as IcAttendanceMenu } from './IcAttendanceMenu.svg';
export { default as IcCheckBox } from './IcCheckBox';
+export { default as IcDeleteFile } from './IcDeleteFile.svg';
export { default as IcGoPrev } from './IcGoPrev.svg';
export { default as IcModalClose } from './IcModalClose.svg';
-export { default as IcNavMenu } from './IcNavMenu.svg';
+export { default as IcNewDropdown } from './IcNewDropdown.svg';
+export { default as IcOrgMenu } from './IcOrgMenu.svg';
+export { default as IcUpload } from './IcUpload.svg';
diff --git a/src/components/alarmAdmin/AlarmList/index.tsx b/src/components/alarmAdmin/AlarmList/index.tsx
new file mode 100644
index 00000000..e45852d5
--- /dev/null
+++ b/src/components/alarmAdmin/AlarmList/index.tsx
@@ -0,0 +1,160 @@
+import dayjs from 'dayjs';
+import { useState } from 'react';
+import { UseQueryResult } from 'react-query';
+
+import IcMore from '@/assets/icons/IcMore.svg';
+import { StActionButton } from '@/components/attendanceAdmin/session/SessionList/style';
+import Chip from '@/components/common/Chip';
+import FilterButton from '@/components/common/FilterButton';
+import ListActionButton from '@/components/common/ListActionButton';
+import ListWrapper from '@/components/common/ListWrapper';
+import Loading from '@/components/common/Loading';
+import Modal from '@/components/common/modal';
+import { deleteAlarm, sendAlarm } from '@/services/api/alarm';
+
+import ShowAlarmModal from '../ShowAlarmModal';
+import { StListItem, StPageHeader } from './style';
+
+const sendStatusList: ALARM_STATUS[] = ['전체', '발송 전', '발송 후'];
+
+interface Props {
+ data: Alarm[];
+ refetch: () => void;
+}
+
+function AlarmList(props: Props) {
+ const { data, refetch } = props;
+
+ const [tab, setTab] = useState('전체');
+ const [activeDropdownId, setActiveDropdownId] = useState(null);
+ const [showAlarmDetail, setShowAlarmDetail] = useState(null);
+ const [isSending, setIsSending] = useState(false);
+
+ const onChangeTab = (value: ALARM_STATUS) => {
+ setTab(value);
+ };
+
+ const toggleDropdown = (e: React.MouseEvent, alarmId: number): void => {
+ e.stopPropagation();
+ if (activeDropdownId === alarmId) {
+ setActiveDropdownId(null);
+ } else {
+ setActiveDropdownId(alarmId);
+ }
+ };
+
+ const onSendAlarm = async (alarmId: number, title: string) => {
+ const response = window.confirm(`${title} 알림을 전송하시겠습니까?`);
+ if (response) {
+ setIsSending(true);
+ const result = await sendAlarm(alarmId);
+ setIsSending(false);
+ window.alert(result ? '전송에 성공했습니다' : '전송에 실패했습니다');
+ refetch();
+ }
+ };
+
+ const onShowAlarmDetail = (alarmId: number) => {
+ setShowAlarmDetail(alarmId);
+ };
+
+ const onCloseAlarmDetail = () => {
+ setShowAlarmDetail(null);
+ };
+
+ const handleDeleteAlarm = async (
+ e: React.MouseEvent,
+ alarmId: number,
+ ) => {
+ e.stopPropagation();
+ const response = window.confirm('알림을 삭제하시겠습니까?');
+ if (response) {
+ const result = await deleteAlarm(alarmId);
+ result && refetch();
+ }
+ };
+
+ const alarmList = data
+ ? data.filter((item) => (tab === '전체' ? true : item.status === tab))
+ : [];
+
+ return (
+ <>
+
+ 알림 관리
+
+ list={sendStatusList}
+ selected={tab}
+ onChange={onChangeTab}
+ />
+ 총 {alarmList.length}개
+
+
+
+ {alarmList.map((alarm) => (
+ onShowAlarmDetail(alarm.alarmId)}>
+
+ {alarm.status}
+
+
+
+
+ 발송 일자:{' '}
+ {alarm.sendAt
+ ? dayjs(alarm.sendAt).format('YYYY/MM/DD HH:mm')
+ : ''}
+
+
+ {alarm.content}
+
+ {
+ e.stopPropagation();
+ onSendAlarm(alarm.alarmId, alarm.title);
+ }}
+ disabled={alarm.status === '발송 후'}
+ />
+
+
+
toggleDropdown(e, alarm.alarmId)}>
+
+
+ {activeDropdownId === alarm.alarmId && (
+
handleDeleteAlarm(e, alarm.alarmId)}>
+
삭제하기
+
+ )}
+
+
+ ))}
+
+
+ {showAlarmDetail && (
+
+
+
+ )}
+
+ {isSending && }
+ >
+ );
+}
+
+export default AlarmList;
diff --git a/src/components/alarmAdmin/AlarmList/style.ts b/src/components/alarmAdmin/AlarmList/style.ts
new file mode 100644
index 00000000..e0365e89
--- /dev/null
+++ b/src/components/alarmAdmin/AlarmList/style.ts
@@ -0,0 +1,127 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+export const StPageHeader = styled.div`
+ h1 {
+ ${fonts.TITLE_32_SB}
+ color: ${colors.gray10};
+ margin-bottom: 41px;
+ }
+ p {
+ ${fonts.TITLE_16_SB}
+ color: ${colors.gray200};
+ margin-top: 55px;
+ margin-bottom: 18px;
+ }
+`;
+
+export const StListItem = styled.li`
+ display: flex;
+ align-items: center;
+ padding: 18px 30px 18px 33px;
+
+ .alarm-status {
+ ${fonts.BODY_14_M}
+ width: 52px;
+ height: 48px;
+ margin-right: 34px;
+ }
+ .before {
+ color: ${colors.error};
+ }
+ .after {
+ color: ${colors.information};
+ }
+ .alarm-info-wrap {
+ width: 290px;
+ margin-right: 36px;
+
+ & > div:first-of-type {
+ display: flex;
+ align-items: flex-start;
+ }
+ .alarm-title {
+ ${fonts.TITLE_18_SB}
+ color: ${colors.gray10};
+ max-width: 146px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-right: 15px;
+ margin-bottom: 4px;
+ }
+ .alarm-sent-at {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray400};
+ }
+ }
+ .alarm-content {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray100};
+ margin-right: 64px;
+ width: 331px;
+ height: 48px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ word-break: break-word;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ }
+ .alarm-send {
+ margin-right: 28px;
+ }
+ & > div:last-of-type {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ .delete_dropdown {
+ ${fonts.BODY_14_M}
+
+ position: absolute;
+ top: 100%;
+ right: 0.001%; // 요소의 왼쪽 경계를 부모의 중앙에 위치시킵니다.
+
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+
+ width: 9.3rem;
+
+ margin-top: 1rem;
+ padding: 0.8rem 0.7rem;
+
+ background-color: ${colors.gray700};
+ border-radius: 1rem;
+
+ animation: appearDropdown 0.6s;
+
+ & > p {
+ width: 100%;
+ height: 100%;
+
+ padding: 0.5rem 0.9rem;
+
+ color: ${colors.error};
+
+ border-radius: 0.6rem;
+
+ &:hover {
+ background-color: ${colors.gray600};
+ }
+ }
+
+ @keyframes appearDropdown {
+ from {
+ opacity: 0;
+ transform: translateY(-1rem);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0rem);
+ }
+ }
+ }
+ }
+`;
diff --git a/src/components/alarmAdmin/CreateAlarmModal/index.tsx b/src/components/alarmAdmin/CreateAlarmModal/index.tsx
new file mode 100644
index 00000000..2402a7e7
--- /dev/null
+++ b/src/components/alarmAdmin/CreateAlarmModal/index.tsx
@@ -0,0 +1,326 @@
+import { useEffect, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { IcDeleteFile, IcUpload } from '@/assets/icons';
+import Button from '@/components/common/Button';
+import DropDown from '@/components/common/DropDown';
+import Input from '@/components/common/Input';
+import Loading from '@/components/common/Loading';
+import ModalFooter from '@/components/common/modal/ModalFooter';
+import ModalHeader from '@/components/common/modal/ModalHeader';
+import OptionTemplate from '@/components/common/OptionTemplate';
+import Selector from '@/components/common/Selector';
+import { currentGenerationState } from '@/recoil/atom';
+import { postNewAlarm } from '@/services/api/alarm';
+import {
+ readPlaygroundId,
+ TARGET_GENERATION_LIST,
+ TARGET_USER_LIST,
+} from '@/utils/alarm';
+import { ACTIVITY_GENERATION } from '@/utils/generation';
+import { partList, partTranslator } from '@/utils/session';
+
+import {
+ StAlarmModalWrapper,
+ StAlarmTypeButton,
+ StCsvUploader,
+ StTextArea,
+} from './style';
+
+interface Props {
+ onClose: () => void;
+ alarmId?: number;
+}
+
+function CreateAlarmModal(props: Props) {
+ const { onClose, alarmId } = props;
+
+ const [selectedValue, setSelectedValue] = useState({
+ attribute: 'NOTICE',
+ part: '발송 파트',
+ isActive: true,
+ generationAt: parseInt(ACTIVITY_GENERATION),
+ targetList: null,
+ title: '',
+ content: '',
+ link: null,
+ });
+ const [dropdownVisibility, setDropdownVisibility] = useState({
+ part: false,
+ target: false,
+ generation: false,
+ targetSelector: false,
+ });
+ const [isActiveUser, setIsActiveUser] = useState('활동 회원');
+ const [uploadedFile, setUploadedFile] = useState(null);
+ const [selectedAlarmType, setSelectedAlarmType] = useState({
+ notice: true,
+ news: false,
+ });
+ const [isReadyToSubmit, setIsReadyToSubmit] = useState(true);
+ const currentGeneration = useRecoilValue(currentGenerationState);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (isActiveUser === '활동 회원') {
+ setSelectedValue((prev) => ({ ...prev, isActive: true }));
+ } else {
+ setSelectedValue((prev) => ({ ...prev, isActive: false }));
+ }
+ if (isActiveUser === '특정 유저 지정') {
+ setSelectedValue((prev) => ({
+ ...prev,
+ part: '발송 파트',
+ isActive: null,
+ }));
+ }
+ }, [isActiveUser]);
+
+ useEffect(() => {
+ if (
+ (selectedValue.part !== '발송 파트' &&
+ selectedValue.title !== '' &&
+ selectedValue.content !== '') ||
+ (uploadedFile !== null &&
+ isActiveUser === 'CSV 첨부' &&
+ selectedValue.content !== '' &&
+ selectedValue.title !== '')
+ ) {
+ setIsReadyToSubmit(false);
+ } else {
+ setIsReadyToSubmit(true);
+ }
+ }, [
+ isActiveUser,
+ selectedValue.content,
+ selectedValue.part,
+ selectedValue.title,
+ uploadedFile,
+ ]);
+
+ const handleSubmit = async () => {
+ setIsSubmitting(true);
+
+ let apiPartValue = selectedValue.part
+ ? partTranslator[selectedValue.part]
+ : null;
+ let apiIsActive = selectedValue.isActive;
+ let targetListValue = selectedValue.targetList;
+
+ if (isActiveUser === 'CSV 첨부') {
+ apiPartValue = null;
+ apiIsActive = null;
+ }
+
+ if (isActiveUser !== 'CSV 첨부') {
+ targetListValue = null;
+ }
+
+ const payload = {
+ ...selectedValue,
+ generation: parseInt(currentGeneration),
+ part: apiPartValue,
+ isActive: apiIsActive,
+ targetList: targetListValue,
+ };
+
+ await postNewAlarm(payload);
+ setIsSubmitting(false);
+ await onClose();
+ };
+
+ const toggleDropdown = (type: AlarmDropdownType) => {
+ setDropdownVisibility((prev) => ({ ...prev, [type]: !prev[type] }));
+ };
+
+ const handleCSVUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ try {
+ const userIds = await readPlaygroundId(file);
+ setUploadedFile(file);
+ setSelectedValue((prev) => ({ ...prev, targetList: userIds }));
+ } catch (error) {
+ console.error('파일을 읽는데 실패했습니다.', error);
+ }
+ }
+ };
+
+ const handleAlarmType = (type: string): void => {
+ if (type === 'NOTICE') {
+ setSelectedValue((prev) => ({
+ ...prev,
+ attribute: 'NOTICE',
+ }));
+ setSelectedAlarmType({ notice: true, news: false });
+ }
+ if (type === 'NEWS') {
+ setSelectedValue((prev) => ({
+ ...prev,
+ attribute: 'NEWS',
+ }));
+ setSelectedAlarmType({ notice: false, news: true });
+ }
+ };
+
+ if (isSubmitting) return ;
+ return (
+
+
+
+
+ handleAlarmType('NOTICE')}
+ isSelected={selectedAlarmType.notice}>
+ 공지
+
+ handleAlarmType('NEWS')}
+ isSelected={selectedAlarmType.news}>
+ 소식
+
+
+
+
+ toggleDropdown('target')}
+ isDisabledValue={false}
+ />
+ {dropdownVisibility.target && (
+ {
+ setIsActiveUser(value);
+ toggleDropdown('target');
+ }}
+ />
+ )}
+
+ {isActiveUser !== 'CSV 첨부' && (
+ <>
+
+ toggleDropdown('part')}
+ isDisabledValue={selectedValue.part === '발송 파트'}
+ />
+ {dropdownVisibility.part && (
+ {
+ setSelectedValue((prev) => ({
+ ...prev,
+ part: value,
+ }));
+ toggleDropdown('part');
+ }}
+ />
+ )}
+
+
+ toggleDropdown('generation')}
+ isDisabledValue={selectedValue.isActive == true}
+ />
+ {dropdownVisibility.generation && !selectedValue.isActive && (
+ !item.includes(ACTIVITY_GENERATION),
+ )}
+ onItemSelected={(value) => {
+ setSelectedValue((prev) => ({
+ ...prev,
+ generation: parseInt(value),
+ }));
+ toggleDropdown('generation');
+ }}
+ />
+ )}
+
+ >
+ )}
+
+
+ {isActiveUser === 'CSV 첨부' && (
+
+
+ {uploadedFile ? (
+
+ {uploadedFile.name}
+ setUploadedFile(null)} />
+
+ ) : (
+
+ document.getElementById('csvUploaderInput')?.click()
+ }>
+
+
+ 눌러서 첨부하기
+
+ )}
+
+
+ )}
+
+ {
+ setSelectedValue((prev) => ({
+ ...prev,
+ title: e.target.value,
+ }));
+ }}
+ />
+
+
+ {
+ setSelectedValue((prev) => ({
+ ...prev,
+ content: e.target.value,
+ }));
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CreateAlarmModal;
diff --git a/src/components/alarmAdmin/CreateAlarmModal/style.ts b/src/components/alarmAdmin/CreateAlarmModal/style.ts
new file mode 100644
index 00000000..49a99bd7
--- /dev/null
+++ b/src/components/alarmAdmin/CreateAlarmModal/style.ts
@@ -0,0 +1,143 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+
+export const StAlarmModalWrapper = styled.section`
+ width: 50.4rem;
+
+ & > main {
+ padding: 1.6rem 3rem 3.2rem 3rem;
+
+ & > .type_selector {
+ display: flex;
+ gap: 2rem;
+ }
+
+ & > .dropdowns {
+ display: flex;
+ gap: 1.6rem;
+ }
+
+ & > .inputs {
+ display: flex;
+ flex-direction: column;
+ align-self: stretch;
+ }
+ }
+ & > footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1.2rem;
+ }
+`;
+
+export const StAlarmTypeButton = styled.button<{
+ isSelected: boolean;
+ readOnly?: boolean;
+}>`
+ padding: 0.8rem 2rem;
+
+ border-radius: 11.8rem;
+
+ text-align: center;
+ font-size: 2rem;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+
+ color: ${({ isSelected }) => (isSelected ? colors.gray950 : colors.gray100)};
+
+ background: ${({ isSelected }) => (isSelected ? colors.gray10 : 'none')};
+
+ pointer-events: ${({ readOnly }) => (readOnly ? 'none' : 'auto')};
+
+ &:hover {
+ background: ${({ isSelected }) =>
+ isSelected ? colors.gray10 : colors.gray700};
+ }
+ &:active {
+ background: ${colors.gray600};
+ }
+`;
+
+export const StCsvUploader = styled.div`
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+
+ padding: 1rem 1.4rem;
+
+ font-size: 1.6rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 2.4rem; /* 150% */
+ letter-spacing: -0.032rem;
+
+ color: ${colors.gray400};
+ background-color: ${colors.gray700};
+
+ border-radius: 0.8rem;
+
+ cursor: pointer;
+
+ & > div.uploaded {
+ width: 100%;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ color: ${colors.gray10};
+
+ & > svg {
+ &:hover {
+ fill: ${colors.gray600};
+ }
+ &:active {
+ fill: ${colors.gray500};
+ }
+ }
+ }
+
+ & > div.pre_upload {
+ width: 100%;
+
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+
+ gap: 1rem;
+ }
+`;
+
+export const StTextArea = styled.textarea`
+ height: 12.8rem;
+
+ padding: 1rem 1.4rem;
+
+ font-size: 1.8rem;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 100%; /* 1.8rem */
+ letter-spacing: -0.018rem;
+
+ color: ${colors.gray10};
+ background-color: ${colors.gray700};
+ border: none;
+ outline: none;
+ resize: none;
+
+ border-radius: 0.8rem;
+
+ &::placeholder {
+ color: ${colors.gray400};
+ }
+
+ &:not(:read-only):focus {
+ background-color: ${colors.gray600};
+ outline: 0.1rem solid ${colors.gray300};
+ }
+ &:focus {
+ cursor: default;
+ }
+`;
diff --git a/src/components/alarmAdmin/ShowAlarmModal/index.tsx b/src/components/alarmAdmin/ShowAlarmModal/index.tsx
new file mode 100644
index 00000000..94678723
--- /dev/null
+++ b/src/components/alarmAdmin/ShowAlarmModal/index.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import Button from '@/components/common/Button';
+import Input from '@/components/common/Input';
+import ModalFooter from '@/components/common/modal/ModalFooter';
+import ModalHeader from '@/components/common/modal/ModalHeader';
+import OptionTemplate from '@/components/common/OptionTemplate';
+import Selector from '@/components/common/Selector';
+import { currentGenerationState } from '@/recoil/atom';
+import { getAlarm } from '@/services/api/alarm';
+
+import {
+ StAlarmModalWrapper,
+ StAlarmTypeButton,
+ StTextArea,
+} from '../CreateAlarmModal/style';
+
+interface Props {
+ onClose: () => void;
+ alarmId: number;
+}
+
+function ShowAlarmModal(props: Props) {
+ const { onClose, alarmId } = props;
+
+ const generation = useRecoilValue(currentGenerationState);
+
+ const [data, setData] = useState({
+ attribute: '',
+ part: null,
+ isActive: null,
+ title: '',
+ content: '',
+ link: null,
+ createdAt: '',
+ sendAt: '',
+ });
+
+ useEffect(() => {
+ (async () => {
+ const alarmData = await getAlarm(alarmId);
+ setData(alarmData);
+ })();
+ }, [alarmId]);
+
+ const activeStatus = useMemo(() => {
+ switch (data.isActive) {
+ case true:
+ return '활동 회원';
+ case false:
+ return '비활동 회원';
+ default:
+ return 'CSV 첨부';
+ }
+ }, [data.isActive]);
+
+ return (
+
+
+
+
+
+ 공지
+
+
+ 소식
+
+
+
+
+
+
+
+ {activeStatus !== 'CSV 첨부' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <>>
+
+
+ );
+}
+
+export default ShowAlarmModal;
diff --git a/src/components/attendanceAdmin/session/AttendanceModal/index.tsx b/src/components/attendanceAdmin/session/AttendanceModal/index.tsx
index 21649fbe..70fb873b 100644
--- a/src/components/attendanceAdmin/session/AttendanceModal/index.tsx
+++ b/src/components/attendanceAdmin/session/AttendanceModal/index.tsx
@@ -2,11 +2,15 @@ import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import Button from '@/components/common/Button';
+import Loading from '@/components/common/Loading';
+import ModalFooter from '@/components/common/modal/ModalFooter';
+import ModalHeader from '@/components/common/modal/ModalHeader';
import { startAttendance } from '@/services/api/lecture';
import { precision } from '@/utils';
import { StAttendanceModal } from './style';
+type Status = 'LOADING' | 'STARTED' | 'FINISHED';
interface Props {
round: number;
lectureId: number;
@@ -21,18 +25,29 @@ function AttendanceModal(props: Props) {
const [timer, setTimer] = useState({ minutes: MINUTES, seconds: SECONDS });
const [code, setCode] = useState('');
- const [isFinished, setIsFinished] = useState(false);
+ const [status, setStatus] = useState('LOADING');
useEffect(() => {
+ const createCode = () => {
+ const randomNum = Math.floor(Math.random() * 99999 + 1) + '';
+ return '0'.repeat(5 - randomNum.length) + randomNum;
+ };
const code = createCode();
setCode(code);
(async () => {
const isStarted = await startAttendance(code, lectureId, round);
- if (!isStarted) {
+ if (isStarted) {
+ setStatus('STARTED');
+ } else {
alert('출석이 정상적으로 시작되지 않았어요');
+ finishAttendance();
}
})();
+ }, [lectureId, round, finishAttendance]);
+
+ useEffect(() => {
+ if (status !== 'STARTED') return;
const startedAt = dayjs();
@@ -44,7 +59,7 @@ function AttendanceModal(props: Props) {
if (elapsedMinutes >= MINUTES) {
clearInterval(id);
- setIsFinished(true);
+ setStatus('FINISHED');
setTimer({ minutes: 0, seconds: 0 });
} else {
setTimer({
@@ -55,16 +70,10 @@ function AttendanceModal(props: Props) {
}, 1000);
return () => clearInterval(id);
- }, []);
+ }, [status]);
- const createCode = () => {
- const code = Math.floor(Math.random() * 99999 + 1) + '';
- const codeLength = code.length;
- return '0'.repeat(5 - codeLength) + code;
- };
-
- const onCloseModal = () => {
- if (isFinished) {
+ const handleCloseModal = () => {
+ if (status === 'FINISHED') {
finishAttendance();
} else {
const confirmed = confirm('출석을 조기 종료하시겠어요?');
@@ -72,13 +81,15 @@ function AttendanceModal(props: Props) {
}
};
+ if (status === 'LOADING') return ;
return (
+
-
-
{round}차 출석하기
-
출석 코드 다섯 자리를 랜덤 생성합니다.
-
{precision(timer.minutes, 2)}:{precision(timer.seconds, 2)}
@@ -90,13 +101,13 @@ function AttendanceModal(props: Props) {
))}
-
+
출석을 정상적으로 종료하기 전에 창을 닫거나 이동하지 마세요!
출석이 제대로 기록되지 않을 수 있어요.
-
-
+
+
);
}
diff --git a/src/components/attendanceAdmin/session/AttendanceModal/style.ts b/src/components/attendanceAdmin/session/AttendanceModal/style.ts
index defbcb01..02bea258 100644
--- a/src/components/attendanceAdmin/session/AttendanceModal/style.ts
+++ b/src/components/attendanceAdmin/session/AttendanceModal/style.ts
@@ -1,68 +1,58 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
export const StAttendanceModal = styled.div`
& > div {
+ width: 90rem;
padding: 3.2rem 4rem 0 4rem;
}
- .modal-header {
- margin-bottom: 4rem;
- h3 {
- font-size: 2.4rem;
- font-weight: 700;
- line-height: 140%;
- margin-bottom: 0.8rem;
- }
- p {
- font-size: 1.8rem;
- line-height: 140%;
- color: ${({ theme }) => theme.color.grayscale.gray80};
- }
- }
.timer {
text-align: center;
font-size: 4.8rem;
+ font-style: normal;
font-weight: 600;
- line-height: 140%;
- color: ${({ theme }) => theme.color.grayscale.black40};
+ line-height: 140%; /* 6.72rem */
+ letter-spacing: -0.096rem;
+ color: ${colors.gray10};
margin-bottom: 2rem;
&-warn {
- color: ${({ theme }) => theme.color.sub.red};
+ color: ${colors.error};
}
}
.code-wrapper {
display: flex;
justify-content: center;
- gap: 2.4rem;
+ gap: 1rem;
margin-bottom: 5.6rem;
& > div {
width: 8.2rem;
height: 11.2rem;
border-radius: 0.8rem;
- border: 2px solid ${({ theme }) => theme.color.grayscale.black40};
- background-color: ${({ theme }) => theme.color.grayscale.gray10};
+ background-color: ${colors.gray700};
display: flex;
justify-content: center;
align-items: center;
& > p {
- color: ${({ theme }) => theme.color.grayscale.black80};
+ color: ${colors.gray10};
+ text-align: center;
+ font-feature-settings: 'clig' off, 'liga' off;
+ font-family: SUIT;
font-size: 4rem;
+ font-style: normal;
font-weight: 700;
+ line-height: 160%; /* 6.4rem */
+ letter-spacing: -0.08rem;
}
}
}
- .modal-footer {
- padding: 2.6rem 4rem;
- background: ${({ theme }) => theme.color.grayscale.gray20};
- border-bottom-left-radius: 1.2rem;
- border-bottom-right-radius: 1.2rem;
+ & > footer {
display: flex;
justify-content: space-between;
align-items: center;
p {
- font-size: 1.4rem;
- font-weight: 600;
- line-height: 160%;
- color: ${({ theme }) => theme.color.sub.red};
+ ${fonts.TITLE_14_SB}
+ color: ${colors.error};
}
}
`;
diff --git a/src/components/attendanceAdmin/session/CreateSessionModal/index.tsx b/src/components/attendanceAdmin/session/CreateSessionModal/index.tsx
index d51a57ce..40d9de6e 100644
--- a/src/components/attendanceAdmin/session/CreateSessionModal/index.tsx
+++ b/src/components/attendanceAdmin/session/CreateSessionModal/index.tsx
@@ -6,34 +6,25 @@ import React from 'react';
import DatePicker from 'react-datepicker';
import { useRecoilValue } from 'recoil';
-import { IcCheckBox, IcModalClose } from '@/assets/icons';
import Button from '@/components/common/Button';
import DropDown from '@/components/common/DropDown';
import IcDropdown from '@/components/common/icons/IcDropDown';
-import InputContainer from '@/components/common/inputContainer';
+import Input from '@/components/common/Input';
+import ModalFooter from '@/components/common/modal/ModalFooter';
+import ModalHeader from '@/components/common/modal/ModalHeader';
+import OptionTemplate from '@/components/common/OptionTemplate';
+import Selector from '@/components/common/Selector';
import { useCreateSession } from '@/hooks/useCreateSession';
import { currentGenerationState } from '@/recoil/atom';
import {
+ attributeList,
partList,
partTranslator,
sessionTranslator,
- sessionType,
times,
} from '@/utils/session';
-import {
- StDatePickerInput,
- StDropDownInput,
- StFooter,
- StHeader,
- StInformationSection,
- StInput,
- StPartSelector,
- StSelectedPart,
- StSessionSelector,
- StTitle,
- StWrapper,
-} from './style';
+import { StDatePickerInput, StSessionModalWrapper } from './style';
interface Props {
onClose: () => void;
@@ -43,16 +34,17 @@ function CreateSessionModal(props: Props) {
const { onClose } = props;
// 세션 생성에 필요한 State
- const [part, setPart] = useState('파트선택');
+ const [part, setPart] = useState('파트 선택');
const [sessionName, setSessionName] = useState();
const [sessionLocation, setSessionLocation] = useState();
const [date, setDate] = useState();
const [startTime, setStartTime] = useState('14:00');
const [endTime, setEndTime] = useState('18:00');
- const [selectedSessionIndex, setSelectedSessionIndex] = useState(-1);
+ const [attribute, setAttribute] = useState('세션 선택');
const [selectedDate, setSelectedDate] = useState(null);
const [isPartOpen, setIsPartOpen] = useState(false);
+ const [isAttributeOpen, setIsAttributeOpen] = useState(false);
const [isStartTimeOpen, setIsStartTimeOpen] = useState(false);
const [isEndTimeOpen, setIsEndTimeOpen] = useState(false);
@@ -71,21 +63,13 @@ function CreateSessionModal(props: Props) {
date &&
startTime &&
endTime &&
- selectedSessionIndex !== -1
+ attribute !== '세션 선택'
) {
setButtonDisabled(false);
} else {
setButtonDisabled(true);
}
- }, [
- part,
- sessionName,
- sessionLocation,
- date,
- startTime,
- endTime,
- selectedSessionIndex,
- ]);
+ }, [part, sessionName, sessionLocation, date, startTime, endTime, attribute]);
/** 각각의 State 에 담아준 상태들을 객체화 시켜 post 하는 함수 */
const handleSubmit = async () => {
@@ -93,8 +77,7 @@ function CreateSessionModal(props: Props) {
setButtonClicked(true);
const translatedPart = partTranslator[part];
- const translatedAttribute =
- sessionTranslator[sessionType[selectedSessionIndex].session];
+ const translatedAttribute = sessionTranslator[attribute];
const submitContents = {
part: translatedPart,
@@ -117,6 +100,11 @@ function CreateSessionModal(props: Props) {
setIsPartOpen(false);
};
+ const handleSelectedAttribute = (selectedAttribute: string): void => {
+ setAttribute(selectedAttribute);
+ setIsAttributeOpen(false);
+ };
+
/** 시간 선택 핸들러 */
const handleSelectedTime = (time: string, timeType: string) => {
if (timeType === 'startTime') {
@@ -154,22 +142,34 @@ function CreateSessionModal(props: Props) {
};
return (
- <>
-
-
-
- 세션 생성
-
-
- 새로운 SOPT 세션을 생성합니다. 대상 파트를 선택해주세요.
-
-
- setIsPartOpen(!isPartOpen)}>
-
- {part}
-
-
+
+
+
+
+
+ setIsAttributeOpen(!isAttributeOpen)}
+ isDisabledValue={attribute === '세션 선택'}
+ />
+ {isAttributeOpen && (
+
+ )}
+
+
+ setIsPartOpen(!isPartOpen)}
+ isDisabledValue={part === '파트 선택'}
+ />
{isPartOpen && (
)}
-
-
-
- handleSessionInfo(e, '세션 이름')}
+
+
+
+
+ handleSessionInfo(e, '세션 이름')}
+ />
+
+
+ handleSessionInfo(e, '세션 장소')}
+ />
+
+
+
+
+
+
+
+
+
+
+ setIsStartTimeOpen(!isStartTimeOpen)}
/>
-
-
- handleSessionInfo(e, '세션 장소')}
+ {isStartTimeOpen && (
+
+ handleSelectedTime(time, 'startTime')
+ }
+ />
+ )}
+
+
+ setIsEndTimeOpen(!isEndTimeOpen)}
/>
-
+ {isEndTimeOpen && (
+
+ handleSelectedTime(time, 'endTime')
+ }
+ />
+ )}
+
-
-
-
-
-
-
-
-
-
-
- setIsStartTimeOpen(!isStartTimeOpen)}>
-
- {startTime}
-
-
- {isStartTimeOpen && (
-
- handleSelectedTime(time, 'startTime')
- }
- />
- )}
-
-
- setIsEndTimeOpen(!isEndTimeOpen)}>
-
- {endTime}
-
-
- {isEndTimeOpen && (
-
- handleSelectedTime(time, 'endTime')
- }
- />
- )}
-
-
-
-
-
-
-
- {sessionType.map((type, index) => (
-
-
- setSelectedSessionIndex(
- selectedSessionIndex === index ? -1 : index,
- )
- }
- />
-
-
- ))}
-
-
-
-
-
-
- >
+
+
+
+
+
+
);
}
diff --git a/src/components/attendanceAdmin/session/CreateSessionModal/style.ts b/src/components/attendanceAdmin/session/CreateSessionModal/style.ts
index 1963c70b..15b74205 100644
--- a/src/components/attendanceAdmin/session/CreateSessionModal/style.ts
+++ b/src/components/attendanceAdmin/session/CreateSessionModal/style.ts
@@ -1,37 +1,37 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
-import { display1 } from '@/styles/fonts';
+export const StSessionModalWrapper = styled.section`
+ width: 46rem;
-export const StWrapper = styled.div`
- padding: 0 4rem;
-`;
-
-export const StTitle = styled.div`
- display: flex;
- justify-content: space-between;
- align-items: center;
+ & > main {
+ padding: 1.6rem 3rem 3.2rem 3rem;
- padding-top: 3.2rem;
- margin-bottom: 0.8rem;
+ & > .dropdowns {
+ display: flex;
+ gap: 1.6rem;
+ }
- & > h1 {
- ${display1}
- color: ${({ theme }) => theme.color.grayscale.black40};
- }
+ & > .inputs {
+ display: flex;
+ flex-direction: column;
+ align-self: stretch;
- & > svg {
- margin-top: 1rem;
+ & > .time {
+ display: flex;
+ gap: 1.6rem;
- cursor: pointer;
+ & > div {
+ flex: 1;
+ }
+ }
+ }
}
-`;
-export const StHeader = styled.header`
- & > h2 {
- font-weight: 400;
- font-size: 1.6rem;
- line-height: 140%;
- letter-spacing: -0.02em;
- color: ${({ theme }) => theme.color.grayscale.gray80};
+
+ & > footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1.2rem;
}
`;
@@ -60,71 +60,6 @@ export const StSelectedPart = styled.span<{ textColor: string }>`
color: ${({ textColor }) => textColor};
`;
-export const StInformationSection = styled.section`
- display: flex;
- flex-direction: column;
- width: 100%;
- padding: 3.2rem 0 5.6rem 0;
-
- & > p {
- font-weight: 600;
- font-size: 2rem;
- line-height: 140%;
- letter-spacing: -0.02em;
-
- color: ${({ theme }) => theme.color.main.purple100};
- }
-
- & > div {
- display: flex;
- gap: 1.6rem;
- width: 100%;
-
- padding-top: 2rem;
-
- & > div.time {
- display: flex;
- flex: row;
- gap: 1.6rem;
-
- width: 100%;
-
- & > div {
- position: relative;
-
- width: 100%;
- }
- }
- }
-`;
-
-export const StInput = styled.input<{ hasValue?: boolean }>`
- width: 100%;
- height: auto;
-
- padding: 1rem 1.4rem;
- border: none;
-
- outline: ${({ hasValue, theme }) =>
- hasValue
- ? `1px solid ${theme.color.grayscale.black40}`
- : `1px solid ${theme.color.grayscale.gray30}`};
- box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
- border-radius: 8px;
-
- font-weight: 400;
- font-size: 16px;
- line-height: 24px;
- letter-spacing: -0.02em;
-
- &::placeholder {
- color: ${({ theme }) => theme.color.grayscale.gray30};
- }
- &:focus {
- outline: ${({ theme }) => theme.color.grayscale.black40} solid 1px;
- }
-`;
-
export const StDatePickerInput = styled.div<{ hasValue?: boolean }>`
width: 100%;
height: auto;
@@ -143,30 +78,32 @@ export const StDatePickerInput = styled.div<{ hasValue?: boolean }>`
padding: 1rem 1.4rem;
- color: ${({ theme }) => theme.color.grayscale.black40};
- font-weight: 500;
+ color: ${colors.gray10};
+ background-color: ${colors.gray700};
+
font-size: 1.6rem;
- line-height: 2.4rem;
- letter-spacing: -0.02em;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 165%; /* 2.64rem */
+ letter-spacing: -0.024rem;
- outline: ${({ hasValue, theme }) =>
- hasValue
- ? `1px solid ${theme.color.grayscale.black40}`
- : `1px solid ${theme.color.grayscale.gray30}`};
+ outline: none;
border: none;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 8px;
&::placeholder {
- color: ${({ theme }) => theme.color.grayscale.gray30};
+ color: ${colors.gray400};
- font-weight: 500;
font-size: 1.6rem;
- line-height: 2.4rem;
- letter-spacing: -0.02em;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 165%; /* 2.64rem */
+ letter-spacing: -0.024rem;
}
&:focus {
- outline: ${({ theme }) => theme.color.grayscale.black40} solid 1px;
+ background-color: ${colors.gray600};
+ outline: 0.1rem solid ${colors.gray300};
}
}
}
@@ -176,76 +113,3 @@ export const StDatePickerInput = styled.div<{ hasValue?: boolean }>`
width: auto;
}
`;
-
-export const StDropDownInput = styled.div<{ hasValue?: boolean }>`
- width: 100%;
- height: auto;
-
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-
- width: 100%;
- padding: 1rem 1.4rem;
-
- color: ${({ theme }) => theme.color.grayscale.black40};
- outline: 1px solid ${({ theme }) => theme.color.grayscale.black40};
- border-radius: 0.8rem;
-
- font-weight: 500;
- font-size: 1.6rem;
- line-height: 2.4rem;
- letter-spacing: -0.02em;
-
- &::placeholder {
- color: ${({ theme }) => theme.color.grayscale.gray30};
- }
-`;
-
-export const StFooter = styled.footer`
- display: flex;
- justify-content: space-between;
-
- padding: 2.9rem 4rem;
-
- background-color: ${({ theme }) => theme.color.grayscale.gray20};
-
- border-radius: 0 0 1.2rem 1.2rem;
-
- & > article {
- display: flex;
- gap: 1.2rem;
- }
-`;
-
-export const StSessionSelector = styled.article`
- display: flex;
- gap: 1.6rem;
-
- & > label {
- display: flex;
- flex-direction: column;
- gap: 0.8rem;
-
- & > h3 {
- font-weight: 500;
- font-size: 16px;
- line-height: 100%;
- letter-spacing: -0.02em;
-
- color: ${({ theme }) => theme.color.grayscale.black40};
- }
- & > p {
- font-weight: 400;
- font-size: 14px;
- line-height: 100%;
- letter-spacing: -0.02em;
-
- color: ${({ theme }) => theme.color.grayscale.gray60};
- }
- &:hover {
- cursor: pointer;
- }
- }
-`;
diff --git a/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/index.tsx b/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/index.tsx
index e8b995f4..ac20e55e 100644
--- a/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/index.tsx
+++ b/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/index.tsx
@@ -32,7 +32,7 @@ const SessionDetailModal = (props: Props) => {
);
if (isConfirmed) {
deleteSession(lectureId);
- alert('세션이 삭제되었습니다.');
+ alert('세션이 삭제되었어요');
onClose();
}
};
diff --git a/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/style.ts b/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/style.ts
index 71e88b60..1ad92561 100644
--- a/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/style.ts
+++ b/src/components/attendanceAdmin/session/SessionList/SessionDetailModal/style.ts
@@ -1,7 +1,5 @@
import styled from '@emotion/styled';
-import { display1 } from '@/styles/fonts';
-
export const StWrapper = styled.div`
padding: 0 4rem;
`;
@@ -25,7 +23,9 @@ export const StTitle = styled.div`
margin-bottom: 0.8rem;
& > h1 {
- ${display1}
+ font-size: 2.4rem;
+ line-height: 3rem;
+ font-weight: 700;
color: ${({ theme }) => theme.color.grayscale.black40};
}
diff --git a/src/components/attendanceAdmin/session/SessionList/index.tsx b/src/components/attendanceAdmin/session/SessionList/index.tsx
index f11d87c8..5283339f 100644
--- a/src/components/attendanceAdmin/session/SessionList/index.tsx
+++ b/src/components/attendanceAdmin/session/SessionList/index.tsx
@@ -3,59 +3,29 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
-import ListActionButton from '@/components/common/ListActionButton';
+import IcDate from '@/assets/icons/IcDate.svg';
+import IcMore from '@/assets/icons/IcMore.svg';
+import IcPlace from '@/assets/icons/IcPlace.svg';
+import Chip from '@/components/common/Chip';
import ListWrapper from '@/components/common/ListWrapper';
import Loading from '@/components/common/Loading';
-import Modal from '@/components/common/modal';
import PartFilter from '@/components/common/PartFilter';
import { currentGenerationState } from '@/recoil/atom';
+import { deleteSession } from '@/services/api/lecture';
import { useGetSessionList } from '@/services/api/lecture/query';
-import { precision } from '@/utils';
-import { partTranslator } from '@/utils/translator';
+import { allPartTranslator } from '@/utils/translator';
-import SessionDetailModal from './SessionDetailModal';
-import {
- StListHeader,
- StPartIndicator,
- StSessionIndicator,
- StSessionName,
- StTbody,
-} from './style';
+import { StActionButton, StListHeader, StListItem } from './style';
function SessionList() {
const router = useRouter();
- const HEADER_LABELS = [
- '순번',
- '파트',
- '세션',
- '세션명',
- '날짜',
- '출석',
- '지각',
- '결석',
- '미정',
- '관리',
- ];
-
- const TABLE_WIDTH = [
- '11%',
- '6.25%',
- '6.25%',
- '24%',
- '14%',
- '6.5%',
- '6.5%',
- '6.5%',
- '6.5%',
- '14.5%',
- ];
-
const [selectedPart, setSelectedPart] = useState('ALL');
const [lectureData, setLectureData] = useState([]);
const [isDetailOpen, setIsDetailOpen] = useState(false);
const [selectedLecture, setSelectedLecture] = useState(0);
const currentGeneration = useRecoilValue(currentGenerationState);
+ const [activeDropdownId, setActiveDropdownId] = useState(null);
const { data, isLoading, isError, error } = useGetSessionList(
parseInt(currentGeneration),
@@ -71,83 +41,120 @@ function SessionList() {
}
}, [data, error, isError, router]);
- const handleManageClick = (lectureId: number) => {
+ const handleManageClick = (lectureId: number): void => {
router.push(`/attendanceAdmin/session/${lectureId}`);
};
- const onChangePart = (part: PART) => {
+ const onChangePart = (part: PART): void => {
setSelectedPart(part);
};
+ const toggleDropdown = (e: React.MouseEvent, lectureId: number): void => {
+ e.stopPropagation();
+ if (activeDropdownId === lectureId) {
+ setActiveDropdownId(null);
+ } else {
+ setActiveDropdownId(lectureId);
+ }
+ };
+
+ const handleDeleteSession = (
+ e: React.MouseEvent,
+ lectureId: number,
+ ): void => {
+ e.stopPropagation();
+ const isConfirmed = confirm(
+ '세션을 삭제하면 복구할 수 없습니다.\n정말 삭제할까요?',
+ );
+ if (isConfirmed) {
+ deleteSession(lectureId);
+ alert('세션이 삭제되었어요');
+ }
+ };
+
return (
<>
출석 세션
-
+
+ 총 {lectureData.length}개
-
-
-
- {HEADER_LABELS.map((label) => (
- {label} |
- ))}
-
-
-
- {lectureData?.map((lecture, index) => {
- const {
- lectureId,
- partValue,
- attributeName,
- name,
- startDate,
- attendances,
- } = lecture;
- const { attendance, tardy, absent, unknown } = attendances;
- const part = partTranslator[partValue] || partValue;
- const date = dayjs(startDate);
- const formattedDate = date.format('YYYY/MM/DD');
- return (
- handleManageClick(lectureId)}>
- {precision(index + 1, 2)} |
-
- {part}
- |
-
-
- {attributeName}
-
- |
-
- {name}
- |
- {formattedDate} |
- {attendance} |
- {tardy} |
- {absent} |
- {unknown} |
-
- {
- setSelectedLecture(lectureId);
- setIsDetailOpen(true);
- }}
- />
- |
-
- );
- })}
-
+
+ {lectureData?.map((lecture) => {
+ const {
+ lectureId,
+ partValue,
+ attributeName,
+ name,
+ startDate,
+ endDate,
+ attendances,
+ place,
+ } = lecture;
+ const { attendance, tardy, absent, unknown } = attendances;
+ const part = allPartTranslator[partValue] || partValue;
+ const formattedStartDate = dayjs(startDate).format(
+ 'YYYY년 MM월 DD일 HH:mm',
+ );
+ const formattedEndDate = dayjs(endDate).format('HH:mm');
+
+ return (
+ handleManageClick(lectureId)}>
+
+
+
+
+ 출석{attendance}
+
+
+ 지각{tardy}
+
+
+ 결석{absent}
+
+
+ 미정{unknown}
+
+
+
+
+
+
+
+ {formattedStartDate} - {formattedEndDate}
+
+
+
+ {place}
+
+
+
+
toggleDropdown(e, lectureId)}>
+
+
+ {activeDropdownId === lectureId && (
+
handleDeleteSession(e, lectureId)}>
+
삭제하기
+
+ )}
+
+
+
+ );
+ })}
- {isDetailOpen && (
-
- setIsDetailOpen(!isDetailOpen)}
- lectureId={selectedLecture}
- />
-
- )}
{isLoading && }
>
);
diff --git a/src/components/attendanceAdmin/session/SessionList/style.ts b/src/components/attendanceAdmin/session/SessionList/style.ts
index ecedfb74..46e29155 100644
--- a/src/components/attendanceAdmin/session/SessionList/style.ts
+++ b/src/components/attendanceAdmin/session/SessionList/style.ts
@@ -1,67 +1,123 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
export const StListHeader = styled.header`
+ h1 {
+ ${fonts.TITLE_32_SB}
+ color: ${colors.gray10};
+ margin-bottom: 41px;
+ }
+ & > p {
+ ${fonts.TITLE_16_SB}
+ color: ${colors.gray200};
+ margin-top: 55px;
+ margin-bottom: 18px;
+ }
+`;
+
+export const StListItem = styled.li`
+ padding: 18px 22px 18px 32px;
display: flex;
justify-content: space-between;
- margin-bottom: 5rem;
-
- & > h1 {
- font-weight: 600;
- font-size: 20px;
- line-height: 25px;
- letter-spacing: -0.02em;
- color: ${({ theme }) => theme.color.grayscale.black60};
+
+ .left-top {
+ display: flex;
+ align-items: center;
+ margin-bottom: 7px;
+
+ & > p:first-of-type {
+ ${fonts.TITLE_20_SB}
+ color: ${colors.gray10};
+ margin-right: 15px;
+ }
}
-`;
+ .left-bottom {
+ display: flex;
+ align-items: center;
+ gap: 14px;
-export const StTbody = styled.tbody`
- cursor: pointer;
+ p {
+ ${fonts.LABEL_12_SB}
+ color: ${colors.gray500};
- & > tr {
- &:hover {
- & > td {
- border-top: 1px solid ${({ theme }) => theme.color.grayscale.black40};
- border-bottom: 1px solid ${({ theme }) => theme.color.grayscale.black40};
- }
- & > td:first-of-type {
- border: 1px solid ${({ theme }) => theme.color.grayscale.black40};
- border-right: none;
- }
- & > td:last-of-type {
- border: 1px solid ${({ theme }) => theme.color.grayscale.black40};
- border-left: none;
+ span {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray300};
+ margin-left: 6px;
}
}
}
-`;
+ .right {
+ ${fonts.BODY_14_M}
+ display: flex;
+ align-items: center;
+ gap: 55px;
-export const IndicatorStructure = styled.span`
- display: inline-block;
- min-width: 4.9rem;
- padding: 0.4rem 0.6rem;
- text-align: center;
- vertical-align: middle;
- color: ${({ theme }) => theme.color.grayscale.white100};
- border-radius: 0.4rem;
-`;
+ & > div:first-of-type {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
-export const StPartIndicator = styled(IndicatorStructure)`
- background-color: ${({ theme }) => theme.color.grayscale.gray100};
-`;
+ p {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ }
+
+ & > div:last-of-type {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+
+ & > div.delete_dropdown {
+ position: absolute;
+ top: 100%;
+ right: 0.001%; // 요소의 왼쪽 경계를 부모의 중앙에 위치시킵니다.
+
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
-export const StSessionIndicator = styled(IndicatorStructure)<{
- attributeName: string;
-}>`
- color: ${({ theme, attributeName }) =>
- attributeName === '기타'
- ? theme.color.grayscale.black60
- : theme.color.grayscale.white100};
- background-color: ${({ theme, attributeName }) =>
- attributeName === '세미나'
- ? theme.color.grayscale.black100
- : attributeName === '행사'
- ? theme.color.grayscale.black40
- : theme.color.grayscale.gray40};
+ width: 9.3rem;
+
+ margin-top: 1rem;
+ padding: 0.8rem 0.7rem;
+
+ background-color: ${colors.gray700};
+ border-radius: 1rem;
+
+ animation: appearDropdown 0.6s;
+
+ & > p {
+ width: 100%;
+ height: 100%;
+
+ padding: 0.5rem 0.9rem;
+
+ color: ${colors.error};
+
+ border-radius: 0.6rem;
+
+ &:hover {
+ background-color: ${colors.gray600};
+ }
+ }
+
+ @keyframes appearDropdown {
+ from {
+ opacity: 0;
+ transform: translateY(-1rem);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0rem);
+ }
+ }
+ }
+ }
+ }
`;
export const StSessionName = styled.p`
@@ -71,3 +127,15 @@ export const StSessionName = styled.p`
white-space: nowrap;
margin: 0 auto;
`;
+
+export const StActionButton = styled.button`
+ position: relative;
+
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+
+ &:hover {
+ background-color: ${colors.gray600};
+ }
+`;
diff --git a/src/components/attendanceAdmin/session/SessionListFooter/index.tsx b/src/components/attendanceAdmin/session/SessionListFooter/index.tsx
index d84c40f3..d3c29690 100644
--- a/src/components/attendanceAdmin/session/SessionListFooter/index.tsx
+++ b/src/components/attendanceAdmin/session/SessionListFooter/index.tsx
@@ -2,7 +2,7 @@ import { useRecoilValue } from 'recoil';
import Button from '@/components/common/Button';
import { currentGenerationState } from '@/recoil/atom';
-import { ACTIVITY_GENRATION } from '@/utils/generation';
+import { ACTIVITY_GENERATION } from '@/utils/generation';
import { StFooterWrapper } from './style';
@@ -19,7 +19,7 @@ function SessionListFooter(props: Props) {
diff --git a/src/components/attendanceAdmin/totalScore/MemberDetail/index.tsx b/src/components/attendanceAdmin/totalScore/MemberDetail/index.tsx
index 55b927f2..aeb3a0a8 100644
--- a/src/components/attendanceAdmin/totalScore/MemberDetail/index.tsx
+++ b/src/components/attendanceAdmin/totalScore/MemberDetail/index.tsx
@@ -2,33 +2,21 @@ import dayjs from 'dayjs';
import { useEffect } from 'react';
import { IcModalClose } from '@/assets/icons';
+import AttendanceChip from '@/components/common/AttendanceChip';
import ListWrapper from '@/components/common/ListWrapper';
import Loading from '@/components/common/Loading';
import Modal from '@/components/common/modal';
import { scoreDetailAttendanceInit } from '@/data/sessionData';
import { useGetMemberAttendance } from '@/services/api/attendance/query';
-import { precision } from '@/utils';
-import { getAttendanceColor } from '@/utils/translator';
+import { addPlus } from '@/utils';
-import { StModalWrap, StSessionName } from './style';
+import { StListItem, StModalWrap, StSessionName } from './style';
interface Props {
memberId: number;
onClose: () => void;
}
-const HEADER_LABELS = [
- '순번',
- '세션명',
- '1차 출석 상태',
- '1차 출석 일시',
- '2차 출석 상태',
- '2차 출석 일시',
- '출석 결과',
- '점수',
-];
-const TABLE_WIDTH = ['10%', '20%', '10%', '15%', '10%', '15%', '10%', '10%'];
-
function MemberDetail(props: Props) {
const { memberId, onClose } = props;
@@ -58,86 +46,73 @@ function MemberDetail(props: Props) {
-
-
-
-
- {HEADER_LABELS.map((label, index) => (
-
- {label}
- |
- ))}
-
-
-
-
-
+
-
- {member.lectures.map((lecture, index) => {
- const firstRound =
- lecture.attendances.find((item) => item.round === 1) ??
- scoreDetailAttendanceInit;
- const secondRound =
- lecture.attendances.find((item) => item.round === 2) ??
- scoreDetailAttendanceInit;
- const firstRoundDate = dayjs(firstRound.date).format(
- 'YYYY/MM/DD HH:mm',
- );
- const secondRoundDate = dayjs(secondRound.date).format(
- 'YYYY/MM/DD HH:mm',
- );
- return (
-
-
- {precision(index + 1, 2)}
- |
-
+ {member.lectures.map((lecture) => {
+ const firstRound =
+ lecture.attendances.find((item) => item.round === 1) ??
+ scoreDetailAttendanceInit;
+ const secondRound =
+ lecture.attendances.find((item) => item.round === 2) ??
+ scoreDetailAttendanceInit;
+ const firstRoundDate = dayjs(firstRound.date).format(
+ 'YYYY/MM/DD HH:mm',
+ );
+ const secondRoundDate = dayjs(secondRound.date).format(
+ 'YYYY/MM/DD HH:mm',
+ );
+ return (
+
+ |
-
- {firstRound.status}
- |
- {firstRoundDate} |
-
- {secondRound.status}
- |
- {secondRoundDate} |
-
- {lecture.status}
- |
-
- {lecture.additiveScore}점
- |
-
- );
- })}
-
+
+
+
+
+ 2023년 00월 00일 14:00 - 18:00
+
+
+
+
+
+ 1차 {firstRound.status}
+
+ {firstRoundDate}
+
+
+
+ 2차 {secondRound.status}
+
+ {secondRoundDate}
+
+
+
+ );
+ })}
+
);
diff --git a/src/components/attendanceAdmin/totalScore/MemberDetail/style.ts b/src/components/attendanceAdmin/totalScore/MemberDetail/style.ts
index af8e2474..6f540736 100644
--- a/src/components/attendanceAdmin/totalScore/MemberDetail/style.ts
+++ b/src/components/attendanceAdmin/totalScore/MemberDetail/style.ts
@@ -1,51 +1,43 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
export const StModalWrap = styled.div`
- padding: 0 4rem 4rem 4rem;
- button.close-btn {
- position: absolute;
- top: 0;
- right: 0;
- width: 4.4rem;
- height: 4.4rem;
- background: none;
- margin: 3.2rem 3.2rem 0 0;
- }
+ padding: 41px 34px 30px 34px;
+ display: flex;
+ align-items: flex-start;
+
header {
- margin-top: 3.2rem;
- display: flex;
- flex-direction: column;
- gap: 0.8rem;
- margin-bottom: 4rem;
- p {
- font-size: 1.6rem;
- line-height: 2.2rem;
- color: ${({ theme }) => theme.color.grayscale.gray80};
- }
.member-name {
- font-size: 2.4rem;
- font-weight: 700;
- line-height: 3.4rem;
- color: ${({ theme }) => theme.color.grayscale.black80};
- span {
- color: ${({ theme }) => theme.color.main.orange50};
- margin-left: 0.8rem;
- }
+ ${fonts.TITLE_28_SB}
+ color: ${colors.gray10};
+ }
+ .member-score {
+ ${fonts.TITLE_28_SB}
+ color: ${colors.orange400};
+ margin-bottom: 16px;
+ }
+ .chip {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray100};
+ background-color: ${colors.gray700};
+ padding: 5px 8px;
+ border-radius: 4px;
+ margin-bottom: 7px;
+ width: fit-content;
}
}
- .list-head > table {
- border-spacing: 0;
- }
- .list-body {
- width: calc(100% + 8px);
- height: 36rem;
+ .score-list {
+ width: 654px;
+ height: 510px;
overflow-y: scroll;
- padding-right: 5px;
+ margin: 0 27px 0 40px;
+
&::-webkit-scrollbar {
width: 3px;
}
&::-webkit-scrollbar-thumb {
- background: ${({ theme }) => theme.color.grayscale.gray30};
+ background: ${colors.gray500};
border-radius: 3px;
}
&::-webkit-scrollbar-track {
@@ -55,9 +47,49 @@ export const StModalWrap = styled.div`
`;
export const StSessionName = styled.p`
- max-width: 15rem;
+ ${fonts.TITLE_18_SB}
+ color: ${colors.gray10};
+ max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- margin: 0 auto;
+ margin-right: 12px;
+`;
+
+export const StListItem = styled.li`
+ pointer-events: none;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 33px;
+ border: 1px solid ${colors.gray700} !important;
+
+ .session-score {
+ display: flex;
+ align-items: center;
+ margin-bottom: 4px;
+ }
+ .session-date {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray300};
+ }
+ .attendance-info {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray300};
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ .attendance {
+ color: ${colors.gray100};
+ margin-right: 15px;
+ }
+ .absent {
+ color: ${colors.error};
+ }
+ span:not(.attendance) {
+ display: inline-block;
+ width: 122px;
+ }
+ }
`;
diff --git a/src/components/attendanceAdmin/totalScore/MemberList/index.tsx b/src/components/attendanceAdmin/totalScore/MemberList/index.tsx
index 52d4581c..ff29aa8a 100644
--- a/src/components/attendanceAdmin/totalScore/MemberList/index.tsx
+++ b/src/components/attendanceAdmin/totalScore/MemberList/index.tsx
@@ -1,7 +1,7 @@
import { RefObject, useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
-import ListActionButton from '@/components/common/ListActionButton';
+import Chip from '@/components/common/Chip';
import ListWrapper from '@/components/common/ListWrapper';
import Loading from '@/components/common/Loading';
import PartFilter from '@/components/common/PartFilter';
@@ -13,35 +13,9 @@ import { precision } from '@/utils';
import { getPartValue, partTranslator } from '@/utils/session';
import MemberDetail from '../MemberDetail';
-import { StListHeader, StMemberName, StMemberUniversity } from './style';
+import { StListItem, StPageHeader } from './style';
function MemberList() {
- const HEADER_LABELS = [
- '순번',
- '회원명',
- '학교명',
- '파트명',
- '총점',
- '출석',
- '지각',
- '결석',
- '참여',
- '관리',
- ];
-
- const TABLE_WIDTH = [
- '10%',
- '13.5%',
- '13.5%',
- '12%',
- '12%',
- '5%',
- '5%',
- '5%',
- '5%',
- '12%',
- ];
-
const [selectedPart, setSelectedPart] = useState('ALL');
const [selectedMember, setSelectedMember] = useState(
null,
@@ -65,7 +39,7 @@ function MemberList() {
setSelectedPart(part);
};
- const onChangeMember = (member: ScoreMember) => {
+ const onShowMemberDetail = (member: ScoreMember) => {
setSelectedMember(member);
};
@@ -75,53 +49,64 @@ function MemberList() {
return (
<>
-
+
출석 총점
-
-
-
-
- {HEADER_LABELS.map((label) => (
- {label} |
- ))}
-
-
-
- {members?.pages.map(
- (pageMembers, pageIndex) =>
- pageMembers &&
- pageMembers.map((member, index) => {
- const { part, name, university, score, total } = member;
- const { attendance, tardy, absent, participate } = total;
- const partName = getPartValue(partTranslator, part) || part;
-
- return (
-
- {precision(pageIndex * PAGE_SIZE + index + 1, 2)} |
-
- {name}
- |
-
- {university}
- |
- {partName} |
- {score} |
- {attendance} |
- {tardy} |
- {absent} |
- {participate} |
-
- onChangeMember(member)}
- />
- |
-
- );
- }),
- )}
-
+ 총 0명
+
+
+ {members?.pages.map(
+ (pageMembers, pageIndex) =>
+ pageMembers &&
+ pageMembers.map((member, index) => {
+ const { part, name, university, score, total } = member;
+ const { attendance, tardy, absent, participate } = total;
+ const partName = getPartValue(partTranslator, part) || part;
+
+ return (
+ onShowMemberDetail(member)}>
+
+
+ {precision(pageIndex * PAGE_SIZE + index + 1, 2)}
+
+
+
+
+
+ 출석
+ {attendance}
+
+
+ 지각
+ {tardy}
+
+
+ 결석
+ {absent}
+
+
+ 미정
+ {participate}
+
+
+ {score}점
+
+
+
+ );
+ }),
+ )}
diff --git a/src/components/attendanceAdmin/totalScore/MemberList/style.ts b/src/components/attendanceAdmin/totalScore/MemberList/style.ts
index 0fee02ab..21b15fb2 100644
--- a/src/components/attendanceAdmin/totalScore/MemberList/style.ts
+++ b/src/components/attendanceAdmin/totalScore/MemberList/style.ts
@@ -1,31 +1,74 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
-export const StListHeader = styled.header`
+export const StPageHeader = styled.header`
+ h1 {
+ ${fonts.TITLE_32_SB}
+ color: ${colors.gray10};
+ margin-bottom: 41px;
+ }
+ p {
+ ${fonts.TITLE_16_SB}
+ color: ${colors.gray200};
+ margin: 56px 0 18px 12px;
+ }
+`;
+
+export const StListItem = styled.li`
+ color: ${colors.gray100};
display: flex;
justify-content: space-between;
- margin-bottom: 5rem;
+ padding: 18px 43px 18px 33px;
- & > h1 {
- font-weight: 600;
- font-size: 20px;
- line-height: 25px;
- letter-spacing: -0.02em;
- color: ${({ theme }) => theme.color.grayscale.black60};
+ .member-info-wrap {
+ display: flex;
+
+ .index {
+ ${fonts.BODY_14_M}
+ width: 26px;
+ margin-right: 33px;
+ }
+ .member-info > div:first-of-type {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ }
+ .member-name {
+ ${fonts.TITLE_18_SB}
+ color: ${colors.gray30};
+ margin-bottom: 4px;
+ }
+ .member-university {
+ ${fonts.BODY_14_M}
+ color: ${colors.gray400};
+ }
}
-`;
+ .member-score-wrap {
+ display: flex;
+ align-items: center;
-export const StMemberName = styled.p`
- max-width: 7.3rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin: 0 auto;
-`;
+ .attendance {
+ ${fonts.BODY_16_M}
+ color: ${colors.gray100};
+ margin-right: 38px;
-export const StMemberUniversity = styled.p`
- max-width: 9.5rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin: 0 auto;
+ span {
+ color: ${colors.gray500};
+ margin-right: 10px;
+ }
+ }
+ .member-score {
+ ${fonts.BODY_16_M}
+ color: ${colors.gray50};
+ background-color: ${colors.gray700};
+ padding: 5px 13px;
+ border-radius: 30px;
+ width: fit-content;
+ margin: 0 auto;
+ }
+ .minus-score {
+ color: ${colors.error};
+ }
+ }
`;
diff --git a/src/components/common/AttendanceChip/index.tsx b/src/components/common/AttendanceChip/index.tsx
new file mode 100644
index 00000000..e5f2bfa1
--- /dev/null
+++ b/src/components/common/AttendanceChip/index.tsx
@@ -0,0 +1,13 @@
+import { StAttendanceChip } from './style';
+
+interface Props {
+ text: string;
+}
+
+function AttendanceChip(props: Props) {
+ const { text } = props;
+
+ return {text};
+}
+
+export default AttendanceChip;
diff --git a/src/components/common/AttendanceChip/style.ts b/src/components/common/AttendanceChip/style.ts
new file mode 100644
index 00000000..95f5f228
--- /dev/null
+++ b/src/components/common/AttendanceChip/style.ts
@@ -0,0 +1,55 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+export const IndicatorStructure = styled.span`
+ ${fonts.BODY_14_M}
+ display: inline-block;
+ color: ${colors.gray10};
+ border-radius: 8px;
+ padding: 2px 11px;
+ margin-right: 6px;
+`;
+
+export const StAttendanceChip = styled(IndicatorStructure)<{ text: string }>`
+ ${({ text }) => getChipColor(text)}
+`;
+
+const getChipColor = (text: string) => {
+ switch (text) {
+ case '출석':
+ case '참여':
+ return css`
+ background-color: ${colors.green900};
+ color: ${colors.information};
+ `;
+ case '결석':
+ return css`
+ background-color: ${colors.red800};
+ color: ${colors.red300};
+ `;
+ case '지각':
+ return css`
+ background-color: ${colors.yellow900};
+ color: ${colors.attention};
+ `;
+ case '미참여':
+ return css`
+ background-color: ${colors.gray600};
+ color: ${colors.gray200};
+ `;
+ default:
+ if (text.includes('-')) {
+ return css`
+ background-color: ${colors.red800};
+ color: ${colors.red300};
+ `;
+ }
+ return css`
+ background-color: ${colors.gray600};
+ color: ${colors.gray10};
+ padding: 2px 8px;
+ `;
+ }
+};
diff --git a/src/components/common/Button/style.ts b/src/components/common/Button/style.ts
index c20f32a6..f008717a 100644
--- a/src/components/common/Button/style.ts
+++ b/src/components/common/Button/style.ts
@@ -1,5 +1,6 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
import { Props } from './index';
@@ -9,28 +10,29 @@ export const StButton = styled.button>`
font-size: 1.6rem;
line-height: 1;
font-weight: 600;
- border-radius: 4rem;
+ padding: 1.6rem 2.4rem;
+ border-radius: 1rem;
&:disabled {
- background-color: ${({ theme }) => theme.color.grayscale.gray30};
- border: 1px solid ${({ theme }) => theme.color.grayscale.gray30};
- color: ${({ theme }) => theme.color.grayscale.white100};
+ background-color: ${colors.gray600};
+ color: ${colors.gray400};
cursor: default;
}
+ &:hover {
+ background-color: ${colors.gray600};
+ }
${({ theme, type }) =>
type === 'button'
? css`
- background-color: ${theme.color.grayscale.white100};
- border: 1px solid ${theme.color.grayscale.gray30};
- color: ${theme.color.grayscale.gray60};
+ background: none;
+ color: ${colors.gray200};
`
: type === 'submit'
? css`
- background-color: ${theme.color.grayscale.black100};
- border: 1px solid ${theme.color.grayscale.black100};
- color: ${theme.color.grayscale.white100};
+ background-color: ${colors.white};
+ color: ${colors.black};
`
: css`
- background-color: ${theme.color.grayscale.gray30};
+ background: none;
color: ${theme.color.grayscale.white100};
`};
`;
diff --git a/src/components/common/Chip/index.tsx b/src/components/common/Chip/index.tsx
new file mode 100644
index 00000000..4cf5564f
--- /dev/null
+++ b/src/components/common/Chip/index.tsx
@@ -0,0 +1,13 @@
+import { StChip } from './style';
+
+interface Props {
+ text: string;
+}
+
+function Chip(props: Props) {
+ const { text } = props;
+
+ return {text};
+}
+
+export default Chip;
diff --git a/src/components/common/Chip/style.ts b/src/components/common/Chip/style.ts
new file mode 100644
index 00000000..d9c5399f
--- /dev/null
+++ b/src/components/common/Chip/style.ts
@@ -0,0 +1,65 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+export const IndicatorStructure = styled.span`
+ ${fonts.LABEL_12_SB}
+ display: inline-block;
+ color: ${colors.gray200};
+ border: 1px solid ${colors.gray500};
+ border-radius: 20px;
+ padding: 3.5px 8px;
+ margin-right: 10px;
+`;
+
+export const StSessionIndicator = styled(IndicatorStructure)<{
+ attributeName: string;
+}>`
+ ${({ attributeName }) =>
+ attributeName === '세미나'
+ ? css`
+ border-color: ${colors.orange600};
+ color: ${colors.orange600};
+ `
+ : attributeName === '행사'
+ ? css`
+ border-color: ${colors.blue400};
+ color: ${colors.blue400};
+ `
+ : css`
+ border-color: ${colors.yellow700};
+ color: ${colors.yellow700};
+ `}
+`;
+
+export const StChip = styled(IndicatorStructure)<{ text: string }>`
+ ${({ text }) => getChipColor(text)}
+`;
+
+const getChipColor = (text: string) => {
+ switch (text) {
+ case '세미나':
+ case '공지':
+ return css`
+ border-color: ${colors.orange600};
+ color: ${colors.orange600};
+ `;
+ case '행사':
+ case '소식':
+ return css`
+ border-color: ${colors.blue400};
+ color: ${colors.blue400};
+ `;
+ case '기타':
+ return css`
+ border-color: ${colors.yellow700};
+ color: ${colors.yellow700};
+ `;
+ default:
+ return css`
+ border-color: ${colors.gray500};
+ color: ${colors.gray200};
+ `;
+ }
+};
diff --git a/src/components/common/DropDown/style.ts b/src/components/common/DropDown/style.ts
index c7ef8b06..f26ab526 100644
--- a/src/components/common/DropDown/style.ts
+++ b/src/components/common/DropDown/style.ts
@@ -1,26 +1,24 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
import { Props } from './index';
export const DropdownWrapper = styled.div>`
position: absolute;
- width: 10rem;
+ top: 100%;
+
+ width: 100%;
+ min-width: 11rem;
height: auto;
- max-height: 32.1rem;
+ margin-top: 1rem;
+ padding: 0.8rem 0.7rem;
- box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.1);
+ box-shadow: 0px 5px 20px 0px rgba(63, 64, 66, 0.15);
border-radius: 1.3rem;
- ${({ type }) =>
- type === 'times' &&
- css`
- margin-left: 9.3rem;
- overflow: scroll;
- `}
-
- background: ${({ theme }) => theme.color.grayscale.white100};
+ background-color: ${colors.gray500};
z-index: 1;
@@ -28,23 +26,41 @@ export const DropdownWrapper = styled.div>`
& > div {
display: flex;
flex-direction: column;
- gap: 1.5rem;
+ gap: 0.4rem;
+
+ max-height: auto;
+
+ ${({ type }) =>
+ type === 'times' &&
+ css`
+ max-height: 20.2rem;
+ overflow-y: scroll; // 세로 스크롤만 허용
+ overflow-x: hidden; // 가로 스크롤 숨기기
+
+ ::-webkit-scrollbar {
+ width: 0.6rem;
+ }
- padding: 0.7rem;
+ ::-webkit-scrollbar-thumb {
+ background-color: ${colors.gray300};
+ border-radius: 0.8rem;
+ }
+ `}
& > p {
- padding: 0.75rem 0 0.75rem 0.5rem;
+ padding: 0.5rem 0.9rem;
- color: ${({ theme }) => theme.color.grayscale.black60};
+ color: ${colors.gray10};
+ font-size: 1.6rem;
+ font-style: normal;
font-weight: 500;
- font-size: 16px;
- line-height: 100%;
- letter-spacing: -0.01em;
+ line-height: 100%; /* 1.6rem */
+ letter-spacing: -0.016rem;
border-radius: 0.6rem;
&:hover {
- background-color: ${({ theme }) => theme.color.grayscale.gray20};
+ background-color: ${colors.gray400};
cursor: pointer;
}
diff --git a/src/components/common/FilterButton/index.tsx b/src/components/common/FilterButton/index.tsx
index dfe47e90..07fe3506 100644
--- a/src/components/common/FilterButton/index.tsx
+++ b/src/components/common/FilterButton/index.tsx
@@ -1,11 +1,12 @@
import {
FilterButtonItem,
- StWrapper,
+ FilterWrapper,
+ StUnderline,
} from '@/components/common/FilterButton/style';
interface Props {
list: T[];
- translator: Record;
+ translator?: Record;
onChange: (item: T) => void;
selected: T;
}
@@ -13,16 +14,19 @@ interface Props {
function FilterButton(props: Props) {
const { list, translator, selected, onChange } = props;
return (
-
- {list.map((item: T) => (
- onChange(item)}>
- {translator[item]}
-
- ))}
-
+ <>
+
+ {list.map((item: T) => (
+ onChange(item)}>
+ {translator ? translator[item] : item}
+
+ ))}
+
+
+ >
);
}
diff --git a/src/components/common/FilterButton/style.ts b/src/components/common/FilterButton/style.ts
index 331a8ec5..f7f42832 100644
--- a/src/components/common/FilterButton/style.ts
+++ b/src/components/common/FilterButton/style.ts
@@ -1,29 +1,36 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
-export const StWrapper = styled.div`
- padding: 0.8rem 1rem;
- background-color: ${({ theme }) => theme.color.grayscale.gray10};
- border-radius: 4rem;
- width: fit-content;
- box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.02);
+export const FilterWrapper = styled.div`
+ display: flex;
+ gap: 26px;
+`;
+export const StUnderline = styled.div`
+ width: calc(100vw - 212px);
+ margin-left: calc((100vw - 212px - 100%) / -2);
+ height: 1px;
+ background-color: ${colors.gray800};
`;
export const FilterButtonItem = styled.button<{ selected: boolean }>`
- font-size: 1.4rem;
- line-height: 1.7rem;
- padding: 1.1rem 3.6rem;
- border-radius: 4rem;
+ ${fonts.TITLE_20_SB}
+
+ padding-bottom: 13px;
transition: all 0.2s;
- ${({ theme, selected }) =>
+
+ ${({ selected }) =>
selected
? css`
- font-weight: 600;
- color: ${theme.color.grayscale.gray10};
- background-color: ${theme.color.grayscale.black60};
+ color: ${colors.gray30};
+ border-bottom: 3px solid ${colors.gray30};
`
: css`
- font-weight: 400;
- color: ${theme.color.grayscale.gray80};
- background-color: ${theme.color.grayscale.gray10};
+ color: ${colors.gray400};
+ border-bottom: 3px solid transparent;
`}
+
+ &:hover {
+ color: ${colors.gray100};
+ }
`;
diff --git a/src/components/common/FloatingButton/index.tsx b/src/components/common/FloatingButton/index.tsx
new file mode 100644
index 00000000..052edc77
--- /dev/null
+++ b/src/components/common/FloatingButton/index.tsx
@@ -0,0 +1,16 @@
+import { ReactNode } from 'react';
+
+import { StFloatingButton } from './style';
+
+interface Props {
+ content: ReactNode;
+ onClick: () => void;
+}
+
+function FloatingButton(props: Props) {
+ const { content, onClick } = props;
+
+ return {content};
+}
+
+export default FloatingButton;
diff --git a/src/components/common/FloatingButton/style.ts b/src/components/common/FloatingButton/style.ts
new file mode 100644
index 00000000..63492c21
--- /dev/null
+++ b/src/components/common/FloatingButton/style.ts
@@ -0,0 +1,17 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+import zIndex from '@/utils/zIndex';
+
+export const StFloatingButton = styled.button`
+ position: fixed;
+ right: 60px;
+ bottom: 60px;
+ padding: 14px 30px;
+ border-radius: 60px;
+ z-index: ${zIndex.footer};
+ background-color: ${colors.gray10};
+ color: ${colors.gray900};
+ ${fonts.TITLE_20_SB}
+`;
diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx
index b4aa74bb..11b76bc9 100644
--- a/src/components/common/Header/index.tsx
+++ b/src/components/common/Header/index.tsx
@@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
-import { IcGoPrev } from '@/assets/icons';
+import AdminStatusDevtools from '@/components/devTools/AdminStatus';
import { destroyToken } from '@/utils/auth';
import { StHeader } from './style';
@@ -15,11 +15,13 @@ function Header() {
return (
-
-
diff --git a/src/components/common/Header/style.ts b/src/components/common/Header/style.ts
index dbf0f016..ff8826cf 100644
--- a/src/components/common/Header/style.ts
+++ b/src/components/common/Header/style.ts
@@ -1,10 +1,11 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
import zIndex from '@/utils/zIndex';
export const StHeader = styled.header`
display: flex;
- justify-content: space-between;
+ justify-content: flex-end;
align-items: center;
position: fixed;
@@ -12,44 +13,41 @@ export const StHeader = styled.header`
top: 0;
left: 22rem;
width: calc(100% - 22rem);
- height: 6rem;
+ height: 8rem;
padding: 0 2.6rem;
- background-color: ${({ theme }) => theme.color.grayscale.gray10};
+ div.status_devtools {
+ width: fit-content;
+ padding: 2rem;
+ }
button {
- display: flex;
- align-items: center;
- gap: 0.9rem;
- background: none;
+ width: 10.2rem;
+ padding: 0.3rem 0.6rem;
+
+ color: ${colors.gray200};
+ background-color: ${colors.gray800};
+
+ border-radius: 1.9rem;
cursor: pointer;
- & > p {
- font-weight: 400;
- font-size: 1.6rem;
- line-height: 100%;
- letter-spacing: -0.02em;
- color: ${({ theme }) => theme.color.grayscale.black40};
+ &:hover {
+ color: ${colors.gray10};
+ background-color: ${colors.gray700};
+ }
+ &:active {
+ background-color: ${colors.gray600};
}
- &.logout > p {
- padding: 1.1rem 2.3rem;
- font-weight: 600;
- font-size: 12px;
- line-height: 100%;
+ & > p {
text-align: center;
- letter-spacing: -0.02em;
-
- background: ${({ theme }) => theme.color.grayscale.white100};
- color: ${({ theme }) => theme.color.grayscale.gray60};
- border: 0.1rem solid ${({ theme }) => theme.color.grayscale.gray20};
- border-radius: 4rem;
- }
- &.logout > p:hover {
- color: ${({ theme }) => theme.color.grayscale.black40};
- background: ${({ theme }) => theme.color.grayscale.gray20};
- border: 0.1rem solid ${({ theme }) => theme.color.grayscale.gray60};
+ font-family: SUIT;
+ font-size: 1.6rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 150%; /* 2.4rem */
+ letter-spacing: -0.024rem;
}
}
`;
diff --git a/src/components/common/Input/index.tsx b/src/components/common/Input/index.tsx
new file mode 100644
index 00000000..a60eb393
--- /dev/null
+++ b/src/components/common/Input/index.tsx
@@ -0,0 +1,27 @@
+import { ChangeEvent, ChangeEventHandler } from 'react';
+
+import { StInput } from './style';
+
+interface Props {
+ type: string;
+ placeholder?: string;
+ value?: string;
+ readOnly?: boolean;
+ onChange?: (event: ChangeEvent) => void;
+}
+
+function Input(props: Props) {
+ const { type, placeholder, value, readOnly = false, onChange } = props;
+
+ return (
+
+ );
+}
+
+export default Input;
diff --git a/src/components/common/Input/style.ts b/src/components/common/Input/style.ts
new file mode 100644
index 00000000..bdb43b72
--- /dev/null
+++ b/src/components/common/Input/style.ts
@@ -0,0 +1,28 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+export const StInput = styled.input`
+ padding: 1rem 1.4rem;
+
+ ${fonts.LABEL_18_SB}
+
+ color: ${colors.gray10};
+ background-color: ${colors.gray700};
+ border: none;
+ outline: none;
+
+ border-radius: 0.8rem;
+
+ &::placeholder {
+ color: ${colors.gray400};
+ }
+
+ &:not(:read-only):focus {
+ background-color: ${colors.gray600};
+ outline: 0.1rem solid ${colors.gray300};
+ }
+ &:read-only {
+ cursor: default;
+ }
+`;
diff --git a/src/components/common/Layout/style.ts b/src/components/common/Layout/style.ts
index aa3c8299..4dfea1f0 100644
--- a/src/components/common/Layout/style.ts
+++ b/src/components/common/Layout/style.ts
@@ -5,7 +5,7 @@ export const StLayout = styled.div`
display: flex;
.main-wrapper {
width: 100%;
- padding: 10rem 0 0 22rem;
+ padding: 88px 0 0 212px;
main {
max-width: 98rem;
width: 100%;
diff --git a/src/components/common/ListActionButton/style.ts b/src/components/common/ListActionButton/style.ts
index 62a3d9a1..45f08b2f 100644
--- a/src/components/common/ListActionButton/style.ts
+++ b/src/components/common/ListActionButton/style.ts
@@ -1,27 +1,27 @@
import styled from '@emotion/styled';
-
-import { body2 } from '@/styles/fonts';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
export const StButton = styled.button`
- ${body2}
+ ${fonts.BODY_14_M}
+ color: ${colors.gray10};
transition: transform 0.1s;
+
display: inline-block;
- padding: 0.6rem 1rem;
- border: 0.1rem solid ${({ theme }) => theme.color.grayscale.gray60};
- border-radius: 1.6rem;
- background-color: ${({ theme }) => theme.color.grayscale.gray20};
- color: ${({ theme }) => theme.color.grayscale.black40};
+ padding: 6px 10px;
+ border: 1px solid ${colors.gray300};
+ border-radius: 8px;
+ background-color: ${colors.gray600};
+ cursor: pointer;
&:disabled {
- background-color: ${({ theme }) => theme.color.grayscale.gray10};
- border: 1px solid ${({ theme }) => theme.color.grayscale.gray20};
- color: ${({ theme }) => theme.color.grayscale.gray30};
+ color: ${colors.gray500};
+ background-color: ${colors.gray800};
+ border-color: ${colors.gray800};
cursor: default;
}
- cursor: pointer;
&:not(:disabled):hover {
transform: scale(1.15);
- border-color: ${({ theme }) => theme.color.grayscale.gray100};
}
`;
diff --git a/src/components/common/ListWrapper/index.tsx b/src/components/common/ListWrapper/index.tsx
index 91a931cf..b4f71182 100644
--- a/src/components/common/ListWrapper/index.tsx
+++ b/src/components/common/ListWrapper/index.tsx
@@ -3,19 +3,13 @@ import { ReactNode } from 'react';
import { StList } from './style';
interface Props {
- tableWidth?: string[]; // % 단위
children: ReactNode;
}
-/**
- * 리스트에 스타일 입혀주는 Wrapper Component
- * @param children thead는 th를 tr로 감싸고, tbody는 td를 tr로 감싸야함
- * @returns 리스트에 스타일을 입혀서 반환
- */
function ListWrapper(props: Props) {
- const { children, tableWidth = [] } = props;
+ const { children } = props;
- return {children};
+ return {children};
}
export default ListWrapper;
diff --git a/src/components/common/ListWrapper/style.ts b/src/components/common/ListWrapper/style.ts
index 78db8b89..75b95bc0 100644
--- a/src/components/common/ListWrapper/style.ts
+++ b/src/components/common/ListWrapper/style.ts
@@ -1,58 +1,31 @@
import styled from '@emotion/styled';
-import css from 'styled-jsx/css';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
-import { body2, caption1 } from '@/styles/fonts';
-
-export const StList = styled.table<{ tableWidth: string[] }>`
+export const StList = styled.ul`
width: 100%;
- border-collapse: separate;
- border-spacing: 0 1.6rem;
- thead > tr {
- ${caption1}
- font-weight: 400;
- color: ${({ theme }) => theme.color.grayscale.black40};
- opacity: 0.7;
- & > th {
- padding-bottom: 2.4rem;
- }
- ${({ tableWidth }) =>
- tableWidth &&
- tableWidth
- .map(
- (width, index) => `th:nth-of-type(${index + 1}) { width: ${width} }`,
- )
- .join('')}
- }
- tbody > tr {
- ${body2}
- height: 7rem;
- color: ${({ theme }) => theme.color.grayscale.black40};
- & > td {
- height: inherit;
- text-align: center;
- vertical-align: middle;
- background-color: ${({ theme }) => theme.color.grayscale.white100};
- border-top: 0.5px solid ${({ theme }) => theme.color.grayscale.gray30};
- border-bottom: 0.5px solid ${({ theme }) => theme.color.grayscale.gray30};
- }
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-bottom: 120px;
- & > td:first-of-type {
- border-top-left-radius: 1rem;
- border-bottom-left-radius: 1rem;
- border: 0.5px solid ${({ theme }) => theme.color.grayscale.gray30};
- border-right: none;
+ & > li {
+ border-radius: 10px;
+ border: 1px solid ${colors.gray800};
+ color: ${colors.gray300};
+
+ &:not(.no-pointer):hover {
+ border: 1px solid ${colors.gray600};
+ background-color: ${colors.gray900};
+ cursor: pointer;
}
- & > td:last-of-type {
- border-top-right-radius: 1rem;
- border-bottom-right-radius: 1rem;
- border: 0.5px solid ${({ theme }) => theme.color.grayscale.gray30};
- border-left: none;
+ &.focused {
+ border: 1px solid ${colors.gray600};
+ background-color: ${colors.gray900};
+
+ &:hover {
+ cursor: default;
+ }
}
}
- .focused > td,
- .focused > td:first-of-type,
- .focused > td:last-of-type {
- border-color: ${({ theme }) => theme.color.grayscale.black40};
- border-width: 1px;
- }
`;
diff --git a/src/components/common/Nav/GenerationDropDown/index.tsx b/src/components/common/Nav/GenerationDropDown/index.tsx
new file mode 100644
index 00000000..30bf7b03
--- /dev/null
+++ b/src/components/common/Nav/GenerationDropDown/index.tsx
@@ -0,0 +1,80 @@
+import { EmotionJSX } from '@emotion/react/types/jsx-namespace';
+import { useRouter } from 'next/router';
+import { useState } from 'react';
+
+import { IcNewDropdown } from '@/assets/icons';
+import { DoSoptLogo } from '@/assets/icons/SoptLogos';
+import { useRecoilGenerationSSR } from '@/hooks/useRecoilGenerationSSR';
+import { GENERATION_INFO } from '@/utils/nav';
+
+import {
+ StDropdownGeneration,
+ StGenerationDropdown,
+ StSelectedGeneration,
+ StWrapper,
+} from './style';
+
+function GenerationDropDown() {
+ const router = useRouter();
+
+ const [currentGeneration, setCurrentGeneration] = useRecoilGenerationSSR();
+ const [isDropdownOn, setIsDropdownOn] = useState(false);
+
+ const [selectedGenerationInfo, setSelectedGenerationInfo] = useState({
+ logo: ,
+ slogan: 'DO',
+ });
+
+ const handleSelectedGeneration = (
+ selectedGeneration: string,
+ logo: EmotionJSX.Element,
+ slogan: string,
+ ) => {
+ setCurrentGeneration(selectedGeneration);
+ setSelectedGenerationInfo({ logo: logo, slogan: slogan });
+ setIsDropdownOn(false);
+ const pathSegments = router.asPath.split('/');
+ if (pathSegments[pathSegments.length - 1].match(/^\d+$/)) {
+ router.push('/attendanceAdmin/session');
+ }
+ };
+
+ return (
+
+ setIsDropdownOn(!isDropdownOn)}>
+ {selectedGenerationInfo.logo}
+
+
+ {currentGeneration}기
+
+ {selectedGenerationInfo.slogan} SOPT
+
+
+ {isDropdownOn && (
+
+
+ {GENERATION_INFO.map((info) => {
+ const { generation, Logo, slogan } = info;
+
+ return (
+
+ handleSelectedGeneration(generation, , slogan)
+ }>
+
+
+
{generation}기
+ {slogan} SOPT
+
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
+
+export default GenerationDropDown;
diff --git a/src/components/common/Nav/GenerationDropDown/style.ts b/src/components/common/Nav/GenerationDropDown/style.ts
new file mode 100644
index 00000000..5c3a6a38
--- /dev/null
+++ b/src/components/common/Nav/GenerationDropDown/style.ts
@@ -0,0 +1,88 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+export const StWrapper = styled.div`
+ padding: 0 1.8rem 3.3rem 1.8rem;
+`;
+
+const GenerationContainer = styled.div`
+ display: flex;
+ gap: 1.4rem;
+ padding: 1rem 0 1rem 1.2rem;
+ border-radius: 1rem;
+
+ cursor: pointer;
+
+ & > div {
+ display: flex;
+ flex-direction: column;
+ gap: 0.1rem;
+
+ & > h1 {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+
+ ${fonts.HEADING_16_B}
+ color: ${colors.gray10};
+ }
+ & > h2 {
+ ${fonts.LABEL_12_SB}
+ color: ${colors.gray300};
+ }
+ }
+`;
+
+export const StSelectedGeneration = styled(GenerationContainer)`
+ &:hover {
+ background-color: ${colors.gray800};
+ }
+ &:active {
+ background-color: ${colors.gray700};
+ }
+`;
+
+export const StDropdownGeneration = styled(GenerationContainer)`
+ &:hover {
+ background-color: ${colors.gray600};
+ }
+ &:active {
+ background-color: ${colors.gray500};
+ }
+`;
+
+export const StGenerationDropdown = styled.div`
+ position: absolute;
+
+ margin-top: 1rem;
+
+ width: 17.6rem;
+ height: auto;
+
+ background-color: ${colors.gray700};
+
+ border-radius: 1.3rem;
+ box-shadow: 0px 5px 20px 0px rgba(63, 64, 66, 0.15);
+
+ animation: appearDropdown 0.6s;
+
+ @keyframes appearDropdown {
+ from {
+ opacity: 0;
+ transform: translateY(-1rem);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0rem);
+ }
+ }
+
+ & > div {
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+
+ padding: 0.8rem 0.7rem;
+ }
+`;
diff --git a/src/components/common/Nav/index.tsx b/src/components/common/Nav/index.tsx
index 396c6e07..a529dbbe 100644
--- a/src/components/common/Nav/index.tsx
+++ b/src/components/common/Nav/index.tsx
@@ -1,77 +1,33 @@
import { useRouter } from 'next/router';
-import React, { useEffect, useRef, useState } from 'react';
+import { Fragment, useContext } from 'react';
-import { IcNavMenu } from '@/assets/icons';
-import { useRecoilGenerationSSR } from '@/hooks/useRecoilGenerationSSR';
-import { GENERATION_LIST } from '@/utils/generation';
+import { adminStatusContext } from '@/components/devTools/AdminContextProvider';
import { MENU_LIST } from '@/utils/nav';
-import DropDown from '../DropDown';
-import IcDropDown from '../icons/IcDropDown';
-import {
- StGenerationDropdown,
- StMenu,
- StNavWrapper,
- StSoptLogo,
- StSubMenu,
-} from './style';
+import GenerationDropDown from './GenerationDropDown';
+import { StMenu, StNavWrapper, StSoptLogo, StSubMenu } from './style';
function Nav() {
const router = useRouter();
- const [currentGeneration, setCurrentGeneration] = useRecoilGenerationSSR();
- const [isDropdownOn, setIsDropdownOn] = useState(false);
+ const { status } = useContext(adminStatusContext);
- const dropdownRef = useRef(null);
-
- const handleSelectedGeneration = (selectedGeneration: string) => {
- setCurrentGeneration(selectedGeneration);
- setIsDropdownOn(false);
- const pathSegments = router.asPath.split('/');
- if (pathSegments[pathSegments.length - 1].match(/^\d+$/)) {
- router.push('/attendanceAdmin/session');
- }
- };
+ const filteredMenuList =
+ status === 'MAKERS'
+ ? MENU_LIST.filter((menu) => menu.title === '알림 관리')
+ : MENU_LIST;
const handleSubMenuClick = (path: string) => {
router.push(path);
};
- const handleClickOutside = (event: MouseEvent) => {
- if (
- dropdownRef.current &&
- !dropdownRef.current.contains(event.target as Node)
- ) {
- setIsDropdownOn(false);
- }
- };
-
- useEffect(() => {
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, []);
-
return (
- {MENU_LIST.map((menu) => (
-
+
+ {filteredMenuList.map((menu) => (
+
menu.path && handleSubMenuClick(menu.path[0])}>
-
+
{menu.title}
@@ -95,7 +51,7 @@ function Nav() {
{subMenu}
))}
-
+
))}
);
diff --git a/src/components/common/Nav/style.ts b/src/components/common/Nav/style.ts
index 60a7cb9f..65d19b4c 100644
--- a/src/components/common/Nav/style.ts
+++ b/src/components/common/Nav/style.ts
@@ -1,46 +1,33 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
export const StNavWrapper = styled.nav`
position: fixed;
left: 0;
top: 0;
- width: 22rem;
+ width: 21.2rem;
height: 100%;
- background-color: ${({ theme }) => theme.color.grayscale.white100};
+ background-color: ${colors.gray900};
& > header {
display: flex;
flex-direction: column;
- gap: 4rem;
+ gap: 2.3rem;
- padding: 4rem 4rem 3.6rem 4rem;
+ padding: 4.3rem 4rem 3.3rem 3rem;
- color: ${({ theme }) => theme.color.grayscale.black40};
+ color: ${colors.gray10};
}
`;
export const StSoptLogo = styled.h1`
+ font-size: 2.1rem;
+ font-style: normal;
font-weight: 700;
- font-size: 24px;
- line-height: 100%;
- letter-spacing: -0.02em;
-`;
-
-export const StGenerationDropdown = styled.div`
- cursor: pointer;
-
- & > div {
- cursor: pointer;
- & > span {
- margin-right: 0.4rem;
-
- font-weight: 600;
- font-size: 24px;
- line-height: 140%;
- letter-spacing: -0.02em;
- }
- }
+ line-height: 140%;
+ letter-spacing: -0.084rem;
+ color: ${colors.gray10};
`;
export const StMenu = styled.div<{ currentPage: boolean | undefined }>`
@@ -48,38 +35,41 @@ export const StMenu = styled.div<{ currentPage: boolean | undefined }>`
flex-direction: column;
gap: 1.6rem;
- margin: 0 1.6rem 0.8rem 1.6rem;
+ margin: 0 1.8rem 0.6rem 1.8rem;
+ font-size: 1.6rem;
+ font-style: normal;
font-weight: 600;
- font-size: 16px;
- line-height: 100%;
- letter-spacing: -0.02em;
+ line-height: 150%;
+ letter-spacing: -0.024rem;
+
+ color: ${colors.gray300};
& > p {
display: flex;
align-items: center;
- gap: 2rem;
+ gap: 1.2rem;
width: 100%;
- padding: 1.6rem 2.4rem 1.6rem 2.4rem;
+ padding: 1.3rem 1.4rem 1.3rem 2.4rem;
text-align: center;
- color: ${({ theme, currentPage }) =>
- currentPage
- ? theme.color.grayscale.white100
- : theme.color.grayscale.gray60};
- background: ${({ theme, currentPage }) =>
- currentPage
- ? theme.color.grayscale.black40
- : theme.color.grayscale.gray10};
- border-radius: 8rem;
+ background: ${({ currentPage }) =>
+ currentPage ? colors.gray800 : '#fffff'};
+ border-radius: 1rem;
cursor: pointer;
&:hover {
- color: ${({ theme }) => theme.color.grayscale.white100};
- background: ${({ theme }) => theme.color.grayscale.black40};
+ background: ${colors.gray800};
+ }
+ &:active {
+ color: ${colors.gray10};
+ background-color: ${colors.gray700};
+ & > svg > path {
+ fill: #fcfcfc;
+ }
}
}
`;
@@ -88,24 +78,29 @@ export const StSubMenu = styled.p<{
currentPage: boolean | undefined;
isLast: boolean;
}>`
- padding: 1.6rem 6.9rem 1.6rem 6rem;
- margin: 0 1.6rem;
+ padding: 1.3rem 6.7rem 1.3rem 2.4rem;
+ margin: 0.6rem 1.8rem 0.6rem 4.4rem;
margin-bottom: ${({ isLast }) => (isLast ? '1.6rem' : '0')};
- font-weight: 400;
- font-size: 16px;
- line-height: 100%;
- letter-spacing: -0.02em;
-
- color: ${({ theme, currentPage }) =>
- currentPage ? theme.color.grayscale.black40 : theme.color.grayscale.gray60};
+ font-family: SUIT;
+ font-size: 1.6rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 150%;
+ letter-spacing: -0.024rem;
+ color: ${({ currentPage }) => (currentPage ? colors.gray10 : colors.gray300)};
+ background-color: ${({ currentPage }) =>
+ currentPage ? colors.gray700 : 'fffff'};
+ border-radius: 1rem;
cursor: pointer;
&:hover {
- color: ${({ theme }) => theme.color.grayscale.black40};
+ color: ${colors.gray300};
+ background-color: ${colors.gray800};
}
&:active {
- color: ${({ theme }) => theme.color.grayscale.black40};
+ color: ${colors.gray10};
+ background-color: ${colors.gray700};
}
`;
diff --git a/src/components/common/OptionTemplate/index.tsx b/src/components/common/OptionTemplate/index.tsx
new file mode 100644
index 00000000..0e69ba78
--- /dev/null
+++ b/src/components/common/OptionTemplate/index.tsx
@@ -0,0 +1,21 @@
+import { ReactNode } from 'react';
+
+import { StTemplateWrapper } from './style';
+
+interface Props {
+ title: string;
+ children: ReactNode;
+}
+
+function OptionTemplate(props: Props) {
+ const { title, children } = props;
+
+ return (
+
+ {title}
+ {children}
+
+ );
+}
+
+export default OptionTemplate;
diff --git a/src/components/common/OptionTemplate/style.ts b/src/components/common/OptionTemplate/style.ts
new file mode 100644
index 00000000..34f96e8b
--- /dev/null
+++ b/src/components/common/OptionTemplate/style.ts
@@ -0,0 +1,18 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
+
+export const StTemplateWrapper = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+
+ & > p {
+ margin-top: 1.6rem;
+
+ ${fonts.LABEL_14_SB}
+
+ color: ${colors.gray300};
+ }
+`;
diff --git a/src/components/common/PartFilter/index.tsx b/src/components/common/PartFilter/index.tsx
index a0e54ac7..d31f1c68 100644
--- a/src/components/common/PartFilter/index.tsx
+++ b/src/components/common/PartFilter/index.tsx
@@ -1,20 +1,25 @@
import FilterButton from '@/components/common/FilterButton';
-import { partList, partTranslator } from '@/utils/translator';
+import {
+ allPartTranslator,
+ partList,
+ partTranslator,
+} from '@/utils/translator';
interface Props {
selected: PART;
onChangePart: (part: PART) => void;
+ isAllPart?: boolean;
}
function PartFilter(props: Props) {
- const { selected, onChangePart } = props;
+ const { selected, onChangePart, isAllPart = false } = props;
return (
list={partList}
selected={selected}
onChange={onChangePart}
- translator={partTranslator}
+ translator={isAllPart ? allPartTranslator : partTranslator}
/>
);
}
diff --git a/src/components/common/Selector/index.tsx b/src/components/common/Selector/index.tsx
new file mode 100644
index 00000000..3389039d
--- /dev/null
+++ b/src/components/common/Selector/index.tsx
@@ -0,0 +1,26 @@
+import { IcNewDropdown } from '@/assets/icons';
+
+import { StSelectorWrapper } from './style';
+
+interface Props {
+ content: string | null;
+ onClick?: () => void;
+ isDisabledValue?: boolean;
+ readOnly?: boolean;
+}
+
+function Selector(props: Props) {
+ const { content, onClick, isDisabledValue = false, readOnly = false } = props;
+
+ return (
+
+ {content}
+
+
+ );
+}
+
+export default Selector;
diff --git a/src/components/common/Selector/style.ts b/src/components/common/Selector/style.ts
new file mode 100644
index 00000000..da28db47
--- /dev/null
+++ b/src/components/common/Selector/style.ts
@@ -0,0 +1,36 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+
+export const StSelectorWrapper = styled.div<{
+ isDisabledValue: boolean;
+ readOnly: boolean;
+}>`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.7rem;
+
+ min-width: 8.6rem;
+
+ padding: 1rem 1.4rem;
+
+ font-size: 1.8rem;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 100%; /* 1.8rem */
+ letter-spacing: -0.018rem;
+
+ color: ${({ isDisabledValue }) =>
+ isDisabledValue ? colors.gray400 : colors.gray10};
+
+ background-color: ${colors.gray700};
+ border-radius: 0.8rem;
+
+ cursor: ${({ isDisabledValue, readOnly }) =>
+ isDisabledValue || readOnly ? 'default' : 'pointer'};
+
+ & > svg > path {
+ fill: ${({ isDisabledValue }) =>
+ isDisabledValue ? colors.gray400 : colors.gray10};
+ }
+`;
diff --git a/src/components/common/modal/ModalFooter/index.tsx b/src/components/common/modal/ModalFooter/index.tsx
new file mode 100644
index 00000000..361ef38d
--- /dev/null
+++ b/src/components/common/modal/ModalFooter/index.tsx
@@ -0,0 +1,14 @@
+import { ReactNode } from 'react';
+
+import { StModalFooterWrapper } from './style';
+
+interface Props {
+ children: ReactNode;
+}
+
+function ModalFooter(props: Props) {
+ const { children } = props;
+ return {children};
+}
+
+export default ModalFooter;
diff --git a/src/components/common/modal/ModalFooter/style.ts b/src/components/common/modal/ModalFooter/style.ts
new file mode 100644
index 00000000..cee5b9b5
--- /dev/null
+++ b/src/components/common/modal/ModalFooter/style.ts
@@ -0,0 +1,15 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+
+export const StModalFooterWrapper = styled.footer`
+ display: flex;
+ align-items: center;
+
+ height: 9.6rem;
+
+ padding: 2.4rem 4rem;
+
+ background-color: ${colors.gray700};
+
+ border-radius: 0 0 1.2rem 1.2rem;
+`;
diff --git a/src/components/common/modal/ModalHeader/index.tsx b/src/components/common/modal/ModalHeader/index.tsx
new file mode 100644
index 00000000..befc91a7
--- /dev/null
+++ b/src/components/common/modal/ModalHeader/index.tsx
@@ -0,0 +1,25 @@
+import { IcModalClose } from '@/assets/icons';
+
+import { StModalHeader } from './style';
+
+interface Props {
+ title: string;
+ desc: string;
+ onClose: () => void;
+}
+
+function ModalHeader(props: Props) {
+ const { title, desc, onClose } = props;
+
+ return (
+
+
+
{title}
+ {desc}
+
+
+
+ );
+}
+
+export default ModalHeader;
diff --git a/src/components/common/modal/ModalHeader/style.ts b/src/components/common/modal/ModalHeader/style.ts
new file mode 100644
index 00000000..84612822
--- /dev/null
+++ b/src/components/common/modal/ModalHeader/style.ts
@@ -0,0 +1,47 @@
+import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+
+export const StModalHeader = styled.header`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ padding: 2.2rem 3.2rem 0.8rem 3.2rem;
+
+ & > div.title {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1.6rem;
+
+ & > h1 {
+ font-size: 2.8rem;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 150%;
+ letter-spacing: -0.056rem;
+
+ color: ${colors.gray10};
+ }
+
+ & > h2 {
+ font-size: 1.4rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ letter-spacing: -0.021rem;
+
+ color: ${colors.gray300};
+ }
+ }
+ & > svg {
+ cursor: pointer;
+
+ &:hover {
+ fill: ${colors.gray700};
+ }
+ &:active {
+ fill: ${colors.gray600};
+ }
+ }
+`;
diff --git a/src/components/common/modal/style.ts b/src/components/common/modal/style.ts
index 62b7260e..bc92d01c 100644
--- a/src/components/common/modal/style.ts
+++ b/src/components/common/modal/style.ts
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
import zIndex from '@/utils/zIndex';
@@ -8,7 +9,7 @@ export const StModalBackground = styled.div`
left: 0;
width: 100%;
height: 100%;
- background-color: rgba(0, 0, 0, 0.4);
+ background-color: rgba(15, 15, 18, 0.8);
z-index: ${zIndex.dim};
display: flex;
justify-content: center;
@@ -16,10 +17,10 @@ export const StModalBackground = styled.div`
`;
export const StModalWrapper = styled.div`
- width: 90rem;
+ width: auto;
height: auto;
- background-color: ${({ theme }) => theme.color.grayscale.white100};
+ background-color: ${colors.gray800};
box-shadow: 0px 0px 40px rgba(16, 24, 40, 0.06);
border-radius: 1.2rem;
diff --git a/src/components/devTools/AdminContextProvider/index.tsx b/src/components/devTools/AdminContextProvider/index.tsx
new file mode 100644
index 00000000..a75d7217
--- /dev/null
+++ b/src/components/devTools/AdminContextProvider/index.tsx
@@ -0,0 +1,44 @@
+import {
+ createContext,
+ Dispatch,
+ ReactNode,
+ SetStateAction,
+ useEffect,
+ useState,
+} from 'react';
+
+interface Props {
+ children: ReactNode;
+}
+
+interface AdminStatusContextType {
+ status: string | null;
+ setStatus: Dispatch>;
+}
+
+const defaultContextValue: AdminStatusContextType = {
+ status: 'DEVELOPER',
+ setStatus: () => {},
+};
+
+export const adminStatusContext =
+ createContext(defaultContextValue);
+
+export const AdminStatusProvider = (props: Props) => {
+ const { children } = props;
+ const [status, setStatus] = useState('NOT_CERTIFIED');
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const savedStatus =
+ sessionStorage.getItem('adminStatus') ?? 'NOT_CERTIFIED';
+ setStatus(savedStatus);
+ }
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/devTools/AdminStatus/index.tsx b/src/components/devTools/AdminStatus/index.tsx
new file mode 100644
index 00000000..8c3f3dc7
--- /dev/null
+++ b/src/components/devTools/AdminStatus/index.tsx
@@ -0,0 +1,59 @@
+import { useRouter } from 'next/router';
+import { useContext, useEffect, useState } from 'react';
+
+import DropDown from '@/components/common/DropDown';
+import OptionTemplate from '@/components/common/OptionTemplate';
+import Selector from '@/components/common/Selector';
+
+import { adminStatusContext } from '../AdminContextProvider';
+import { StAdminStatusContainer } from './style';
+
+function AdminStatusDevtools() {
+ const router = useRouter();
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const { status, setStatus } = useContext(adminStatusContext);
+
+ const STATUS_SELECTION: ADMIN_STATUS[] = ['SOPT', 'MAKERS'];
+
+ const isAdminStatus = (value: string): value is ADMIN_STATUS => {
+ return [
+ 'SUPER_USER',
+ 'SOPT',
+ 'MAKERS',
+ 'NOT_CERTIFIED',
+ 'DEVELOPER',
+ ].includes(value);
+ };
+
+ const handleStatusChange = (newStatus: string) => {
+ if (isAdminStatus(newStatus)) {
+ setStatus(newStatus);
+ sessionStorage.setItem('adminStatus', newStatus);
+ if (newStatus === 'MAKERS') {
+ router.push('/alarmAdmin');
+ }
+ } else {
+ console.error('유효하지 않은 권한입니다.');
+ }
+ };
+
+ return (
+
+
+ setIsDropdownOpen(!isDropdownOpen)}
+ />
+ {isDropdownOpen && (
+
+ )}
+
+
+ );
+}
+
+export default AdminStatusDevtools;
diff --git a/src/components/devTools/AdminStatus/style.ts b/src/components/devTools/AdminStatus/style.ts
new file mode 100644
index 00000000..54aecb96
--- /dev/null
+++ b/src/components/devTools/AdminStatus/style.ts
@@ -0,0 +1,5 @@
+import styled from '@emotion/styled';
+
+export const StAdminStatusContainer = styled.div`
+ width: 18.5rem;
+`;
diff --git a/src/components/session/Select/index.tsx b/src/components/session/Select/index.tsx
index 3fa2a1ad..18e12e80 100644
--- a/src/components/session/Select/index.tsx
+++ b/src/components/session/Select/index.tsx
@@ -1,9 +1,8 @@
-import { useTheme } from '@emotion/react';
import { useCallback, useEffect, useRef, useState } from 'react';
-import IcDropdown from '@/components/icons/IcDropdown';
-import { ACTIVITY_GENRATION } from '@/utils/generation';
-import { attendanceTranslator, getAttendanceColor } from '@/utils/translator';
+import IcDropdownCheck from '@/assets/icons/IcDropdownCheck.svg';
+import { ACTIVITY_GENERATION } from '@/utils/generation';
+import { attendanceTranslator } from '@/utils/translator';
import { StOptions, StSelect, StSelectWrap } from './style';
@@ -11,11 +10,12 @@ interface Props {
options: Array<{ label: string; value: ATTEND_STATUS }>;
selected: ATTEND_STATUS;
generation: string;
+ round: '1차' | '2차';
onChange: (value: ATTEND_STATUS) => void;
}
function Select(props: Props) {
- const { options, selected, generation, onChange } = props;
+ const { options, selected, generation, round, onChange } = props;
const optionsRef = useRef(null);
@@ -23,8 +23,8 @@ function Select(props: Props) {
const [showOptions, setShowOptions] = useState(false);
const toggleOptions = useCallback(() => {
- setShowOptions(!showOptions);
- }, [showOptions]);
+ setShowOptions((prevShowOptions) => !prevShowOptions);
+ }, []);
useEffect(() => {
const handleClickOutside = (event: any) => {
@@ -47,13 +47,13 @@ function Select(props: Props) {
return (
-
-
- {attendanceTranslator[currentValue]}
+
+
+ {round} {attendanceTranslator[currentValue]}
+
- {generation === ACTIVITY_GENRATION && }
- {showOptions && generation === ACTIVITY_GENRATION && (
+ {showOptions && generation === ACTIVITY_GENERATION && (
{options.map((option) => (
onClickOption(option.value)}>
diff --git a/src/components/session/Select/style.ts b/src/components/session/Select/style.ts
index e6e3740f..a75e9d1f 100644
--- a/src/components/session/Select/style.ts
+++ b/src/components/session/Select/style.ts
@@ -1,4 +1,6 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
import zIndex from '@/utils/zIndex';
@@ -7,31 +9,50 @@ export const StSelectWrap = styled.div`
justify-content: center;
align-items: center;
`;
-export const StSelect = styled.button`
- background: none;
- font-size: 1.4rem;
- font-weight: 500;
- line-height: 2rem;
- color: ${({ theme }) => theme.color.grayscale.black40};
+export const StSelect = styled.button<{
+ value: ATTEND_STATUS | ATTEND_STATUS_KR;
+}>`
+ ${fonts.BODY_14_M}
display: flex;
align-items: center;
- gap: 1.2rem;
- transform: translateX(0.6rem);
+ padding: 3.5px 8px 3.5px 12px;
+ gap: 12px;
+ border-radius: 8px;
+ background-color: ${({ value }) => getAttendanceColor(value).background};
+ margin-right: 20px;
+
+ p {
+ color: ${({ value }) => getAttendanceColor(value).font};
+ }
+ svg {
+ margin-left: 12px;
+ }
+ path {
+ fill: ${({ value }) => getAttendanceColor(value).font};
+ }
`;
export const StOptions = styled.ul`
+ ${fonts.BODY_14_M}
+
z-index: ${zIndex.select};
position: absolute;
- transform: translateY(6.5rem);
- background-color: ${({ theme }) => theme.color.grayscale.realwhite};
- border-radius: 1rem;
- padding: 0.7rem;
- box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.1);
+ transform: translate(-1rem, 6.2rem);
+
+ background-color: ${colors.gray600};
+ box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.1);
+ border-radius: 13px;
+ padding: 8px 6px;
animation: appear 0.6s;
+
li {
+ padding: 5px 21px 5px 9px;
+ border: none;
border-radius: 0.6rem;
- padding: 0.9rem 1.7rem;
+ color: ${colors.gray10};
+ background-color: ${colors.gray600};
+
&:hover {
- background-color: ${({ theme }) => theme.color.grayscale.gray20};
+ background-color: ${colors.gray500};
cursor: pointer;
}
}
@@ -39,11 +60,27 @@ export const StOptions = styled.ul`
@keyframes appear {
from {
opacity: 0;
- transform: translateY(5.5rem);
+ transform: translate(-1rem, 5.2rem);
}
to {
opacity: 1;
- transform: translateY(6.5rem);
+ transform: translate(-1rem, 6.2rem);
}
}
`;
+
+const getAttendanceColor = (value: ATTEND_STATUS | ATTEND_STATUS_KR) => {
+ switch (value) {
+ case 'ABSENT':
+ case '결석':
+ return { font: colors.red300, background: colors.red800 };
+ case 'TARDY':
+ case '지각':
+ return { font: colors.attention, background: colors.yellow900 };
+ case 'ATTENDANCE':
+ case '출석':
+ return { font: colors.information, background: colors.green900 };
+ default:
+ return { font: colors.gray10, background: colors.gray600 };
+ }
+};
diff --git a/src/data/sessionData.ts b/src/data/sessionData.ts
index 9fb30ce9..b223e694 100644
--- a/src/data/sessionData.ts
+++ b/src/data/sessionData.ts
@@ -18,10 +18,19 @@ export const subLectureInit: SubLecture = {
code: null,
};
-export const attendanceOptions: Array<{
- label: string;
- value: ATTEND_STATUS;
-}> = [
- { label: '출석', value: 'ATTENDANCE' },
- { label: '결석', value: 'ABSENT' },
-];
+export const attendanceOptions: Record<
+ string,
+ Array<{
+ label: string;
+ value: ATTEND_STATUS;
+ }>
+> = {
+ first: [
+ { label: '1차 출석', value: 'ATTENDANCE' },
+ { label: '1차 결석', value: 'ABSENT' },
+ ],
+ second: [
+ { label: '2차 출석', value: 'ATTENDANCE' },
+ { label: '2차 결석', value: 'ABSENT' },
+ ],
+};
diff --git a/src/hooks/useRecoilGenerationSSR.ts b/src/hooks/useRecoilGenerationSSR.ts
index 279f9b7e..695b4e2c 100644
--- a/src/hooks/useRecoilGenerationSSR.ts
+++ b/src/hooks/useRecoilGenerationSSR.ts
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { currentGenerationState } from '@/recoil/atom';
-import { ACTIVITY_GENRATION } from '@/utils/generation';
+import { ACTIVITY_GENERATION } from '@/utils/generation';
export const useRecoilGenerationSSR = () => {
const [isInitial, setIsInitial] = useState(true);
@@ -12,5 +12,5 @@ export const useRecoilGenerationSSR = () => {
setIsInitial(false);
}, []);
- return [isInitial ? ACTIVITY_GENRATION : value, setValue] as const;
+ return [isInitial ? ACTIVITY_GENERATION : value, setValue] as const;
};
diff --git a/src/hooks/useUnauthorizedStatus.ts b/src/hooks/useUnauthorizedStatus.ts
new file mode 100644
index 00000000..30b63c53
--- /dev/null
+++ b/src/hooks/useUnauthorizedStatus.ts
@@ -0,0 +1,16 @@
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+export const useUnauthorizedStatus = (status: ADMIN_STATUS) => {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (
+ typeof window !== 'undefined' &&
+ sessionStorage.getItem('adminStatus') === status
+ ) {
+ alert('접근 권한이 없는 계정입니다.');
+ router.back();
+ }
+ }, [router, status]);
+};
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 35d14e68..e765af8a 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,5 +1,6 @@
import { Global, ThemeProvider } from '@emotion/react';
import type { AppProps } from 'next/app';
+import Head from 'next/head';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
@@ -7,6 +8,7 @@ import { ReactQueryDevtools } from 'react-query/devtools';
import { RecoilRoot } from 'recoil';
import Layout from '@/components/common/Layout';
+import { AdminStatusProvider } from '@/components/devTools/AdminContextProvider';
import global from '@/styles/global';
import theme from '@/styles/theme';
import { getToken } from '@/utils/auth';
@@ -29,18 +31,25 @@ export default function App({ Component, pageProps }: AppProps) {
}, []);
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+ SOPT Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
index af2f1aba..a53754b4 100644
--- a/src/pages/_document.tsx
+++ b/src/pages/_document.tsx
@@ -3,7 +3,11 @@ import { Head, Html, Main, NextScript } from 'next/document';
export default function Document() {
return (
-
+
+
+
+
+
diff --git a/src/pages/alarmAdmin/index.tsx b/src/pages/alarmAdmin/index.tsx
new file mode 100644
index 00000000..dfe9ddf8
--- /dev/null
+++ b/src/pages/alarmAdmin/index.tsx
@@ -0,0 +1,47 @@
+import { useState } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import AlarmList from '@/components/alarmAdmin/AlarmList';
+import CreateAlarmModal from '@/components/alarmAdmin/CreateAlarmModal';
+import FloatingButton from '@/components/common/FloatingButton';
+import Loading from '@/components/common/Loading';
+import Modal from '@/components/common/modal';
+import { currentGenerationState } from '@/recoil/atom';
+import { useGetAlarmList } from '@/services/api/alarm/query';
+
+function AlarmAdminPage() {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const currentGeneration = useRecoilValue(currentGenerationState);
+
+ const { data, isLoading, refetch } = useGetAlarmList(
+ parseInt(currentGeneration),
+ );
+
+ const handleModalClose = async () => {
+ setIsModalOpen(!isModalOpen);
+ refetch();
+ };
+
+ const refetchAlarmList = () => {
+ refetch();
+ };
+
+ if (isLoading || !data) return ;
+ return (
+ <>
+
+ 알림 생성하기>}
+ onClick={() => setIsModalOpen(!isModalOpen)}
+ />
+ {isModalOpen && (
+
+
+
+ )}
+ >
+ );
+}
+
+export default AlarmAdminPage;
diff --git a/src/pages/attendanceAdmin/session/[id].tsx b/src/pages/attendanceAdmin/session/[id].tsx
index 91bf48b7..698b56d5 100644
--- a/src/pages/attendanceAdmin/session/[id].tsx
+++ b/src/pages/attendanceAdmin/session/[id].tsx
@@ -1,11 +1,13 @@
import styled from '@emotion/styled';
+import { colors } from '@sopt-makers/colors';
+import { fonts } from '@sopt-makers/fonts';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
-import { RefObject, useRef, useState } from 'react';
+import { ReactNode, RefObject, useRef, useState } from 'react';
import AttendanceModal from '@/components/attendanceAdmin/session/AttendanceModal';
-import Button from '@/components/common/Button';
-import Footer from '@/components/common/Footer';
+import Chip from '@/components/common/Chip';
+import FloatingButton from '@/components/common/FloatingButton';
import HelperText from '@/components/common/HelperText';
import ListActionButton from '@/components/common/ListActionButton';
import ListWrapper from '@/components/common/ListWrapper';
@@ -16,6 +18,7 @@ import Select from '@/components/session/Select';
import { PAGE_SIZE } from '@/data/queryData';
import { attendanceInit, attendanceOptions } from '@/data/sessionData';
import useObserver from '@/hooks/useObserver';
+import { useUnauthorizedStatus } from '@/hooks/useUnauthorizedStatus';
import {
updateMemberAttendStatus,
updateMemberScore,
@@ -26,20 +29,15 @@ import {
useGetSessionDetail,
} from '@/services/api/lecture/query';
import { addPlus, precision } from '@/utils';
-import { ACTIVITY_GENRATION } from '@/utils/generation';
-
-const HEADER_LABELS = [
- '순번',
- '회원명',
- '학교명',
- '1차 출석 상태',
- '1차 출석 일시',
- '2차 출석 상태',
- '2차 출석 일시',
- '변동점수',
- 'ㅤ',
-];
-const TABLE_WIDTH = ['9%', '9%', '12%', '10%', '16%', '10%', '16%', '9%', '9%'];
+import { ACTIVITY_GENERATION } from '@/utils/generation';
+import { allPartTranslator, attributeTranslator } from '@/utils/translator';
+
+interface ChangedUpdatedStatus {
+ memberId: number;
+ firstRoundStatus: ATTEND_STATUS;
+ secondRoundStatus: ATTEND_STATUS;
+ updatedScore: number;
+}
function SessionDetailPage() {
const router = useRouter();
@@ -48,9 +46,14 @@ function SessionDetailPage() {
const [selectedPart, setSelectedPart] = useState('ALL');
const [changedMembers, setChangedMembers] = useState([]);
+ const [changedUpdatedStatusList, setChangedUpdatedStatusList] = useState<
+ ChangedUpdatedStatus[]
+ >([]);
const [modal, setModal] = useState(null);
const bottomRef: RefObject = useRef(null);
+ useUnauthorizedStatus('MAKERS');
+
const {
data: session,
isLoading: isLoadingSession,
@@ -75,38 +78,115 @@ function SessionDetailPage() {
setSelectedPart(part);
};
+ const calcUpdatedScore = (
+ memberId: number,
+ attendances: Attendance[],
+ round: number,
+ status: ATTEND_STATUS,
+ ) => {
+ const attribute = session?.attribute;
+
+ const prevStatus = changedUpdatedStatusList.find(
+ (item) => item.memberId === memberId,
+ );
+ const anotherRound = round === 1 ? 2 : 1;
+ const anotherRoundStatus = prevStatus
+ ? round === 1
+ ? prevStatus.secondRoundStatus
+ : prevStatus.firstRoundStatus
+ : (attendances.find((attendance) => attendance.round === anotherRound)
+ ?.status as ATTEND_STATUS);
+
+ const firstRoundStatus = round === 1 ? status : anotherRoundStatus;
+ const secondRoundStatus = round === 2 ? status : anotherRoundStatus;
+ let updatedScore = 0;
+
+ switch (attribute) {
+ case 'SEMINAR':
+ if (
+ firstRoundStatus === 'ATTENDANCE' &&
+ secondRoundStatus === 'ATTENDANCE'
+ ) {
+ updatedScore = 0;
+ } else if (
+ firstRoundStatus === 'ABSENT' &&
+ secondRoundStatus === 'ATTENDANCE'
+ ) {
+ updatedScore = -0.5;
+ } else {
+ updatedScore = -1;
+ }
+ break;
+ case 'EVENT':
+ if (
+ (firstRoundStatus === 'ATTENDANCE' ||
+ firstRoundStatus === 'ABSENT') &&
+ secondRoundStatus === 'ATTENDANCE'
+ ) {
+ updatedScore = 0.5;
+ } else {
+ updatedScore = 0;
+ }
+ break;
+ case 'ETC':
+ updatedScore = 0;
+ break;
+ }
+
+ const newList = changedUpdatedStatusList.filter(
+ (item) => item.memberId !== memberId,
+ );
+ setChangedUpdatedStatusList([
+ ...newList,
+ { memberId, firstRoundStatus, secondRoundStatus, updatedScore },
+ ]);
+ };
+
const onChangeStatus = async (
status: ATTEND_STATUS,
member: SessionMember,
- subAttendanceId: number,
+ round: number,
+ ) => {
+ setChangedMembers([...changedMembers, member]);
+ calcUpdatedScore(member.member.memberId, member.attendances, round, status);
+ };
+
+ const onUpdateScore = async (
+ memberId: number,
+ firstSubAttendanceId: number,
+ secondSubAttendanceId: number,
) => {
if (session) {
- const result = await updateMemberAttendStatus(
- subAttendanceId,
- status,
+ const { firstRoundStatus, secondRoundStatus, updatedScore } =
+ changedUpdatedStatusList.find(
+ (item) => item.memberId === memberId,
+ ) as ChangedUpdatedStatus;
+
+ const firstRoundError = await updateMemberAttendStatus(
+ firstSubAttendanceId,
+ firstRoundStatus,
session.attribute,
);
- if (result) {
- alert(result.error);
+ const secondRoundError = await updateMemberAttendStatus(
+ secondSubAttendanceId,
+ secondRoundStatus,
+ session.attribute,
+ );
+ const updateScoreError = await updateMemberScore(memberId);
+
+ if (firstRoundError || secondRoundError || updateScoreError) {
+ alert('출석 점수를 갱신하는데 실패했어요');
} else {
- setChangedMembers([...changedMembers, member]);
- // refetchMembers();
+ setChangedMembers(
+ changedMembers.filter(
+ (member) => member.member.memberId !== memberId,
+ ),
+ );
+ refetchSession();
}
}
};
- const onUpdateScore = async (memberId: number) => {
- const result = await updateMemberScore(memberId);
- if (result) {
- alert(result.error);
- } else {
- setChangedMembers(
- changedMembers.filter((member) => member.member.memberId !== memberId),
- );
- refetchSession();
- }
- };
-
const isChangedMember = (member: SessionMember) => {
return changedMembers.find(
(item) => item.member.memberId === member.member.memberId,
@@ -140,111 +220,151 @@ function SessionDetailPage() {
}
};
+ const getMemberCount = (attendances: Record) => {
+ let sum = 0;
+ Object.keys(attendances).map((key) => (sum += attendances[key]));
+ return sum;
+ };
+
+ const getButtonContent = (status: SESSION_STATUS): ReactNode => {
+ switch (status) {
+ case 'BEFORE':
+ return <>1차 출석 시작하기>;
+ case 'FIRST':
+ return <>2차 출석 시작하기>;
+ case 'SECOND':
+ return <>출석 완료하기>;
+ default:
+ return <>>;
+ }
+ };
+
+ const getButtonClickHandler = (status: SESSION_STATUS) => {
+ switch (status) {
+ case 'BEFORE':
+ return () => startAttendance(1);
+ case 'FIRST':
+ return () => startAttendance(2);
+ case 'SECOND':
+ return () => closeAttendance();
+ default:
+ // eslint-disable-next-line prettier/prettier
+ return () => {};
+ }
+ };
+
return (
{session && (
-
-
- {session.name} 출석 관리
-
-
+
+
{session.name} 출석 관리
+
+
+
+ {session.part === 'ALL' && (
+
+ )}
+
+
총 {getMemberCount(session.attendances)}명
+
출석 {session.attendances.attendance}
지각 {session.attendances.tardy}
결석 {session.attendances.absent}
미정 {session.attendances.unknown}
- {session.part === 'ALL' && (
-
- )}
)}
{session && members ? (
-
-
-
- {HEADER_LABELS.map((label) => (
- {label} |
- ))}
-
-
-
- {members?.pages.map(
- (pageMembers, pageIndex) =>
- pageMembers &&
- pageMembers.map((member, index) => {
- const firstRound =
- member.attendances.find((item) => item.round === 1) ??
- attendanceInit;
- const secondRound =
- member.attendances.find((item) => item.round === 2) ??
- attendanceInit;
- const firstRoundTime = dayjs(firstRound.updateAt).format(
- 'YYYY/MM/DD HH:mm',
- );
- const secondRoundTime = dayjs(secondRound.updateAt).format(
- 'YYYY/MM/DD HH:mm',
- );
- return (
-
- {precision(pageIndex * PAGE_SIZE + index + 1, 2)} |
- {member.member.name} |
-
+
+ {members?.pages.map(
+ (pageMembers, pageIndex) =>
+ pageMembers &&
+ pageMembers.map((member, index) => {
+ const firstRound =
+ member.attendances.find((item) => item.round === 1) ??
+ attendanceInit;
+ const secondRound =
+ member.attendances.find((item) => item.round === 2) ??
+ attendanceInit;
+ const firstRoundTime = dayjs(firstRound.updateAt).format(
+ 'YYYY/MM/DD HH:mm',
+ );
+ const secondRoundTime = dayjs(secondRound.updateAt).format(
+ 'YYYY/MM/DD HH:mm',
+ );
+ const updatedScore =
+ changedUpdatedStatusList.find(
+ (item) => item.memberId === member.member.memberId,
+ )?.updatedScore ?? member.updatedScore;
+
+ return (
+
+
+ {precision(pageIndex * PAGE_SIZE + index + 1, 2)}
+
+
+
+ {member.member.name}
+
+
+
{member.member.university}
- |
-
- |
- {firstRoundTime} |
-
- |
- {secondRoundTime} |
- {addPlus(member.updatedScore)}점 |
-
- onUpdateScore(member.member.memberId)}
- text="갱신"
- disabled={
- !(
- session.status === 'END' &&
- isChangedMember(member) &&
- String(session.generation) === ACTIVITY_GENRATION
- )
- }
- />
- |
-
- );
- }),
- )}
-
+
+
+