From 06c003f5221873c553b0b8c8e77820c9ddcf62f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=A7=80=EC=9B=90?= <81554184+jeewonMoon@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:17:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=83=81=EB=8B=A8=EB=B0=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - neutral4: #777을 추가했습니다. - 기존의 neutral4와 neutral5는 neutral5, 6로 순번이 밀렸습니다. - 이에 따른 수정 사항들을 수정했습니다. - 학번 칸에 0이 기본적으로 들어가 있는 버그를 수정했습니다. - input 디자인도 수정했습니다. - 필요한 이미지 파일들을 추가했습니다. - 로그인 유효시간 타이머 기능을 추가했습니다. 현재는 다른 처리 없이 로그인 화면으로 이동시키고 있습니다. - 로그아웃 버튼도 동일하게 로그인 화면으로 이동하도록 했습니다. --- src/assets/img/btn_main_top_left.svg | 14 ++++ src/assets/img/btn_main_top_right.png | Bin 985 -> 0 bytes src/assets/img/btn_main_top_right.svg | 14 ++++ src/assets/img/input_dropdown.png | Bin 0 -> 247 bytes src/assets/img/red_edge.png | Bin 0 -> 957 bytes src/assets/img/top_menu_down.png | Bin 0 -> 952 bytes src/components/Header.tsx/TImer.tsx | 53 ++++++++++++ src/components/Header.tsx/TopMenu.tsx | 107 +++++++++++++++++++++++++ src/components/Header.tsx/TopNav.tsx | 27 +++++++ src/components/Header.tsx/index.tsx | 37 +++++++++ src/components/LoginForm/FormInput.tsx | 2 +- src/components/LoginForm/index.tsx | 6 +- src/components/Menubar/Menu.tsx | 4 +- src/pages/index/Home.tsx | 11 ++- src/styles/GlobalStyle.tsx | 2 + src/styles/theme/Theme.ts | 5 +- 16 files changed, 272 insertions(+), 10 deletions(-) create mode 100644 src/assets/img/btn_main_top_left.svg delete mode 100644 src/assets/img/btn_main_top_right.png create mode 100644 src/assets/img/btn_main_top_right.svg create mode 100644 src/assets/img/input_dropdown.png create mode 100644 src/assets/img/red_edge.png create mode 100644 src/assets/img/top_menu_down.png create mode 100644 src/components/Header.tsx/TImer.tsx create mode 100644 src/components/Header.tsx/TopMenu.tsx create mode 100644 src/components/Header.tsx/TopNav.tsx create mode 100644 src/components/Header.tsx/index.tsx diff --git a/src/assets/img/btn_main_top_left.svg b/src/assets/img/btn_main_top_left.svg new file mode 100644 index 0000000..2f7a3dd --- /dev/null +++ b/src/assets/img/btn_main_top_left.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/assets/img/btn_main_top_right.png b/src/assets/img/btn_main_top_right.png deleted file mode 100644 index d21d608840db46a7cb03306777cc827a6ee9f971..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 985 zcmaJ=PiWIn7>|r?Wl-2jP(+^@2%=41(sgZOV@;aY6_;7NvK3Ee$y*z`&CM-}n2z_q~2~^=5MJ>>R@| z$;z@`qiZ6L6EpPx=mVdl%NbH?kTtwTx@LeF)yA6$R9v%(YRI&EJ0DS=VWxmnZ;*zu zB3szy%oxK(u20zvlP^TRX|)jno2cn{3j5>DTNXIB!fs>?X!u3ca+dc3w6?cexAxkW zWV40qARoz8z(vFak-P1Ma-^^WU75!5G0%blgtQg**i^%)f+7wO$Z#-a!6gWm1WrgV zX0zF=K!gJ1p~wqjO32D-QHBr@B&bmxsCNPGMVw_%hFTyIroE=5WyD1xb=( z4N*){L@Mlg#Eep2xG+-CQD_B@PaN!lSkc_X9ip(*>2V3JKbG~vVVh{f_{j8mfrD{L zBcNgYAL_bebVzFG#P2_a!+Ot0d<})T6Ik@(wie=4zFZ8DNpMidcze{vY6}w_wy+P1 zYZ~erzFeq4dCVR + + + + + + + diff --git a/src/assets/img/input_dropdown.png b/src/assets/img/input_dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..9032549a8b540164532d7126f88135c17d0b20d8 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^f*{Pn1|+R>-G2e8eV#6kAr}70DGQhqmNZH79KJaL z2ww8=@JO;8$~-p9G}~z6)dek{IXODt`{nJ!{{H`epS#`ByzcL>ryDXaFPm<`%jn6% z&c55&$mrdHv=jgT|F^xiE_SzYEMNAUgwJBi%8$+X`1&>+?G~?BJtbgoZ(nF;wv8ER zzEl8j{*0M3W5d?PR0_`KE?hSGz<~ocX=%?o6@2Ue|2rGMzwWO}3}fGAfh7)Mhs0)7 tFpEVBiJVZd+jc>TvUb_GQ literal 0 HcmV?d00001 diff --git a/src/assets/img/red_edge.png b/src/assets/img/red_edge.png new file mode 100644 index 0000000000000000000000000000000000000000..bee4c484a735f5e4a158b900dc8cfeab55a9ae59 GIT binary patch literal 957 zcmaJ=O^ee&7|tR{cVW?^dK$9}BG_coY?~%DuHB|}15GJyp*@-=(>8RI8I!3^>qYkD z;>n{Ye}E!*78GK-sm{wrCg(H=!Sdr-w9p-BEu*%K#dbPBqLgT&IGs*~sVv}dC`ziTCK|b1mLamy zETDFr4WgY?!9bA{dW3p70EwdA#}lga%;|XwK3T~I(XvdeU}9_&Q4(O1QVO)J|3iI$ zg^p+oUHJW{aMYO*B(_k5C!xbGZm^StB3dOxHpO8F!6ISJTkl0#tGDF3>%A1$az$oFOs@5>Yfg_??CYG6&EsqJAG>dF-+OL! lpWHlo&Ar;KZ#2xSTn2Jq&Ne?$GI*LiD6`ry-W)tQ{sRzVDC7VD literal 0 HcmV?d00001 diff --git a/src/assets/img/top_menu_down.png b/src/assets/img/top_menu_down.png new file mode 100644 index 0000000000000000000000000000000000000000..60432f926188df2ed4b088855cb482333bd654fe GIT binary patch literal 952 zcmaJ=&x_MQ7|mK-WMyIXr1mmo#gk1YNt@WvSktE6Et*ogg{>DcO{Q(gCKHpXNsEXG z9z?{u9=(hAJ?KgBU-0PBiyj3JI_b7u57xkBe(>J=-uKNn&pYkC?X4?Y48v@<_Khx` zH|fZo%l_{-9{-}#Wzy)A13V;&6(L4<@Bo39Z;enFS~mIz z;Zi@OY=)^+)6lX#M8E)z+(2W$AAewh>uBs@$%JNDM`L$?7NLV#yJyclyX>&lo1l`a zRKQ2X0;zu##A>Rsb6u6LvtynGa|rP?_QX`*?0`Cs5GZl5U_%)KS>Obzs3^*HAVLB1 zP~-)%ASkLNst|(3$I@t#GgP}qa}kT~G1Z z+BDTx5ECn~QOnR+%I93yQ5B_D){8}`mr9}_H1wJbWgXtA6%AQ3bVXir4Qx+*6p$s? zIpqp#xmhFlA&qRH$bE>MW`un(Pgr%=dXXAy^%h)btrw}emdjH!e0HsWUGwsY_D;53 zH7(t&+8zb8*CX207jCC#>4V@}hTcoR3@VR){`mcN@0AF0p66{|%jfgWo!s4>tDnw% jeRAp6x9Y_=8{i{Td2#;Pz2ld!v$xc0w2gPQ`%nJ>V@e|R literal 0 HcmV?d00001 diff --git a/src/components/Header.tsx/TImer.tsx b/src/components/Header.tsx/TImer.tsx new file mode 100644 index 0000000..03137fb --- /dev/null +++ b/src/components/Header.tsx/TImer.tsx @@ -0,0 +1,53 @@ +import {useEffect, useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import styled from 'styled-components'; + +interface TimerProps { + name: string; +} + +function Timer({name}: TimerProps) { + const navigate = useNavigate(); + const [time, setTime] = useState(1800); + + const formatTime = (time: number) => { + return time.toString().padStart(2, '0'); + }; + + const resetTime = () => { + setTime(1800); + }; + + useEffect(() => { + const timer = setInterval(() => { + setTime(prev => prev - 1); + }, 1000); + + if (time === 0) { + navigate('/login'); + } + + return () => clearInterval(timer); + }, [time]); + + return ( + + {name} + + 님 [00:{formatTime(Math.floor(time / 60))}:{formatTime(time % 60)}] + + + ); +} + +const TimerWrap = styled.button` + ${props => props.theme.texts.tableTitle}; + font-size: 1.3rem; + + > span { + color: ${props => props.theme.colors.neutral4}; + font-weight: 400; + } +`; + +export default Timer; diff --git a/src/components/Header.tsx/TopMenu.tsx b/src/components/Header.tsx/TopMenu.tsx new file mode 100644 index 0000000..53f34c9 --- /dev/null +++ b/src/components/Header.tsx/TopMenu.tsx @@ -0,0 +1,107 @@ +import styled from 'styled-components'; +import {useNavigate} from 'react-router-dom'; +import Left from '@assets/img/btn_main_top_left.svg?react'; +import Right from '@assets/img/btn_main_top_right.svg?react'; +import logout from '@assets/img/logout.png'; +import down from '@assets/img/top_menu_down.png'; +import notice from '@assets/img/notice.png'; +import setting from '@assets/img/setitng.png'; +import menu from '@assets/img/menu.png'; +import Timer from './TImer'; + +function TopMenu() { + const navigate = useNavigate(); + const name = '세종대'; + + const handleLogout = () => { + navigate('/login'); + }; + + return ( + + + + + + + + + PC + + + + KOR + + + + + + + + + ); +} + +const TopMenuContainer = styled.div` + display: flex; + align-items: center; + column-gap: 1rem; +`; + +const ArrowWrap = styled.div` + display: flex; + align-items: center; + column-gap: 1.2rem; + margin-right: 1.5rem; +`; + +const StyledLeft = styled(Left)` + fill: ${props => props.theme.colors.neutral4}; + &:hover { + fill: ${props => props.theme.colors.primary}; + } +`; + +const StyledRight = styled(Right)` + fill: ${props => props.theme.colors.neutral4}; + &:hover { + fill: ${props => props.theme.colors.primary}; + } +`; + +const LogoutBtn = styled.button` + background-image: url(${logout}); + width: 1.4rem; + height: 1.4rem; + + &:hover { + filter: brightness(20%); + } +`; + +const DropdownWrap = styled.div` + ${props => props.theme.texts.tableTitle}; + font-size: 1.3rem; + display: flex; + align-items: center; + column-gap: 1.5rem; + + > img { + &:hover { + filter: brightness(20%); + } + } +`; + +const GroupWrap = styled.div` + display: flex; + column-gap: 1rem; + + > img { + &:hover { + filter: brightness(20%); + } + } +`; + +export default TopMenu; diff --git a/src/components/Header.tsx/TopNav.tsx b/src/components/Header.tsx/TopNav.tsx new file mode 100644 index 0000000..66293f8 --- /dev/null +++ b/src/components/Header.tsx/TopNav.tsx @@ -0,0 +1,27 @@ +import styled from 'styled-components'; + +function TopNav() { + return ( + + 학부생학사정보 + 학생서비스 + + ); +} + +const TopNavContatiner = styled.div` + display: flex; + flex-shrink: 0; +`; + +const TopNavWrap = styled.button` + ${props => props.theme.texts.title}; + font-size: 1.6rem; + margin-right: 4rem; + + &:focus { + color: ${props => props.theme.colors.primary}; + } +`; + +export default TopNav; diff --git a/src/components/Header.tsx/index.tsx b/src/components/Header.tsx/index.tsx new file mode 100644 index 0000000..1e99463 --- /dev/null +++ b/src/components/Header.tsx/index.tsx @@ -0,0 +1,37 @@ +import styled from 'styled-components'; +import logo from '@assets/img/main_logo.png'; +import TopNav from './TopNav'; +import TopMenu from './TopMenu'; + +function Header() { + return ( + +
+ +
+ + + + +
+ ); +} + +const HeaderContainer = styled.div` + border-top: 0.5rem solid ${props => props.theme.colors.primary}; + max-width: 100%; + height: 6rem; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; +`; + +const HeaderBox = styled.div` + display: flex; + flex-grow: 1; + justify-content: space-between; + margin-left: 7.5rem; +`; + +export default Header; diff --git a/src/components/LoginForm/FormInput.tsx b/src/components/LoginForm/FormInput.tsx index 16977d3..a05cdc0 100644 --- a/src/components/LoginForm/FormInput.tsx +++ b/src/components/LoginForm/FormInput.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; import {setType} from '.'; interface InputProps { - value: string | number; + value: string | number | undefined; setValue: React.Dispatch>; type: string; } diff --git a/src/components/LoginForm/index.tsx b/src/components/LoginForm/index.tsx index 94e64ca..8ce83f1 100644 --- a/src/components/LoginForm/index.tsx +++ b/src/components/LoginForm/index.tsx @@ -2,10 +2,10 @@ import styled from 'styled-components'; import FormInput from './FormInput'; import {useState} from 'react'; -export type setType = string | number; +export type setType = string | number | undefined; function LoginForm() { - const [id, setId] = useState(0); + const [id, setId] = useState(undefined); const [name, setName] = useState(''); return ( @@ -57,7 +57,7 @@ const CheckboxWrap = styled.div` const FindWrap = styled.div` ${props => props.theme.texts.tableTitle}; - color: #777; + color: ${props => props.theme.colors.neutral4}; float: inline-end; margin-bottom: 2.5rem; `; diff --git a/src/components/Menubar/Menu.tsx b/src/components/Menubar/Menu.tsx index f5cdfdb..93002d0 100644 --- a/src/components/Menubar/Menu.tsx +++ b/src/components/Menubar/Menu.tsx @@ -56,7 +56,7 @@ const MenuTitleBox = styled.div` align-items: center; justify-content: space-between; padding: 0 15px; - border-bottom: 1px solid ${props => props.theme.colors.neutral4}; + border-bottom: 1px solid ${props => props.theme.colors.neutral5}; `; const MenuTitleWrap = styled.div` ${props => props.theme.texts.menuTitle}; @@ -69,7 +69,7 @@ const MenuSubtitleBox = styled(MenuTitleBox)` `; const DetailBox = styled.div` - background-color: ${props => props.theme.colors.neutral5}; + background-color: ${props => props.theme.colors.neutral6}; display: flex; flex-direction: column; align-items: center; diff --git a/src/pages/index/Home.tsx b/src/pages/index/Home.tsx index 6dc66f2..ea7d540 100644 --- a/src/pages/index/Home.tsx +++ b/src/pages/index/Home.tsx @@ -1,17 +1,24 @@ import styled from 'styled-components'; import Menubar from '@components/Menubar'; +import Header from '@components/Header.tsx'; function Home() { return ( - - 강의시간표/수업계획서조회 +
+ + + 강의시간표/수업계획서조회 + ); } const Container = styled.div` ${props => props.theme.texts.title}; +`; + +const Box = styled.div` display: flex; `; diff --git a/src/styles/GlobalStyle.tsx b/src/styles/GlobalStyle.tsx index e37b293..1bef676 100644 --- a/src/styles/GlobalStyle.tsx +++ b/src/styles/GlobalStyle.tsx @@ -26,6 +26,8 @@ const GlobalStyle = createGlobalStyle` } input { border-radius: 2px; + outline: none; + border: none; } button { cursor: pointer; diff --git a/src/styles/theme/Theme.ts b/src/styles/theme/Theme.ts index 062f09e..10a72d4 100644 --- a/src/styles/theme/Theme.ts +++ b/src/styles/theme/Theme.ts @@ -6,8 +6,9 @@ const colors = { neutral1: '#222', neutral2: '#333', neutral3: '#444', - neutral4: '#E8E8E8', - neutral5: '#F7F7F7', + neutral4: '#777', + neutral5: '#E8E8E8', + neutral6: '#F7F7F7', blue: '#A9D0F5', black: '#000', white: '#FFF', From e0b1ee5e093b3f2e4043c9814af5ca0188c8323f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=A7=80=EC=9B=90?= <81554184+jeewonMoon@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:01:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EA=B0=95=EC=9D=98=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=ED=91=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rename: header 폴더명 변경 - Header.tsx -> Header으로 변경했습니다. * feat: 상단 탭 구현 - 상단에 탭 부분을 구현했습니다. - 아직 사이드 바와 연동되어 있지 않습니다. 탭 닫기 와 전체 닫기 버튼은 작동하지만 다시 탭을 열 수는 없습니다. - 테마에 선언한 텍스트 스타일의 이름을 변경했습니다. (tap->tab) * rename: 이미지 파일명 변경 * rename: 이미지 파일명 변경 * Revert "rename: 이미지 파일명 변경" This reverts commit c9bd819a2c8feee97fcde2ae1b8580ddff615756. * feat: 필터박스 구현 - 드롭다운, 인풋, 조회 버튼을 컴포넌트화 했습니다. - Warning: Received "true" for a non-boolean attribute 오류를 해결했습니다. - 스타일 요소들을 수정했습니다. * feat: 테이블 구현 - API 연결이 되어있지 않아 임의로 데이터를 만들어 구현했습니다. - 열의 항목들이 페이지마다 달라서 Table을 사용할 때 열 데이터를 만들어 전달해야 합니다. --- src/assets/data/filter.ts | 171 +++++++++++++++ src/assets/img/btn_gnb_cu.png | Bin 0 -> 989 bytes src/assets/img/search.png | Bin 1152 -> 1120 bytes src/assets/img/table_drodown.gif | Bin 0 -> 229 bytes src/assets/img/{red_edge.png => tag.png} | Bin src/assets/types/tableType.ts | 9 + .../{Header.tsx => Header}/TImer.tsx | 0 .../{Header.tsx => Header}/TopMenu.tsx | 0 .../{Header.tsx => Header}/TopNav.tsx | 0 .../{Header.tsx => Header}/index.tsx | 0 src/components/LectureList/Filters.tsx | 165 +++++++++++++++ src/components/LectureList/index.tsx | 108 ++++++++++ src/components/Menubar/BarTitle.tsx | 1 - src/components/Menubar/Menu.tsx | 4 +- src/components/Menubar/MenuItem.tsx | 8 +- src/components/TabMenu/Tab.tsx | 87 ++++++++ src/components/TabMenu/index.tsx | 73 +++++++ src/components/common/FilterButton.tsx | 39 ++++ src/components/common/FilterInput.tsx | 45 ++++ src/components/common/SelectBox.tsx | 198 ++++++++++++++++++ src/components/common/Table/TableHead.tsx | 159 ++++++++++++++ src/components/common/Table/index.tsx | 191 +++++++++++++++++ src/pages/index/Home.tsx | 23 +- src/styles/GlobalStyle.tsx | 19 ++ src/styles/theme/Theme.ts | 6 +- 25 files changed, 1294 insertions(+), 12 deletions(-) create mode 100644 src/assets/data/filter.ts create mode 100644 src/assets/img/btn_gnb_cu.png create mode 100644 src/assets/img/table_drodown.gif rename src/assets/img/{red_edge.png => tag.png} (100%) create mode 100644 src/assets/types/tableType.ts rename src/components/{Header.tsx => Header}/TImer.tsx (100%) rename src/components/{Header.tsx => Header}/TopMenu.tsx (100%) rename src/components/{Header.tsx => Header}/TopNav.tsx (100%) rename src/components/{Header.tsx => Header}/index.tsx (100%) create mode 100644 src/components/LectureList/Filters.tsx create mode 100644 src/components/LectureList/index.tsx create mode 100644 src/components/TabMenu/Tab.tsx create mode 100644 src/components/TabMenu/index.tsx create mode 100644 src/components/common/FilterButton.tsx create mode 100644 src/components/common/FilterInput.tsx create mode 100644 src/components/common/SelectBox.tsx create mode 100644 src/components/common/Table/TableHead.tsx create mode 100644 src/components/common/Table/index.tsx diff --git a/src/assets/data/filter.ts b/src/assets/data/filter.ts new file mode 100644 index 0000000..ea06a02 --- /dev/null +++ b/src/assets/data/filter.ts @@ -0,0 +1,171 @@ +export const term = [ + {id: 0, value: '2024/1학기'}, + {id: 1, value: '2024/2학기'}, +]; + +export const completion = [ + {id: 0, value: '-전체-'}, + {id: 1, value: '교양필수'}, + {id: 2, value: '공통교양필수'}, + {id: 3, value: '교양선택(1영역)'}, + {id: 4, value: '학문기초교양필수'}, + {id: 5, value: '교양선택'}, + {id: 6, value: '학문기초교양'}, + {id: 7, value: '전공기초'}, + {id: 8, value: '전공필수'}, + {id: 9, value: '전공선택'}, + {id: 10, value: '교직'}, + {id: 11, value: '무관후보생교육'}, +]; + +export const optional = [ + {id: 0, value: '-전체-'}, + {id: 1, value: '인성과도덕'}, + {id: 2, value: '역사와문화'}, + {id: 3, value: '사회와제도'}, + {id: 4, value: '생명과 과학'}, + {id: 5, value: '예술과생활'}, + {id: 6, value: '지구촌의이해'}, + {id: 7, value: '학문기초'}, + {id: 8, value: '인성과창의력'}, + {id: 9, value: '역량강화'}, + {id: 10, value: '사상과역사'}, + {id: 11, value: '사회와문화'}, + {id: 12, value: '융합과창업'}, + {id: 13, value: '자연과과학기술'}, + {id: 14, value: '세계와지구촌'}, + {id: 15, value: '예술과체육'}, + {id: 17, value: '역사와사상'}, + {id: 18, value: '자연과과학'}, + {id: 19, value: '경제와사회'}, + {id: 20, value: '문화와예술'}, + {id: 21, value: '사상과 역사'}, + {id: 22, value: '대학위성강좌'}, +]; + +export const major = [ + {id: 0, value: '-선택-'}, + {id: 1, value: 'AI로봇학과【3517 학부】인공지능융합대학'}, + {id: 2, value: 'AI연계융합전공【3317 학부】연계전공'}, + { + id: 3, + value: 'AI연계융합전공 소셜미디어매니지먼트소프트웨어【3328 학부】연계전공', + }, + { + id: 4, + value: + 'AI연계융합전공 스마트투어리즘매니지먼트소프트웨어【3331 학부】연계전공', + }, + {id: 5, value: 'AI연계융합전공 시스템생명공학【3324 학부】연계전공'}, + { + id: 6, + value: 'AI연계융합전공 에듀테크콘텐츠애널리틱스【3326 학부】연계전공', + }, + {id: 7, value: '건설환경공학과【2733 학부】공과대학'}, + {id: 8, value: '건축공학과【2720 학부】공과대학'}, + {id: 9, value: '건축공학부 건축공학전공【2779 학부】공과대학'}, + {id: 10, value: '건축공학부 건축학전공【2780 학부】공과대학'}, + {id: 11, value: '건축학과【2739 학부】공과대학'}, + {id: 12, value: '경영학부【2274 학부】경영경제대학'}, + {id: 13, value: '경제학과【2273 학부】경영경제대학'}, + {id: 14, value: '교육학과【2114 학부】인문과학대학'}, + {id: 15, value: '국방시스템공학과【2784 학부】공과대학'}, + {id: 17, value: '국어국문학과【2111 학부】인문과학대학'}, + {id: 18, value: '국제학부【2130 학부】인문과학대학'}, + {id: 19, value: '국제학부 영어영문학전공【2131 학부】인문과학대학'}, + {id: 20, value: '국제학부 일어일문학전공【2132 학부】인문과학대학'}, + {id: 21, value: '국제학부 중국통상학전공【2133 학부】인문과학대학'}, + {id: 22, value: '글로벌미디어소프트웨어 융합전공【3330 학부】연계전공'}, + {id: 23, value: '글로벌인재학부【2122 학부】인문과학대학'}, + {id: 24, value: '글로벌조리학과【3037 학부】호텔관광대학'}, + {id: 25, value: '기계공학과【2725 학부】공과대학'}, + {id: 26, value: '기계항공우주공학부 기계공학전공【2723 학부】공과대학'}, + {id: 27, value: '기계항공우주공학부 항공우주공학전공【2724 학부】공과대학'}, + {id: 28, value: '나노신소재공학과【2786 학부】공과대학'}, + {id: 29, value: '뉴미디어퍼포먼스 융합전공【3376 학부】연계전공'}, + {id: 30, value: '대양휴머니티칼리지【9005 학부】대양휴머니티칼리지'}, + {id: 31, value: '데이터사이언스학과【3225 학부】소프트웨어융합대학'}, + {id: 32, value: '디지털역사문화자원큐레이션융합전공【3395 학부】연계전공'}, + {id: 33, value: '럭셔리 브랜드 디자인 융합전공【3370 학부】연계전공'}, + {id: 34, value: '무용과【2515 학부】예체능대학'}, + {id: 35, value: '문화산업경영 융합전공【3366 학부】연계전공'}, + {id: 36, value: '물리천문학과【2450 학부】자연과학대학'}, + {id: 37, value: '미디어커뮤니케이션학과【2233 학부】사회과학대학'}, + {id: 38, value: '반도체시스템공학과【2931 학부】전자정보공학대학'}, + {id: 39, value: '반도체시스템공학과【3512 학부】인공지능융합대학'}, + {id: 40, value: '법학과【2053 학부】사회과학대학'}, + {id: 41, value: '법학부 법학전공【2052 학부】대학'}, + {id: 42, value: '비즈니스 애널리틱스 융합전공【3350 학부】연계전공'}, + {id: 43, value: '생명시스템학부【3140 학부】생명과학대학'}, + { + id: 44, + value: '생명시스템학부 바이오산업자원공학전공【3144 학부】생명과학대학', + }, + {id: 45, value: '생명시스템학부 바이오융합공학전공【3142 학부】생명과학대학'}, + {id: 46, value: '생명시스템학부 식품생명공학전공【3145 학부】생명과학대학'}, + {id: 47, value: '소프트웨어학과【3220 학부】소프트웨어융합대학'}, + {id: 48, value: '소프트웨어학과【3515 학부】인공지능융합대학'}, + {id: 49, value: '수학통계학과【2658 학부】자연과학대학'}, + {id: 50, value: '수학통계학부 수학전공【2648 학부】자연과학대학'}, + {id: 51, value: '수학통계학부 응용통계학전공【2649 학부】자연과학대학'}, + {id: 52, value: '스마트생명산업융합학과【3146 학부】생명과학대학'}, + {id: 53, value: '양자원자력공학과【2789 학부】공과대학'}, + {id: 54, value: '엔터테인먼트 소프트웨어 융합전공【3320 학부】연계전공'}, + {id: 55, value: '역사학과【2115 학부】인문과학대학'}, + {id: 56, value: '영상디자인 융합전공【3360 학부】연계전공'}, + {id: 57, value: '영화예술학과【2525 학부】예체능대학'}, + {id: 58, value: '예술융합콘텐츠 융합전공【3386 학부】연계전공'}, + {id: 59, value: '우주항공드론공학부【2757 학부】공과대학'}, + {id: 60, value: '우주항공드론공학부 항공시스템공학전공【2761 학부】공과대학'}, + { + id: 61, + value: '우주항공시스템공학부 항공시스템공학전공【2793 학부】공과대학', + }, + {id: 62, value: '융합창업전공【3310 학부】연계전공'}, + {id: 63, value: '음악과【2513 학부】예체능대학'}, + {id: 64, value: '인공지능데이터사이언스학과【3516 학부】인공지능융합대학'}, + {id: 65, value: '인공지능학과【3238 학부】소프트웨어융합대학'}, + {id: 66, value: '전자정보통신공학과【2930 학부】전자정보공학대학'}, + {id: 67, value: '전자정보통신공학과【3511 학부】인공지능융합대학'}, + {id: 68, value: '정보보호학과【3215 학부】소프트웨어융합대학'}, + {id: 69, value: '정보보호학과【3514 학부】인공지능융합대학'}, + {id: 70, value: '지구자원시스템공학과【2788 학부】공과대학'}, + {id: 71, value: '지능기전공학과【3233 학부】소프트웨어융합대학'}, + { + id: 72, + value: '지능기전공학부 무인이동체공학전공【3231 학부】소프트웨어융합대학', + }, + { + id: 73, + value: '지능기전공학부 스마트기기공학전공【3232 학부】소프트웨어융합대학', + }, + { + id: 74, + value: '창의소프트학부 디자인이노베이션전공【3236 학부】소프트웨어융합대학', + }, + { + id: 75, + value: '창의소프트학부 만화애니메이션텍전공【3237 학부】소프트웨어융합대학', + }, + {id: 76, value: '창의소프트학부【3518 학부】인공지능융합대학'}, + {id: 77, value: '체육학과【2514 학부】예체능대학'}, + {id: 78, value: '컴퓨터공학과【3210 학부】소프트웨어융합대학'}, + {id: 79, value: '컴퓨터공학과【3513 학부】인공지능융합대학'}, + {id: 80, value: '패션디자인학과【2536 학부】예체능대학'}, + {id: 81, value: '항공시스템공학과【2787 학부】공과대학'}, + {id: 82, value: '행정학과【2223 학부】사회과학대학'}, + {id: 83, value: '호텔관광외식경영학부【3029 학부】호텔관광대학'}, + { + id: 84, + value: '호텔관광외식경영학부 외식경영학전공【3036 학부】호텔관광대학', + }, + { + id: 85, + value: '호텔관광외식경영학부 호텔관광경영학전공【3035 학부】호텔관광대학', + }, + {id: 86, value: '호텔외식관광프랜차이즈경영학과【3033 학부】호텔관광대학'}, + {id: 87, value: '호텔외식비즈니스학과【3034 학부】호텔관광대학'}, + {id: 88, value: '화학과【2433 학부】자연과학대학'}, + {id: 89, value: '환경에너지공간융합학과【2790 학부】공과대학'}, + {id: 90, value: '환경에너지공간융합학과【2790 학부】공과대학'}, +]; diff --git a/src/assets/img/btn_gnb_cu.png b/src/assets/img/btn_gnb_cu.png new file mode 100644 index 0000000000000000000000000000000000000000..9caea972df5eebc6818e9cf1cdccc292c9ec1a7d GIT binary patch literal 989 zcmaJ=J#W)M7`7S#rBbDKg9xOPJFpOcrOwASrV4gKE!9<&SZX8~>e$!DYVC9F3vs$2 z!GuHw3!)tuSrDoi7?2pK1Pg1&POw0ckbr-{IZaXqf~E6)=y{&^dEa~QVQuBs^i+C^ zVVG%sNvqR&mX4Xp3Hra+NKMk|GO0941Fw^=6(XkW;1&Y9XKkQ5vYg)bCsbmX6mZQZ zX&TGW#vW%S7%uh#%4V3-VjNiZCL*APHe6p}f4+Oq0@qR4RoM`XKt*kLX(vRDofXsG z*|dueyLbbXVn_u%L@W?{TYdy%h3)G?x=xOH7W5%xQ(;d{HH{ijafpD-2^m|+2_PqO zVs<{C&tC_UAPT%7@uHLw^DrwxK>&k~rO`rX9oDt#AQs&zY@3h(@_e`3<+@o8ha0?D zEEW?DNy<<}ChGabiZgyRH&oD2WQT4*TUkq{MC$0w?>~hjvlk$~jw0L%ZF+I*b4jQGsv)un4o!@=hE=S!F~LzA z2S9DeVAb^<+>QEZ!+^RU5zDuct|=_#bFS;as+KFLvX(8Yd07-I6I1j;O5%>KV=IxTmS$7 literal 0 HcmV?d00001 diff --git a/src/assets/img/search.png b/src/assets/img/search.png index 78880d70da497b974f87835278f18507baced5fd..ed1c40478afaa917ad13715ec0dd33d0fe2fd93c 100644 GIT binary patch delta 457 zcmZqRe88dD8Q|y6%O%Cdz`(%k>ERLtqJfx?gAGW2sh@UfqoOiXjGL>0i-m!?qobv( zi=m;bqotXVk%hUjg{g^|i;01So039BZh^0_l}mndX>Mv>iKmOL5>P}hB{Rhes@DmZ z-pLD?3>2V>EO09Nz$61xWQkLe4zp8zZQes8pwq8;x;TbZ+)DcK|Gz!6$Z5upCJJ>w z=m95II-j6HE95SZ_Dp5bC(O;7i$xAM7d8jz>~f91$x? zclemK;!jG7um6IkT^1%Uou1A=a73c>(w5!SvywKN(VcXG(B1_dC2{JFFVdQ&MBb@0B!o3g8%>k delta 490 zcmaFB(ZH$L8Q|y6%O%Cdz`(%k>ERLtq~(BEh=UDC`h3c}xlvJ>DaOgv*wM(?!qm~s zz`)SZ)zQ+)#mUmy+|0<(*vQ=3&_qe0BDcWT*UBY7ximL5uf)^ERtYGgmy(%c1=VYY zOYh_bOa=;2MaDQ4ePEJ-DKf#SNQc>}{%k|vX`s`;dAc};RNPAX^Z);T5oYGY#%#Zs zG}YP|nU+N8uphbH@Iz7HfBPllg^!jn2Lv!1GBdv6Ix_!%GW-06pLkRfG?R3uF+buD z@ZQF}QB`2G$XMi-p8zHsH@sInlJQ(mtZtenZ{wchzuP}dc| zn3hk5JSvwDCCxaW&h)W(h3ol+e+Z^fkB2r2P6QplYuo!VS8_iXV<}{YjvtwA2XjdNlTt!e6Oazcjkfu z4HlvPnKzOXJ1pk*w3n2uG!yY}TvD~LNU-Sy(); + const handleSelect = ( + name: keyof LectureProps, + value: string | undefined, + ) => { + let dept = ''; + let colledge = ''; + + if (name === 'schDeptAlias') { + dept = value!.substring(0, value!.indexOf('【')); + colledge = value!.substring(value!.indexOf('】') + 1); + + setLecture(prevState => ({ + ...prevState, + schDeptAlias: dept, + schCollegeAlias: colledge, + })); + } else { + setLecture(prevState => ({ + ...prevState, + [name]: value, + })); + } + }; + + return ( + + + + + 조직분류 + handleSelect('curiTypeCdNm', value)} + /> + + + 년도/학기 + handleSelect('curiTypeCdNm', value)} + /> + + + 이수구분 + handleSelect('curiTypeCdNm', value)} + /> + + + 선택영역 + handleSelect('sltDomainCdNm', value)} + /> + + + 학과전공 + handleSelect('schDeptAlias', value)} + /> + + + 교과목명 + handleSelect('curiNm', value)} + /> + + + 교수명 + handleSelect('lesnEmp', value)} + /> + + + + + +

+ ※ 교양과목(중필, 중선, 전공기초교양, 자유선택교양), 교직과목, + ROTC과목은 개설학과전공을 대양휴머니티칼리지(또는 교양학부)로 하여 + 조회하시기 바랍니다. +

+

+ ※ 과목에 대한 문의는 개설학과가 아닌 주관학과에 문의하시기 바라며, + 영어과목에 대한 문의는 교양영어실로 문의하시기 바랍니다. +

+
+
+ ); +} + +const FilterContainer = styled.div` + border: 0.1rem solid #714656; + border-radius: 2px; + padding: 0.5rem 1.5rem; + margin-bottom: 2rem; +`; + +const FilterBox = styled.div` + display: flex; + flex-wrap: wrap; + gap: 0.7rem 3rem; +`; + +const FilterArea = styled.div` + display: flex; + align-items: flex-end; + margin-bottom: 1rem; +`; + +const FilterWrap = styled.div` + ${props => props.theme.texts.tableTitle}; + > span { + display: inline-block; + margin-right: 1rem; + text-align: right; + min-width: 4.5rem; + } +`; + +const WarningWrap = styled.div` + ${props => props.theme.texts.warning}; + color: #c30e2e; + margin-bottom: -1.5rem; + + > p { + margin-bottom: 1.5rem; + } +`; + +export default Filters; diff --git a/src/components/LectureList/index.tsx b/src/components/LectureList/index.tsx new file mode 100644 index 0000000..688a79a --- /dev/null +++ b/src/components/LectureList/index.tsx @@ -0,0 +1,108 @@ +import styled from 'styled-components'; +import Filters from './Filters'; +import Table from '@components/common/Table'; + +const data = [ + { + schDeptAlias: '대양휴머니티칼리지', + curiNo: '011312', + class_: '001', + schCollegeAlias: '대양휴머니티칼리지', + curiNm: '경영학', + curiLangNm: null, + curiTypeCdNm: '균형교양필수', + sltDomainCdNm: '경제와사회', + tmNum: '3.0 / 3 / 0', + studentYear: '2', + corsUnitGrpCdNm: '학사', + manageDeptNm: '대양휴머니티칼리지', + lesnEmp: '이지훈', + lesnTime: '목 18:00~19:00', + lesnRoom: '집301', + cyberTypeCdNm: '본교 e-러닝강의', + internshipTypeCdNm: null, + inoutSubCdtExchangeYn: null, + remark: '사회과학,경영경제,호텔관광대학2 제외', + }, + { + schDeptAlias: '대양휴머니티칼리지', + curiNo: '011312', + class_: '001', + schCollegeAlias: '대양휴머니티칼리지', + curiNm: '경영학', + curiLangNm: null, + curiTypeCdNm: '균형교양필수', + sltDomainCdNm: '경제와사회', + tmNum: '3.0 / 3 / 0', + studentYear: '2', + corsUnitGrpCdNm: '학사', + manageDeptNm: '대양휴머니티칼리지', + lesnEmp: '이지훈', + lesnTime: '목 18:00~19:00', + lesnRoom: '집301', + cyberTypeCdNm: '본교 e-러닝강의', + internshipTypeCdNm: null, + inoutSubCdtExchangeYn: null, + remark: '사회과학,경영경제,호텔관광대학2 제외', + }, + { + schDeptAlias: '대양휴머니티칼리지', + curiNo: '011312', + class_: '001', + schCollegeAlias: '대양휴머니티칼리지', + curiNm: '경제학', + curiLangNm: null, + curiTypeCdNm: '균형교양필수', + sltDomainCdNm: '경제와사회', + tmNum: '3.0 / 3 / 0', + studentYear: '2', + corsUnitGrpCdNm: '학사', + manageDeptNm: '대양휴머니티칼리지', + lesnEmp: '이지훈', + lesnTime: '목 18:00~19:00', + lesnRoom: '집301', + cyberTypeCdNm: '본교 e-러닝강의', + internshipTypeCdNm: null, + inoutSubCdtExchangeYn: null, + remark: '사회과학,경영경제,호텔관광대학2 제외', + }, +]; + +const colData = [ + {name: 'schDeptAlias', value: '개설학과전공', initialWidth: 167}, + {name: 'curiNo', value: '학수번호', initialWidth: 92}, + {name: 'class_', value: '분반', initialWidth: 58}, + {name: 'curiNm', value: '교과목명', initialWidth: 232}, + {name: 'curiLangNm', value: '강의언어', initialWidth: 73}, + {name: 'curiTypeCdNm', value: '이수구분'}, + {name: 'sltDomainCdNm', value: '선택영역', initialWidth: 136}, + {name: 'tmNum', value: '학점/이론/실습', initialWidth: 134}, + {name: 'studentYear', value: '학년 (학기)'}, + {name: 'corsUnitGrpCdNm', value: '대상과정'}, + {name: 'manageDeptNm', value: '주관학과', initialWidth: 135}, + {name: 'lesnEmp', value: '교수명'}, + {name: 'lesnTime', value: '요일 및 강의시간', initialWidth: 130}, + {name: 'lesnRoom', value: '강의실', initialWidth: 114}, + {name: 'cyberTypeCdNm', value: '사이버강좌', initialWidth: 104}, + {name: 'internshipTypeCdNm', value: '강좌유형', initialWidth: 126}, + {name: 'inoutSubCdtExchangeYn', value: '학점교류수강가능', initialWidth: 130}, + {name: 'remark', value: '수강대상및유의사항', initialWidth: 230}, +]; + +function LectureList() { + return ( + + + + + ); +} + +const ListContainer = styled.div``; + +export default LectureList; diff --git a/src/components/Menubar/BarTitle.tsx b/src/components/Menubar/BarTitle.tsx index 90a0e27..9b6d6f7 100644 --- a/src/components/Menubar/BarTitle.tsx +++ b/src/components/Menubar/BarTitle.tsx @@ -23,7 +23,6 @@ function BarTitle({setIsOpen}: OpenProps) { } const BarTitleContainer = styled.div` - background: rgb(163, 20, 50); background: linear-gradient( 90deg, rgba(163, 20, 50, 1) 0%, diff --git a/src/components/Menubar/Menu.tsx b/src/components/Menubar/Menu.tsx index 93002d0..d29fe77 100644 --- a/src/components/Menubar/Menu.tsx +++ b/src/components/Menubar/Menu.tsx @@ -11,9 +11,9 @@ interface ItemProps { } const menuItems: ItemProps[] = [ - {id: 0, name: '수강신청', type: 'study'}, + {id: 0, name: '강의시간표 조회', type: 'view'}, {id: 1, name: '관심과목 담기', type: 'study'}, - {id: 2, name: '강의시간표 조회', type: 'view'}, + {id: 2, name: '수강신청', type: 'study'}, ]; function Menu() { diff --git a/src/components/Menubar/MenuItem.tsx b/src/components/Menubar/MenuItem.tsx index 80fd08a..2b6f1bb 100644 --- a/src/components/Menubar/MenuItem.tsx +++ b/src/components/Menubar/MenuItem.tsx @@ -12,14 +12,14 @@ interface DetailProps { function MenuItem({id, type, name, isActive, onClick}: DetailProps) { return ( - onClick(id)}> + onClick(id)}> {type === 'view' ? : } {name} ); } -const DetailWrap = styled.button<{isactive: boolean}>` +const DetailWrap = styled.button<{$isactive: boolean}>` ${props => props.theme.texts.tableTitle}; width: 17.5rem; height: 2.8rem; @@ -29,8 +29,8 @@ const DetailWrap = styled.button<{isactive: boolean}>` padding-left: 10px; background-color: ${props => - props.isactive ? props.theme.colors.primary : 'transparent'}; - color: ${props => props.isactive && props.theme.colors.white}; + props.$isactive ? props.theme.colors.primary : 'transparent'}; + color: ${props => props.$isactive && props.theme.colors.white}; `; export default MenuItem; diff --git a/src/components/TabMenu/Tab.tsx b/src/components/TabMenu/Tab.tsx new file mode 100644 index 0000000..8a34af7 --- /dev/null +++ b/src/components/TabMenu/Tab.tsx @@ -0,0 +1,87 @@ +import {useState} from 'react'; +import styled, {css} from 'styled-components'; +import close from '@assets/img/tab_close.png'; + +interface TabProps { + id: number; + label: string; + isActive: boolean; + onClick: (id: number) => void; +} + +function Tab({id, label, isActive, onClick}: TabProps) { + const [isOpen, setIsOpen] = useState(true); + + const handleClose = (e: React.MouseEvent) => { + e.stopPropagation(); + + if (id > 0) { + onClick(id - 1); + } else { + onClick(1); + } + + setIsOpen(false); + }; + + return ( + <> + {isOpen && ( + onClick(id)} $isactive={isActive}> +

{label}

+ +
+ )} + + ); +} + +const TabContainer = styled.a<{$isactive: boolean}>` + ${props => + props.$isactive + ? props.theme.texts.tabTitleFocus + : props.theme.texts.tabTitle}; + background-color: ${props => + props.$isactive ? props.theme.colors.white : 'transparent'}; + width: calc(99% / 8); + height: 100%; + border-top: 0.3rem solid + ${props => (props.$isactive ? props.theme.colors.primary : 'none')}; + border-right: 1px solid #ccc; + border-radius: 0; + padding: 0 1rem; + display: flex; + align-items: center; + text-align: center; + cursor: pointer; + filter: ${props => (props.$isactive ? 'grayscale(0)' : 'grayscale(100%)')}; + + > p { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-all; + margin-right: 1.5rem; + } + + ${props => + !props.$isactive && + css` + &:hover { + background-color: white; + filter: grayscale(0); + } + `} +`; + +const CloseBtn = styled.button` + z-index: 5; + width: 1rem; + height: 100%; + background-image: url(${close}); + background-repeat: no-repeat; + background-position-y: center; +`; + +export default Tab; diff --git a/src/components/TabMenu/index.tsx b/src/components/TabMenu/index.tsx new file mode 100644 index 0000000..ea0f137 --- /dev/null +++ b/src/components/TabMenu/index.tsx @@ -0,0 +1,73 @@ +import {useState} from 'react'; +import styled from 'styled-components'; +import Tab from './Tab'; +import closeAll from '@assets/img/tab_close_all.png'; +import up from '@assets/img/btn_gnb_cu.png'; + +function TabMenu() { + const [focused, setFocused] = useState(0); + const [close, setClose] = useState(false); + + const handleClick = (id: number) => { + setFocused(id); + }; + + return ( + + + {!close && ( + <> + + + + )} + + + setClose(true)} /> + + + + ); +} + +const TabMenuContainer = styled.div` + width: 100%; + height: 4rem; + background-color: ${props => props.theme.colors.neutral6}; + display: flex; + justify-content: space-between; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +`; + +const TabWrap = styled.div` + width: 100%; + display: flex; +`; + +const ButtonWrap = styled.div` + display: flex; + align-items: center; + margin-right: 1rem; + column-gap: 0.8rem; +`; + +const CloseAllBtn = styled.button` + width: 1.2rem; + height: 100%; + background-image: url(${closeAll}); + background-repeat: no-repeat; + background-position-y: center; +`; + +export default TabMenu; diff --git a/src/components/common/FilterButton.tsx b/src/components/common/FilterButton.tsx new file mode 100644 index 0000000..35846f4 --- /dev/null +++ b/src/components/common/FilterButton.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import search from '@assets/img/search.png'; +import {LectureProps} from '@components/LectureList/Filters'; + +interface ButtonProps { + label: string; + lecture: LectureProps | undefined; +} + +function FilterButton({label, lecture}: ButtonProps) { + const handleClick = () => { + console.log(lecture); + }; + + return ( + + + {label} + + ); +} + +const ButtonWrap = styled.button` + ${props => props.theme.texts.content}; + background: linear-gradient( + 90deg, + rgba(163, 20, 50, 1) 0%, + rgba(51, 77, 97, 1) 100% + ); + color: ${props => props.theme.colors.white}; + min-width: 6.5rem; + height: 2.4rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.8rem; +`; + +export default FilterButton; diff --git a/src/components/common/FilterInput.tsx b/src/components/common/FilterInput.tsx new file mode 100644 index 0000000..68a36d5 --- /dev/null +++ b/src/components/common/FilterInput.tsx @@ -0,0 +1,45 @@ +import styled, {css} from 'styled-components'; + +interface InputInterface { + sizes: string; + onChange: (value: string) => void; +} + +function FilterInput({sizes, onChange}: InputInterface) { + return ( + <> + ) => + onChange(e.target.value) + } + /> + + ); +} + +const InputWrap = styled.input<{sizes: string}>` + ${props => props.theme.texts.content}; + + ${props => + props.sizes === 's' && + css` + width: 14rem; + `}; + ${props => + props.sizes === 'm' && + css` + width: 19.5rem; + `}; + ${props => + props.sizes === 'xl' && + css` + width: 48.5rem; + `}; + + height: 2.4rem; + border: 1px solid ${props => props.theme.colors.neutral5}; + padding-left: 0.8rem; +`; + +export default FilterInput; diff --git a/src/components/common/SelectBox.tsx b/src/components/common/SelectBox.tsx new file mode 100644 index 0000000..ed1a4f2 --- /dev/null +++ b/src/components/common/SelectBox.tsx @@ -0,0 +1,198 @@ +import {useEffect, useRef, useState} from 'react'; +import styled, {css} from 'styled-components'; +import arrow from '@assets/img/input_dropdown.png'; +import tag from '@assets/img/tag.png'; + +interface OptionsInterface { + id: number; + value: string; +} + +interface SelectProps { + options: OptionsInterface[]; + tagged: boolean; + disabled?: boolean; + sizes: string; + onSelect: (value: string) => void; +} + +function SelectBox({ + options, + tagged, + disabled = false, + sizes, + onSelect, +}: SelectProps) { + const [open, setOpen] = useState(false); + const [input, setInput] = useState(options[0].value); + const [selected, setSelected] = useState(options[0].value); + const [filtered, setFiltered] = useState(options); + const dropdownRef = useRef(null); + + const handleBtnClick = () => { + if (!disabled) { + setFiltered(options); + setOpen(prev => !prev); + } + }; + + const handleInput = (e: React.ChangeEvent) => { + setInput(e.target.value); + setFiltered( + options.filter(option => option.value.includes(e.target.value)), + ); + setOpen(true); + }; + + const handleOptionClick = (value: string) => { + onSelect(value); + setInput(value); + setSelected(value); + setOpen(false); + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + if (open) { + setOpen(false); + setInput(selected); + } + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [open]); + + return ( + + + {tagged && } + + + + {open && ( + + {filtered.map(option => ( + handleOptionClick(option.value)} + > + {option.value} + + ))} + + )} + + ); +} + +const SelectContainer = styled.div<{sizes: string}>` + ${props => props.theme.texts.content}; + ${props => + props.sizes === 's' && + css` + width: 15rem; + `}; + ${props => + props.sizes === 'm' && + css` + width: 20.5rem; + `}; + ${props => + props.sizes === 'xl' && + css` + width: 50rem; + `}; + height: 2.4rem; + position: relative; + display: inline-block; +`; + +const InputContainer = styled.div<{disabled: boolean}>` + position: relative; + width: inherit; + height: inherit; + border: 1px solid ${props => props.theme.colors.neutral5}; + + &:hover { + background-color: ${props => props.theme.colors.blue}; + } + + &:disabled { + background-color: ${props => props.theme.colors.neutral5}; + } +`; + +const TagWrap = styled.img<{disabled: boolean}>` + position: absolute; + z-index: 2; + filter: ${props => (props.disabled ? 'grayscale(100%)' : 'none')}; +`; + +const InputWrap = styled.input` + ${props => props.theme.texts.content}; + width: calc(100% - 1rem); + height: inherit; + padding: 0 0 0 1rem; + + &:hover { + background-color: ${props => props.theme.colors.blue}; + } + + &:read-only { + background-color: ${props => props.theme.colors.neutral5}; + } +`; + +const ArrowWrap = styled.img` + position: absolute; + right: 0.3rem; + top: 10%; + border: 1px solid transparent; + border-radius: 5px; + + &:hover { + border: 1px solid ${props => props.theme.colors.neutral5}; + } +`; + +const SelectWrap = styled.ul` + width: inherit; + max-height: 12rem; + position: absolute; + top: 100%; + z-index: 5; + border: 1px solid ${props => props.theme.colors.neutral5}; + background-color: ${props => props.theme.colors.white}; + overflow-y: scroll; +`; + +const OptionWrap = styled.li<{$isselected: boolean}>` + height: 2.4rem; + display: flex; + align-items: center; + padding-left: 1rem; + + ${props => + props.$isselected && + css` + background-color: ${props => props.theme.colors.neutral6}; + color: ${props => props.theme.colors.primary}; + `} + + &:hover { + background-color: ${props => props.theme.colors.neutral6}; + color: ${props => props.theme.colors.primary}; + } +`; + +export default SelectBox; diff --git a/src/components/common/Table/TableHead.tsx b/src/components/common/Table/TableHead.tsx new file mode 100644 index 0000000..7871db5 --- /dev/null +++ b/src/components/common/Table/TableHead.tsx @@ -0,0 +1,159 @@ +import styled from 'styled-components'; +import dropdown from '@assets/img/table_drodown.gif'; +import {useEffect, useRef, useState} from 'react'; + +interface HeadProps { + label: string; + index: number; + width: number; + options: string[]; + handleMouseDown: (index: number) => (event: React.MouseEvent) => void; + selectedOptions: string[]; + onFilterChange: (index: number, selectedOptions: string[]) => void; +} + +function TableHead({ + label, + index, + width, + options, + selectedOptions, + onFilterChange, + handleMouseDown, +}: HeadProps) { + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + + const handleCheckboxChange = (option: string) => { + const updatedSelection = selectedOptions.includes(option) + ? selectedOptions.filter(selectedOption => selectedOption !== option) + : [...selectedOptions, option]; + + if (updatedSelection.length === options.length) { + onFilterChange(index, options); + } else { + onFilterChange(index, updatedSelection); + } + }; + + const handleSelectAll = () => { + if (selectedOptions.length === options.length) { + onFilterChange(index, []); + } else { + onFilterChange(index, options); + } + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [open]); + + return ( + +
+ {label} + setOpen(prev => !prev)} /> + {open && ( + + + + + + {options?.map((option, index) => ( + + handleCheckboxChange(option)} + /> + + + ))} + + )} + +
+
+ ); +} + +const Wrap = styled.th<{width: number}>` + min-width: ${props => props.width}px; + text-align: ${props => (props.width > 100 ? 'center' : 'left')}; + + > div { + display: flex; + justify-content: space-between; + align-items: center; + + > span { + text-align: center; + flex-grow: 1; + } + } +`; + +const Resizer = styled.div` + width: 5px; + height: 100%; + position: absolute; + right: 0; + top: 0; + bottom: 0; + cursor: col-resize; + background-color: transparent; + z-index: 1; +`; + +const DropdownBtn = styled.button` + width: 1.5rem; + height: 1.5rem; + background: url(${dropdown}) no-repeat center; +`; + +const OptionBox = styled.ul` + width: 100%; + overflow: scroll; + background: white; + position: absolute; + top: 3rem; + left: 0; +`; + +const OptionWrap = styled.li` + ${props => props.theme.texts.content}; + text-align: left; + border-bottom: 1px solid #b3b3b3; + color: #066196; + height: 2rem; + + > label { + display: inline-block; + width: 86%; + height: 100%; + cursor: pointer; + } +`; + +export default TableHead; diff --git a/src/components/common/Table/index.tsx b/src/components/common/Table/index.tsx new file mode 100644 index 0000000..9440238 --- /dev/null +++ b/src/components/common/Table/index.tsx @@ -0,0 +1,191 @@ +import {useEffect, useRef, useState} from 'react'; +import styled from 'styled-components'; +import TableHead from './TableHead'; +import {TableHeadTypes, TableTypes} from '@assets/types/tableType'; + +interface TableProps { + colData: TableHeadTypes[]; + data: TableTypes[]; + initialWidth: string; + height: string; +} + +function Table({data, colData, initialWidth, height}: TableProps) { + const tableRef = useRef(null); + const [columnWidths, setColumnWidths] = useState([]); + const [filters, setFilters] = useState( + colData.map(col => { + const uniqueOptions = Array.from( + new Set(data.map(row => row[col.name])), + ).filter(option => option !== null) as string[]; + return uniqueOptions.length === 0 ? ['빈값'] : uniqueOptions.sort(); + }), + ); + + useEffect(() => { + if (tableRef.current) { + const initialWidths = Array.from( + tableRef.current.querySelectorAll('th'), + ).map(th => th.getBoundingClientRect().width); + setColumnWidths(initialWidths); + } + }, [tableRef]); + + const handleMouseDown = (index: number) => (event: React.MouseEvent) => { + const startX = event.clientX; + const startWidth = columnWidths[index]; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const newWidth = startWidth + (moveEvent.clientX - startX); + setColumnWidths(prevWidths => + prevWidths.map((width, i) => (i === index ? newWidth : width)), + ); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + const getOptions = colData.map(col => { + const uniqueOptions = Array.from( + new Set(data.map(row => row[col.name])), + ).filter(option => option !== null) as string[]; + return uniqueOptions.length === 0 ? ['빈값'] : uniqueOptions.sort(); + }); + + const handleFilterChange = (index: number, selectedOptions: string[]) => { + setFilters(prevFilters => { + const newFilters = [...prevFilters]; + newFilters[index] = selectedOptions; + return newFilters; + }); + }; + + const filteredData = data.filter(row => + colData.every( + (col, index) => + filters[index].includes('빈값') || + filters[index].includes(row[col.name] ?? ''), + ), + ); + + return ( + + + 개설강좌 + + + +
+ + {colData.map((item, index) => ( + + ))} + + + + + {colData.map((item, index) => ( + + ))} + + + + {filteredData.map((row, rowIdx) => ( + + {rowIdx + 1} + {colData.map((col, colIdx) => ( + + ))} + + ))} + + + + + ); +} + +const TableContainer = styled.div``; + +const TableTitleWrap = styled.div` + margin-bottom: 1rem; +`; + +const TableTitle = styled.div` + ${props => props.theme.texts.subtitle}; + border-left: 4px solid ${props => props.theme.colors.primary}; + padding-left: 0.5rem; +`; + +const TableBox = styled.div<{width: string; height: string}>` + width: ${props => props.width}; + height: ${props => props.height}; + overflow: scroll; + border-left: 1px solid #c3c3c3; + border-bottom: 1px solid #c3c3c3; +`; + +const TableWrap = styled.table` + ${props => props.theme.texts.content}; + border-top: 1px solid ${props => props.theme.colors.black}; + white-space: nowrap; + border-collapse: collapse; + + > thead > tr > th { + ${props => props.theme.texts.tableTitle}; + position: relative; + border-top: 1px solid black; + background-color: ${props => props.theme.colors.neutral5}; + } +`; + +const RowWrap = styled.tr` + height: 3rem; + > th, + td { + border: 1px solid #c3c3c3; + border-left: none; + padding: 0 0.5rem; + vertical-align: middle; + } +`; + +const IndexWrap = styled.td` + background-color: ${props => props.theme.colors.blue}; + text-align: center; +`; + +const ContentWrap = styled(RowWrap)<{$isEven: boolean}>` + background-color: ${props => + props.$isEven ? 'rgb(252, 252, 252)' : props.theme.colors.white}; + text-align: center; + &:hover { + background-color: rgb(250, 235, 238); + } + + &:focus { + background-color: rgb(252, 248, 227); + } +`; + +export default Table; diff --git a/src/pages/index/Home.tsx b/src/pages/index/Home.tsx index ea7d540..0ce6881 100644 --- a/src/pages/index/Home.tsx +++ b/src/pages/index/Home.tsx @@ -1,6 +1,8 @@ import styled from 'styled-components'; import Menubar from '@components/Menubar'; -import Header from '@components/Header.tsx'; +import Header from '@components/Header'; +import LectureList from '@components/LectureList'; +import TabMenu from '@components/TabMenu'; function Home() { return ( @@ -8,7 +10,13 @@ function Home() {
- 강의시간표/수업계획서조회 +
+ +
+

강의시간표/수업계획서조회

+ +
+
); @@ -22,4 +30,15 @@ const Box = styled.div` display: flex; `; +const Main = styled.div` + width: 100%; +`; + +const Article = styled.div` + padding: 2rem 1rem; + > p { + margin-bottom: 1.5rem; + } +`; + export default Home; diff --git a/src/styles/GlobalStyle.tsx b/src/styles/GlobalStyle.tsx index 1bef676..acb27b5 100644 --- a/src/styles/GlobalStyle.tsx +++ b/src/styles/GlobalStyle.tsx @@ -28,6 +28,7 @@ const GlobalStyle = createGlobalStyle` border-radius: 2px; outline: none; border: none; + padding: 0; } button { cursor: pointer; @@ -37,6 +38,24 @@ const GlobalStyle = createGlobalStyle` border:none; padding:0; } + select::-ms-expand { + display: none; + } + select { + -o-appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + outline: none; + } + thead, + tbody { + white-space: nowrap; + } + thead { + position: sticky; + top: 0; + } `; export default GlobalStyle; diff --git a/src/styles/theme/Theme.ts b/src/styles/theme/Theme.ts index 10a72d4..072c622 100644 --- a/src/styles/theme/Theme.ts +++ b/src/styles/theme/Theme.ts @@ -9,7 +9,7 @@ const colors = { neutral4: '#777', neutral5: '#E8E8E8', neutral6: '#F7F7F7', - blue: '#A9D0F5', + blue: '#E5EFF7', black: '#000', white: '#FFF', }; @@ -40,12 +40,12 @@ const texts = { fontWeight: '600', color: '#FAEBEE', }, - tapTitle: { + tabTitle: { fontSize: '1.3rem', fontWeight: '400', color: '#444', }, - tapTitleFocus: { + tabTitleFocus: { fontSize: '1.3rem', fontWeight: '600', color: '#a31432', From 2bd3e93c6432dff3be9cd4e6b2171cec7a287449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=A7=80=EC=9B=90?= <81554184+jeewonMoon@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:01:12 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20api=20=EB=AA=A8=EB=93=88=ED=99=94?= =?UTF-8?q?=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - axios를 설치했습니다. - axios 인스턴스를 생성했습니다. - authAPI는 로그인 구현과 함께 수정하겠습니다. --- src/apis/api/course.ts | 10 ++++++++++ src/apis/utils/instance.ts | 20 ++++++++++++++++++++ src/pages/index/Home.tsx | 2 +- src/styles/GlobalStyle.tsx | 2 +- tsconfig.app.json | 15 ++++++++------- vite.config.ts | 10 +++++++--- 6 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/apis/api/course.ts create mode 100644 src/apis/utils/instance.ts diff --git a/src/apis/api/course.ts b/src/apis/api/course.ts new file mode 100644 index 0000000..cc2eb87 --- /dev/null +++ b/src/apis/api/course.ts @@ -0,0 +1,10 @@ +import {baseAPI} from '../utils/instance'; + +export const getCourseList = async (body: object) => { + try { + const {data} = await baseAPI.get('/schedules/search', body); + return data; + } catch (error) { + console.log('get course list fail: ', error); + } +}; diff --git a/src/apis/utils/instance.ts b/src/apis/utils/instance.ts new file mode 100644 index 0000000..451ff9d --- /dev/null +++ b/src/apis/utils/instance.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; + +const baseURL = import.meta.env.VITE_BASE_URL; + +export const baseAPI = axios.create({ + baseURL: baseURL, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, +}); + +// 토큰 받아오는 작업 필요 +export const authAPI = axios.create({ + baseURL: baseURL, + headers: { + // Authorization: `Bearer ${token}`, + }, + withCredentials: true, +}); diff --git a/src/pages/index/Home.tsx b/src/pages/index/Home.tsx index 0ce6881..e56aa9c 100644 --- a/src/pages/index/Home.tsx +++ b/src/pages/index/Home.tsx @@ -31,7 +31,7 @@ const Box = styled.div` `; const Main = styled.div` - width: 100%; + width: calc(100% - 23rem); `; const Article = styled.div` diff --git a/src/styles/GlobalStyle.tsx b/src/styles/GlobalStyle.tsx index acb27b5..1ca4b7e 100644 --- a/src/styles/GlobalStyle.tsx +++ b/src/styles/GlobalStyle.tsx @@ -54,7 +54,7 @@ const GlobalStyle = createGlobalStyle` } thead { position: sticky; - top: 0; + top: -1px; } `; diff --git a/tsconfig.app.json b/tsconfig.app.json index 5fb7ab2..8eb668a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -23,14 +23,15 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "baseUrl": ".", + "baseUrl": "./src", "paths": { - "@components/*": ["src/components/*"], - "@pages/*": ["src/pages/*"], - "@assets/*": ["src/assets/*"], - "@store/*": ["src/store/*"], - "@plugins/*": ["src/plugins/*"], - "@/*": ["./src/*"] + "@/*": ["./*"], + "@components/*": ["components/*"], + "@pages/*": ["pages/*"], + "@assets/*": ["assets/*"], + "@store/*": ["store/*"], + "@plugins/*": ["plugins/*"], + "@apis/*": ["apis/*"] }, "allowSyntheticDefaultImports": true }, diff --git a/vite.config.ts b/vite.config.ts index 9dd4100..64fb0dc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from 'vite'; +import {defineConfig} from 'vite'; import react from '@vitejs/plugin-react'; -import { resolve } from 'node:path'; +import {resolve} from 'node:path'; import svgr from 'vite-plugin-svgr'; // https://vitejs.dev/config/ @@ -12,7 +12,7 @@ export default defineConfig({ resolve: { alias: [ { - find: '@src', + find: '@', replacement: resolve(__dirname, './src'), }, { @@ -35,6 +35,10 @@ export default defineConfig({ find: '@plugins', replacement: resolve(__dirname, './src/plugins'), }, + { + find: '@apis', + replacement: resolve(__dirname, './src/apis'), + }, ], }, });
순번
{row[col.name]}