diff --git a/public/img/Daily-Challenge.svg b/public/img/Daily-Challenge.svg new file mode 100644 index 00000000..838affc5 --- /dev/null +++ b/public/img/Daily-Challenge.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/algorithm_tag_icon.svg b/public/img/algorithm_tag_icon.svg new file mode 100644 index 00000000..8d343e6c --- /dev/null +++ b/public/img/algorithm_tag_icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/img/arrow-l-white.svg b/public/img/arrow-l-white.svg new file mode 100644 index 00000000..52cb9671 --- /dev/null +++ b/public/img/arrow-l-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/arrow-r-white.svg b/public/img/arrow-r-white.svg new file mode 100644 index 00000000..d225c354 --- /dev/null +++ b/public/img/arrow-r-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/bling.svg b/public/img/bling.svg new file mode 100644 index 00000000..d84779a8 --- /dev/null +++ b/public/img/bling.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/changeticket.png b/public/img/changeticket.png new file mode 100644 index 00000000..aed401c5 Binary files /dev/null and b/public/img/changeticket.png differ diff --git a/public/img/checknoattendanceicon.png b/public/img/checknoattendanceicon.png new file mode 100644 index 00000000..ba7bacb4 Binary files /dev/null and b/public/img/checknoattendanceicon.png differ diff --git a/public/img/dailychallenge_background.png b/public/img/dailychallenge_background.png new file mode 100644 index 00000000..df036b66 Binary files /dev/null and b/public/img/dailychallenge_background.png differ diff --git a/public/img/dailychallenge_title.svg b/public/img/dailychallenge_title.svg new file mode 100644 index 00000000..a673127b --- /dev/null +++ b/public/img/dailychallenge_title.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/koala-gold-l.png b/public/img/koala-gold-l.png new file mode 100644 index 00000000..95e8c1ca Binary files /dev/null and b/public/img/koala-gold-l.png differ diff --git a/public/img/koala-gold-s.png b/public/img/koala-gold-s.png new file mode 100644 index 00000000..10b24dfe Binary files /dev/null and b/public/img/koala-gold-s.png differ diff --git a/public/img/koala-silver-m.png b/public/img/koala-silver-m.png new file mode 100644 index 00000000..412d47d2 Binary files /dev/null and b/public/img/koala-silver-m.png differ diff --git a/public/img/koala_blue_big.png b/public/img/koala_blue_big.png new file mode 100644 index 00000000..b91d9526 Binary files /dev/null and b/public/img/koala_blue_big.png differ diff --git a/public/img/koala_blue_small.png b/public/img/koala_blue_small.png new file mode 100644 index 00000000..fe00d6b1 Binary files /dev/null and b/public/img/koala_blue_small.png differ diff --git a/public/img/koala_pink_big.png b/public/img/koala_pink_big.png new file mode 100644 index 00000000..819f2ff5 Binary files /dev/null and b/public/img/koala_pink_big.png differ diff --git a/public/img/koala_pink_small.png b/public/img/koala_pink_small.png new file mode 100644 index 00000000..16cbbac2 Binary files /dev/null and b/public/img/koala_pink_small.png differ diff --git a/public/img/koalalogo_dark.png b/public/img/koalalogo_dark.png new file mode 100644 index 00000000..4799cea3 Binary files /dev/null and b/public/img/koalalogo_dark.png differ diff --git a/public/img/stamp.png b/public/img/stamp.png new file mode 100644 index 00000000..116fedcf Binary files /dev/null and b/public/img/stamp.png differ diff --git a/public/img/sticker-shadow.svg b/public/img/sticker-shadow.svg new file mode 100644 index 00000000..35840069 --- /dev/null +++ b/public/img/sticker-shadow.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/sticker.svg b/public/img/sticker.svg new file mode 100644 index 00000000..62a33438 --- /dev/null +++ b/public/img/sticker.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/tablearrow.png b/public/img/tablearrow.png deleted file mode 100644 index f33ea416..00000000 Binary files a/public/img/tablearrow.png and /dev/null differ diff --git a/public/img/tablearrowleft.png b/public/img/tablearrowleft.png new file mode 100644 index 00000000..1fdcbd8a Binary files /dev/null and b/public/img/tablearrowleft.png differ diff --git a/public/img/tablearrowright.png b/public/img/tablearrowright.png new file mode 100644 index 00000000..73f5ee86 Binary files /dev/null and b/public/img/tablearrowright.png differ diff --git a/public/img/test-star.png b/public/img/test-star.png new file mode 100644 index 00000000..d300e965 Binary files /dev/null and b/public/img/test-star.png differ diff --git a/public/img/tier_icon.png b/public/img/tier_icon.png new file mode 100644 index 00000000..8acac29a Binary files /dev/null and b/public/img/tier_icon.png differ diff --git a/public/img/untouched-attendance-icon.png b/public/img/untouched-attendance-icon.png new file mode 100644 index 00000000..041b678e Binary files /dev/null and b/public/img/untouched-attendance-icon.png differ diff --git a/src/APP/components/Footer/Footer.footer.jsx b/src/APP/components/Footer/Footer.footer.jsx index c3208a71..8f2d0698 100644 --- a/src/APP/components/Footer/Footer.footer.jsx +++ b/src/APP/components/Footer/Footer.footer.jsx @@ -1,23 +1,62 @@ -import React from 'react' -import * as itemS from "./Styled/Footer.footer.styles" +import React from 'react'; +import * as itemS from './Styled/Footer.footer.styles'; export default function footer() { + const currentYear = new Date().getFullYear(); return ( -
- KOALA(한국항공대학교 알고리즘 학회) +
+ KOALA + (한국항공대학교 알고리즘 학회)
- 문의처 : kau-koala@naver.com |업데이트 노트 링크 - Copyrightⓒ2024.KOALA. All rights reserved. + + 문의처 : kau-koala@naver.com | + + 업데이트 노트{' '} + 링크 + + + + Copyrightⓒ{currentYear}.KOALA. All rights reserved. + - - + + + + + + - ) + ); } diff --git a/src/APP/components/Header/Header.header.jsx b/src/APP/components/Header/Header.header.jsx index a77c0f25..c778d0fb 100644 --- a/src/APP/components/Header/Header.header.jsx +++ b/src/APP/components/Header/Header.header.jsx @@ -1,25 +1,25 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from "react"; import * as itemS from "./Styled/Header.header.styles"; -import request from '../../Api/request'; -import ProfileModal from './Header.profile.modal'; +import request from "../../Api/request"; +import ProfileModal from "./Header.profile.modal"; -export default function Header() { +export default function Header({ dark }) { const [isLoggedIn, setIsLoggedIn] = useState(false); - const [userName, setUserName] = useState(''); - const [profileUrl, setProfileUrl] = useState(''); + const [userName, setUserName] = useState(""); + const [profileUrl, setProfileUrl] = useState(""); const [showProfileModal, setShowProfileModal] = useState(false); - const [activeMenu, setActiveMenu] = useState(''); + const [activeMenu, setActiveMenu] = useState(""); // Refs for detecting clicks outside - const modalRef = useRef(null); + const modalRef = useRef(null); const studyMenuRef = useRef(null); const codingMenuRef = useRef(null); // Fetch login status on component mount const checkLoginStatus = async () => { try { - const response = await request.get('/member/info'); - console.log("로그인 멤버 정보 조회", response); + const response = await request.get("/member/info"); + // console.log("로그인 멤버 정보 조회", response); if (response["isSuccess"]) { setUserName(response.result.name); setProfileUrl(response.result.profileUrl); @@ -30,8 +30,7 @@ export default function Header() { } }; useEffect(() => { - - const accessToken = localStorage.getItem('accessToken'); + const accessToken = localStorage.getItem("accessToken"); if (accessToken) { checkLoginStatus(); } @@ -39,72 +38,90 @@ export default function Header() { // Toggle Profile Modal const toggleProfileModal = () => { - setShowProfileModal(prev => !prev); - setActiveMenu(''); + setShowProfileModal((prev) => !prev); + setActiveMenu(""); }; // Handle Menu Clicks const handleMenuClick = (menu) => { - setActiveMenu((prev) => (prev === menu ? '' : menu)); + setActiveMenu((prev) => (prev === menu ? "" : menu)); }; // Handle Navigation and Close Menus const handleNav = () => { - setActiveMenu(''); + setActiveMenu(""); }; // Click Outside Handler useEffect(() => { const handleClickOutside = (event) => { // Check if click is inside Profile Modal - const clickedInsideProfile = modalRef.current && modalRef.current.contains(event.target); + const clickedInsideProfile = + modalRef.current && modalRef.current.contains(event.target); // Check if click is inside any open submenu let clickedInsideMenu = false; - if (activeMenu === 'study') { - clickedInsideMenu = studyMenuRef.current && studyMenuRef.current.contains(event.target); - } else if (activeMenu === 'coding') { - clickedInsideMenu = codingMenuRef.current && codingMenuRef.current.contains(event.target); + if (activeMenu === "study") { + clickedInsideMenu = + studyMenuRef.current && studyMenuRef.current.contains(event.target); + } else if (activeMenu === "coding") { + clickedInsideMenu = + codingMenuRef.current && codingMenuRef.current.contains(event.target); } // If click is outside both Profile Modal and any open submenu, close them if (!clickedInsideProfile && !clickedInsideMenu) { setShowProfileModal(false); - setActiveMenu(''); + setActiveMenu(""); } }; // Attach the event listener - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); // Cleanup the event listener on component unmount return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, [showProfileModal, activeMenu]); return ( - + - - + + 나의 스터디 - handleMenuClick('study')}> + handleMenuClick("study")}> 스터디 - {activeMenu === 'study' && ( + {activeMenu === "study" && ( - {e.stopPropagation(); handleNav();}}> + { + e.stopPropagation(); + handleNav(); + }} + > 정규 스터디 - {e.stopPropagation(); handleNav();}}> + { + e.stopPropagation(); + handleNav(); + }} + > 자율 스터디 @@ -114,36 +131,44 @@ export default function Header() { 커뮤니티 - handleMenuClick('coding')}> + handleMenuClick("coding")}> 코딩테스트 분석 - {activeMenu === 'coding' && ( + {activeMenu === "coding" && ( - {e.stopPropagation(); handleNav();}}> - 기업/부트캠프 + { + e.stopPropagation(); + handleNav(); + }} + > + + 기업/부트캠프 + )} {isLoggedIn ? ( -
+
안녕하세요, {userName} 님 - + {showProfileModal && ( - )}
) : ( - 로그인/회원가입 + 로그인/회원가입 )} diff --git a/src/APP/components/Header/Styled/Header.header.styles.js b/src/APP/components/Header/Styled/Header.header.styles.js index 4611778f..a205fce9 100644 --- a/src/APP/components/Header/Styled/Header.header.styles.js +++ b/src/APP/components/Header/Styled/Header.header.styles.js @@ -15,7 +15,7 @@ export const HeaderContainer = styled.div` export const InnerContainer = styled.div` width: 100%; - border-bottom: 1px solid ${tokens.colors.B_Grey_3}; + border-bottom: ${({ $dark }) => $dark ? "none" : `1px solid ${tokens.colors.B_Grey_3}`}; `; // HeaderWrap부분이 admin이랑 약간 다른 듯 @@ -115,6 +115,8 @@ export const Btn = styled.button` @media (max-width: 300px) { ${tokens.typography.T7_SB_12}; } + + background: ${({ $dark }) => $dark && "rgba(0, 153, 237, 0.18)"}; `; // user의 경우에는 @media를 통해 모바일 버젼 만드는 것도 고려해서 수정이 필요함 diff --git a/src/APP/components/Widget/DailyChallengeWidget/Styled/Widget.DailyChallengeWidget.main.styles.js b/src/APP/components/Widget/DailyChallengeWidget/Styled/Widget.DailyChallengeWidget.main.styles.js new file mode 100644 index 00000000..d37c0afd --- /dev/null +++ b/src/APP/components/Widget/DailyChallengeWidget/Styled/Widget.DailyChallengeWidget.main.styles.js @@ -0,0 +1,115 @@ +import styled, { css } from "styled-components"; +import * as tokens from "../../../../../tokens"; + +export const DailyChallengeWidget = styled.div` + position: fixed; + bottom: 2rem; + right: 2rem; + width: 11.54rem; + height: 7rem; + background-color: white; + box-shadow: 0 0.166rem 0.563rem 0.25rem rgba(80, 115, 135, 0.15); + border-radius: 0.542rem; + + z-index: 999; +`; + +export const Relative = styled.div` + position: relative; + display: flex; + /* flex-direction: column; + justify-content: center; + align-items: center; */ + width: 11.54rem; + height: 7rem; + padding: 0.916rem 0.875rem 0.7rem 0.875rem; +`; + +export const Icon = styled.img` + position: absolute; + top: -7%; + left: -8%; + width: 2.8rem; + height: 2.8rem; +`; + +export const TextImg = styled.img` + width: 6.8rem; + height: 1rem; +`; + +export const TextWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; +`; + +export const Title = styled.div` + font-family: "Schablona"; + font-size: 1.125rem; + font-weight: 400; + line-height: 1.125rem; + color: ${tokens.colors.Grey_8}; + /* margin-bottom: 0.3rem; */ +`; + +export const RemainingTimeWrapper = styled.div` + display: flex; + align-items: baseline; + gap: 0.333rem; + /* margin-bottom: 0.416rem; */ +`; + +export const RemainingLabel = styled.span` + ${({ status }) => { + switch (status) { + case 1: + return css` + font-size: 1.666rem; + font-weight: 700; + color: #0099ed; + `; + case 2: + return css` + ${tokens.typography.T3_B_24}; + color: #0099ed; + `; + case 3: + return css` + ${tokens.typography.T3_B_24}; + color: ${tokens.colors.B_Grey_6}; + `; + default: + return css` + font-size: 1.666rem; + font-weight: 700; + color: #0099ed; + `; + } + }} +`; + +export const RemainingValue = styled.span` + ${tokens.typography.T3_B_24}; + color: #0099ed; +`; + +export const RemainingTime = styled.div` + font-family: "Pretendard"; + font-size: 1.666rem; + font-weight: 700; + line-height: 1.833rem; + color: #0099ed; +`; + +export const ActionButton = styled.button` + width: 9.8rem; + height: 2rem; + ${tokens.typography.T3_B_24}; + background-color: ${({ status }) => (status === 1 ? "#0099ED" : "#3E495A")}; + color: white; + border: none; + border-radius: 0.3rem; + cursor: pointer; +`; diff --git a/src/APP/components/Widget/DailyChallengeWidget/Widget.DailyChallengeWidget.main.jsx b/src/APP/components/Widget/DailyChallengeWidget/Widget.DailyChallengeWidget.main.jsx new file mode 100644 index 00000000..cf774c87 --- /dev/null +++ b/src/APP/components/Widget/DailyChallengeWidget/Widget.DailyChallengeWidget.main.jsx @@ -0,0 +1,137 @@ +import React, { useEffect, useState } from "react"; +import * as itemS from "./Styled/Widget.DailyChallengeWidget.main.styles"; +import { useNavigate } from "react-router-dom"; +import request from "../../../Api/request"; + +const getTodayEndTime = () => { + const now = new Date(); + const end = new Date(now); + end.setHours(24, 0, 0, 0); + return end; +}; + +const calculateRemainingTime = () => { + const now = new Date(); + const diff = getTodayEndTime() - now; + + if (diff <= 0) return "00:00:00"; + + const hours = String(Math.floor(diff / (1000 * 60 * 60))).padStart(2, "0"); + const minutes = String(Math.floor((diff / (1000 * 60)) % 60)).padStart( + 2, + "0" + ); + const seconds = String(Math.floor((diff / 1000) % 60)).padStart(2, "0"); + + return `${hours}:${minutes}:${seconds}`; +}; + +export default function DailyChallengeWidget() { + const navigate = useNavigate(); + const [remainingTime, setRemainingTime] = useState("00:00:00"); + const [isSolved, setIsSolved] = useState(false); //false + const [status, setStatus] = useState(1); // 상태 번호 1~3 + const [currentDate, setCurrentDate] = useState(() => { + return new Date().toISOString().split("T")[0]; // "2025-08-06" + }); + + const fetchChallengeStatus = async () => { + try { + const response = await request.get("/challenge/check-join"); + if (response.isSuccess) { + setIsSolved(response.result); + } else { + console.error("데일리 챌린지 상태 조회 실패:", response); + } + } catch (error) { + console.error("데일리 챌린지 상태 조회 오류", error); + } + }; + + // 상태 계산 함수 + const determineStatus = (isSolved, remaining) => { + const [hh, mm, ss] = remaining.split(":").map(Number); + const totalSeconds = hh * 3600 + mm * 60 + ss; + + if (isSolved && totalSeconds < 3600) return 3; // 상태 3 + if (isSolved) return 2; // 상태 2 + return 1; // 상태 1 + }; + + useEffect(() => { + const dateWatcher = setInterval(() => { + const today = new Date().toISOString().split("T")[0]; + if (today !== currentDate) { + setCurrentDate(today); + setIsSolved(false); // 초기화 + } + }, 1000); + + return () => clearInterval(dateWatcher); + }, [currentDate]); + + useEffect(() => { + fetchChallengeStatus(); + + const updateTimer = () => { + const remaining = calculateRemainingTime(); + setRemainingTime(remaining); + setStatus(determineStatus(isSolved, remaining)); + }; + + updateTimer(); // 초기 1회 실행 + + const timer = setInterval(updateTimer, 1000); + return () => clearInterval(timer); + }, [isSolved]); + + const handleClick = () => { + navigate("/dailychallenge"); + }; + + // 상태에 따른 텍스트 렌더링 + const getCenterText = () => { + if (status === 2) return "오늘의 챌린지 완료!"; + if (status === 3) { + const [, mm, ss] = remainingTime.split(":"); + return `순위 공개까지 ${mm}:${ss}`; + } + return remainingTime; // 상태 1 + }; + + const getButtonText = () => { + if (status === 1) return "문제 풀기"; + if (status === 2) return "순위 확인"; + if (status === 3) return "실시간 순위 확인"; + }; + + return ( + + + + + + + {status === 3 ? ( + <> + + 순위 공개까지 + + + {remainingTime.split(":").slice(1).join(":")} + + + ) : ( + + {getCenterText()} + + )} + + + {getButtonText()} + + + + + ); +} diff --git a/src/APP/user-pages/Community/Styled/Community.community.main.styles.js b/src/APP/user-pages/Community/Styled/Community.community.main.styles.js index 52ffbb35..df53314c 100644 --- a/src/APP/user-pages/Community/Styled/Community.community.main.styles.js +++ b/src/APP/user-pages/Community/Styled/Community.community.main.styles.js @@ -1,7 +1,5 @@ -import styled, { css } from 'styled-components'; -import * as tokens from "../../../../tokens" - - +import styled, { css } from "styled-components"; +import * as tokens from "../../../../tokens"; export const OuterContainer = styled.div` // background: linear-gradient(to bottom, #EFF1FD, #E8F7FF); @@ -11,13 +9,13 @@ export const OuterContainer = styled.div` export const Container = styled.div` display: flex; - justify-content: center; + justify-content: center; `; export const InnerContainer = styled.div` display: flex; flex-direction: column; - align-items: center; + align-items: center; background-color: ${tokens.colors.White}; border-radius: 0.25rem; padding: 1.4rem 15rem 4rem 15rem; @@ -83,7 +81,7 @@ export const Search = styled.input` margin: 0 0.42rem; border: none; outline: none; - + &:focus { outline: none; } @@ -171,7 +169,7 @@ export const CategoryDrop = styled.div` export const SortIcon = styled.img` width: 1rem; height: 1rem; - self-items: center; + align-self: center; cursor: pointer; `; @@ -185,7 +183,7 @@ export const SortDrop = styled.div` height: 4.5rem; border-radius: 0.17rem; position: absolute; - box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.10); + box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.1); z-index: 99; top: -0.17rem; `; @@ -198,15 +196,14 @@ export const SortText = styled.div` height: 1.5rem; ${tokens.typography.B3_M_14}; color: ${tokens.colors.Grey_6}; - + &:hover { - background-color: rgba(102, 201, 255, 0.2); + background-color: rgba(102, 201, 255, 0.2); } - + cursor: pointer; `; - // 페이지 export const PaginationContainer = styled.div` display: flex; @@ -232,7 +229,7 @@ export const WriteBtn = styled.button` export const Pagination = styled.div` display: flex; justify-content: center; - align-items: center; + align-items: center; padding: 0.83rem; list-style: none; // margin-top: 1.6rem; @@ -241,24 +238,25 @@ export const Pagination = styled.div` export const PaginationArrow = styled.div` width: 1rem; height: 1rem; - background-image: url('/img/grayarrow.png'); + background-image: url("/img/grayarrow.png"); background-size: contain; background-repeat: no-repeat; - transform: ${(props) => (props.left ? 'rotate(180deg)' : 'none')}; - cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; + transform: ${(props) => (props.left ? "rotate(180deg)" : "none")}; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; opacity: ${(props) => (props.disabled ? 0.5 : 1)}; `; export const PaginationNumber = styled.div` display: flex; justify-content: center; - align-items: center; + align-items: center; margin: 0 0.21rem; width: 0.33rem; height: 0.88rem; padding: 0.42rem; cursor: pointer; - color: ${(props) => (props.active ? tokens.colors.Blue_3 : tokens.colors.B_Grey_7)}; - font-weight: ${(props) => (props.active ? 'bold' : 'normal')}; + color: ${(props) => + props.active ? tokens.colors.Blue_3 : tokens.colors.B_Grey_7}; + font-weight: ${(props) => (props.active ? "bold" : "normal")}; ${tokens.typography.B3_M_14}; -`; \ No newline at end of file +`; diff --git a/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.background.jsx b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.background.jsx new file mode 100644 index 00000000..1e0319ba --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.background.jsx @@ -0,0 +1,527 @@ +// DailyChallenge.dailychallenge.background.js +import React, { useEffect, useMemo, useRef, useState } from "react"; +import * as Styled from "./Styled/DailyChallenge.dailychallenge.background.styles"; + +/* ===== 모바일/저전력 감지 훅 ===== */ +function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(() => + typeof window !== "undefined" ? window.matchMedia(`(max-width: ${breakpoint}px)`).matches : false + ); + useEffect(() => { + if (typeof window === "undefined") return; + const mq = window.matchMedia(`(max-width: ${breakpoint}px)`); + const handler = (e) => setIsMobile(e.matches); + mq.addEventListener?.("change", handler); + return () => mq.removeEventListener?.("change", handler); + }, [breakpoint]); + return isMobile; +} +function usePrefersReducedMotion() { + const [reduced, setReduced] = useState(() => + typeof window !== "undefined" + ? window.matchMedia("(prefers-reduced-motion: reduce)").matches + : false + ); + useEffect(() => { + if (typeof window === "undefined") return; + const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); + const handler = (e) => setReduced(e.matches); + mq.addEventListener?.("change", handler); + return () => mq.removeEventListener?.("change", handler); + }, []); + return reduced; +} + +/* ===== 좌표/문자 유틸 ===== */ +const SYMBOLS1 = ["<","*",";",":","/","%","=","{","}","[","!","/","<","$",":","+"]; +const SYMBOLS2 = ["<","&","#","{}","[","/","]","{","()","*","//","<","!",">","#",";"]; +const toPct = (v) => (v / 1000) * 100; + +function hLineDots1(y, x1, x2, gap, color = "pink", idPrefix = "h") { + const out = []; let id = 0; + for (let x = x1; x <= x2; x += gap) { + out.push({ id: `${idPrefix}-${id++}`, x, y, color, char: SYMBOLS1[id % SYMBOLS1.length] }); + } + return out; +} +function hLineDots2(y, x1, x2, gap, color = "pink", idPrefix = "h") { + const out = []; let id = 0; + for (let x = x1; x <= x2; x += gap) { + out.push({ id: `${idPrefix}-${id++}`, x, y, color, char: SYMBOLS2[id % SYMBOLS2.length] }); + } + return out; +} +function vLineDots1(x, y1, y2, gap, color = "pink", idPrefix = "v") { + const out = []; let id = 0; + for (let y = y1; y <= y2; y += gap) { + out.push({ id: `${idPrefix}-${id++}`, x, y, color, char: SYMBOLS1[id % SYMBOLS1.length] }); + } + return out; +} +function vLineDots2(x, y1, y2, gap, color = "pink", idPrefix = "v") { + const out = []; let id = 0; + for (let y = y1; y <= y2; y += gap) { + out.push({ id: `${idPrefix}-${id++}`, x, y, color, char: SYMBOLS2[id % SYMBOLS2.length] }); + } + return out; +} + +/* ----- 길 정의 ----- */ +/* 분홍 */ +function buildPinkDots() { + const leftH = hLineDots1(156, 28, 174, 24, "pink", "ph"); + const midV = vLineDots2(100, 60, 410, 24, "pink", "pm"); + const rightV = vLineDots1(898, 60, 410, 24, "pink", "pr"); + return [...leftH, ...midV, ...rightV]; +} +function buildPinkPathD() { + return [ + "M 28 156 L 200 156", + "M 100 60 L 100 420", + "M 898 60 L 898 420", + ].join(" "); +} + +/* 하늘 */ +function buildBlueDots() { + const leftV = vLineDots1(200, 60, 410, 24, "blue", "bvL"); + const rightV = vLineDots2(800, 60, 410, 24, "blue", "bvR"); + const rightH1 = hLineDots1(252, 800, 890, 24, "blue", "bh1"); + const rightH2 = hLineDots2(252, 928, 978, 24, "blue", "bh2"); + return [...leftV, ...rightV, ...rightH1, ...rightH2]; +} +function buildBluePathD() { + return [ + "M 200 60 L 200 410", + "M 800 60 L 800 410", + "M 800 252 L 978 252", + ].join(" "); +} + +/* 스냅용 (드래그 드롭) */ +function closestLengthOnPath(pathEl, x, y, samples = 350) { + const total = pathEl.getTotalLength(); + let best = 0, bestD = Infinity; + for (let i = 0; i <= samples; i++) { + const l = (i / samples) * total; + const p = pathEl.getPointAtLength(l); + const d2 = (p.x - x) ** 2 + (p.y - y) ** 2; + if (d2 < bestD) { bestD = d2; best = l; } + } + return best; +} + +/* ===== Trail(잔상) 훅: 코알라와 독립적으로 선분 배열을 관리 ===== */ +function useTrails({ + color = "#FFBEC6", + source, // {x, y, rot} + enabled, // true면 잔상 생성 / false면 생성 중지 + headGap = 10, // 머리 바로 뒤 지점(선분이 머리쪽부터 이어짐) + segLen = 80, // 최대 선 길이(캡) + width = 2, + fadeMs = 2300, + spawnEveryPx = 12, + maxSegments = 450, +}) { + const [segments, setSegments] = useState([]); + const srcRef = useRef({ x: 0, y: 0, rot: 0 }); + const lastHeadRef = useRef(null); + const lastSpawnAtRef = useRef({ x: 0, y: 0 }); + const nowRef = useRef(typeof performance !== "undefined" ? performance.now() : Date.now()); + const [, forceTick] = useState(0); + + // 축 정렬(0°, 90°, 180°)인지 판정 (허용 오차 포함) + const ORIENT_EPS = 0.5; + const isAxisAligned = (x1, y1, x2, y2) => + Math.abs(x1 - x2) <= ORIENT_EPS || Math.abs(y1 - y2) <= ORIENT_EPS; + + useEffect(() => { + srcRef.current = { x: source.x, y: source.y, rot: source.rot || 0 }; + }, [source.x, source.y, source.rot]); + + useEffect(() => { + let raf = 0; + const step = (t) => { + nowRef.current = t; + + // 1) 만료 제거 + setSegments((prev) => prev.filter((s) => (t - s.t) < fadeMs)); + + // 2) 이동 누적 기준 이상이면 선분 추가 + const { x, y, rot } = srcRef.current; + const dx = x - lastSpawnAtRef.current.x; + const dy = y - lastSpawnAtRef.current.y; + const moved2 = dx * dx + dy * dy; + + if (enabled) { + const hx = x - Math.cos(rot) * headGap; + const hy = y - Math.sin(rot) * headGap; + + if (moved2 >= spawnEveryPx * spawnEveryPx) { + if (lastHeadRef.current) { + let { x: px, y: py } = lastHeadRef.current; + + // 최대 길이 캡 + const vx = hx - px, vy = hy - py; + const dist = Math.hypot(vx, vy); + if (dist > segLen) { + const s = segLen / dist; + px = hx - vx * s; + py = hy - vy * s; + } + + // ⬇️ 축 정렬(가로/세로)인 경우에만 잔상 추가 + if (isAxisAligned(px, py, hx, hy)) { + const id = `${color}-${t}-${hx.toFixed(1)}-${hy.toFixed(1)}`; + setSegments((prev) => { + const next = [...prev, { id, x1: px, y1: py, x2: hx, y2: hy, t }]; + return next.length > maxSegments ? next.slice(next.length - maxSegments) : next; + }); + } + } + + // 기준 지점 갱신(잔상 추가 여부와 무관하게 최신 위치로 동기화) + lastHeadRef.current = { x: hx, y: hy }; + lastSpawnAtRef.current = { x, y }; + } + } else { + const hx = x - Math.cos(rot) * headGap; + const hy = y - Math.sin(rot) * headGap; + lastHeadRef.current = { x: hx, y: hy }; + lastSpawnAtRef.current = { x, y }; + } + + // 3) 투명도 업데이트용 틱 + forceTick(t); + raf = requestAnimationFrame(step); + }; + raf = requestAnimationFrame(step); + return () => cancelAnimationFrame(raf); + }, [enabled, headGap, segLen, fadeMs, spawnEveryPx, color, maxSegments]); + + return { + segments, + width, + color, + opacityOf: (t0) => { + const age = Math.max(0, (nowRef.current - t0) / fadeMs); + const clamped = Math.min(1, age); + return 1 - clamped; // 선형 페이드아웃 + }, + }; +} + +/* ===== 공통 훅(한 코알라) ===== */ +function useKoalaMover({ + svgRef, pathSelector, dots, speed = 120, hitRadius = 22, disabled = false +}) { + const koalaRef = useRef(null); + const pathRef = useRef(null); + + const [hiddenSet, setHiddenSet] = useState(new Set()); + const timersRef = useRef(new Map()); + const hoveredRef = useRef(false); + const draggingRef = useRef(false); + const [hovered, setHovered] = useState(false); + const [dragging, setDragging] = useState(false); + const progressRef = useRef(0); + const totalLenRef = useRef(1); + const lastRotRef = useRef(0); // 마지막 각도(스냅 전후 기록용) + + // 코알라 현재 위치/방향 (SVG 좌표계 0..1000) + const [pos, setPos] = useState({ x: 0, y: 0, rot: 0 }); + + /* init */ + useEffect(() => { + if (disabled) return; + const svg = svgRef.current; + if (!svg) return; + svg.setAttribute("viewBox", "0 0 1000 1000"); + const path = svg.querySelector(pathSelector); + if (!path) return; + + pathRef.current = path; + totalLenRef.current = path.getTotalLength(); + progressRef.current = totalLenRef.current * 0.05; + + const p = path.getPointAtLength(progressRef.current); + const rot = safeTangent(path, progressRef.current, totalLenRef.current, lastRotRef.current); + lastRotRef.current = rot; + setKoalaPosPercent(p.x, p.y, rot); + }, [svgRef, pathSelector, disabled]); + + /* loop */ + useEffect(() => { + if (disabled) return; + let raf = 0, last = performance.now(); + const step = (now) => { + const dt = (now - last) / 1000; last = now; + const path = pathRef.current; + if (path && !hoveredRef.current && !draggingRef.current) { + const total = totalLenRef.current; + let prog = progressRef.current + speed * dt; + if (prog > total) prog %= total; + progressRef.current = prog; + + const p0 = path.getPointAtLength(prog); + const rot = safeTangent(path, prog, total, lastRotRef.current); + lastRotRef.current = rot; + + setKoalaPosPercent(p0.x, p0.y, rot); + eatNearby(p0.x, p0.y); + } + raf = requestAnimationFrame(step); + }; + raf = requestAnimationFrame(step); + return () => cancelAnimationFrame(raf); + }, [speed, hitRadius, disabled]); + + /* hover pause */ + useEffect(() => { + if (disabled) return; + const k = koalaRef.current; if (!k) return; + const onEnter = () => { hoveredRef.current = true; setHovered(true); }; + const onLeave = () => { hoveredRef.current = false; setHovered(false); }; + k.addEventListener("pointerenter", onEnter); + k.addEventListener("pointerleave", onLeave); + return () => { + k.removeEventListener("pointerenter", onEnter); + k.removeEventListener("pointerleave", onLeave); + }; + }, [disabled]); + + /* drag */ + useEffect(() => { + if (disabled) return; + const k = koalaRef.current; if (!k) return; + const onDown = (e) => { draggingRef.current = true; setDragging(true); k.setPointerCapture(e.pointerId); }; + const onMove = (e) => { + if (!draggingRef.current) return; + const svg = svgRef.current; if (!svg) return; + const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; + const inv = svg.getScreenCTM()?.inverse(); if (!inv) return; + const p = pt.matrixTransform(inv); + setKoalaPosPercent(p.x, p.y); + }; + const onUp = (e) => { + if (!draggingRef.current) return; draggingRef.current = false; setDragging(false); + const svg = svgRef.current; const path = pathRef.current; if (!svg || !path) return; + const rect = svg.getBoundingClientRect(); + const x = (e.clientX - rect.left) * (1000 / rect.width); + const y = (e.clientY - rect.top) * (1000 / rect.height); + const len = closestLengthOnPath(path, x, y, 500); + progressRef.current = len; totalLenRef.current = path.getTotalLength(); + const p = path.getPointAtLength(len); + const rot = safeTangent(path, len, totalLenRef.current, lastRotRef.current); + lastRotRef.current = rot; + setKoalaPosPercent(p.x, p.y, rot); + }; + k.addEventListener("pointerdown", onDown); + window.addEventListener("pointermove", onMove, { passive: true }); + window.addEventListener("pointerup", onUp); + return () => { + k.removeEventListener("pointerdown", onDown); + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + }; + }, [svgRef, disabled]); + + /* === helpers === */ + + // 0°, 90°, 180° 중 가장 가까운 각으로 스냅 + function snapRightAngle(rad) { + const CAND = [0, Math.PI / 2, Math.PI]; + let best = CAND[0], bestDiff = Math.PI * 2; + for (const c of CAND) { + const diff = Math.abs(Math.atan2(Math.sin(rad - c), Math.cos(rad - c))); + if (diff < bestDiff) { bestDiff = diff; best = c; } + } + return best; + } + + // 방어 로직 제거: 앞쪽 한 점만 이용해 접선 추정 → 곧바로 직각 스냅 + function safeTangent(path, len, total /*, fallbackRot */) { + const EPS = 0.8; // 미세 전진 샘플 + const l0 = Math.min(Math.max(len, 0), total); + const l1 = Math.min(len + EPS, total); + const p0 = path.getPointAtLength(l0); + const p1 = path.getPointAtLength(l1); + const ang = Math.atan2(p1.y - p0.y, p1.x - p0.x) || 0; // (0,0)일 때 0 처리 + return snapRightAngle(ang); + } + + function setKoalaPosPercent(x, y, rad) { + const el = koalaRef.current; if (!el) return; + el.style.setProperty("--x", `${toPct(x)}%`); + el.style.setProperty("--y", `${toPct(y)}%`); + if (typeof rad === "number") el.style.setProperty("--rot", `${rad}rad`); + setPos((prev) => ({ x, y, rot: typeof rad === "number" ? rad : (prev?.rot ?? 0) })); + } + + function eatNearby(x, y) { + const r2 = hitRadius * hitRadius; + setHiddenSet((prev) => { + const next = new Set(prev); + for (const d of dots) { + if (next.has(d.id)) continue; + const dx = d.x - x, dy = d.y - y; + if (dx * dx + dy * dy <= r2) { + next.add(d.id); + const t = setTimeout(() => { + setHiddenSet((p) => { const n = new Set(p); n.delete(d.id); return n; }); + timersRef.current.delete(d.id); + }, 3000); + timersRef.current.set(d.id, t); + } + } + return next; + }); + } + + return { koalaRef, hiddenSet, pos, hovered, dragging }; +} + +/* ===== Main (한 캔버스) ===== */ +export default function Background() { + const svgRef = useRef(null); + + // 환경 감지 + const isMobile = useIsMobile(768); + const prefersReduced = usePrefersReducedMotion(); + const disableAnim = isMobile || prefersReduced; + + // 길/기호 + const pinkDots = useMemo(() => buildPinkDots(), []); + const blueDots = useMemo(() => buildBlueDots(), []); + const pinkPathD = useMemo(() => buildPinkPathD(), []); + const bluePathD = useMemo(() => buildBluePathD(), []); + + // 애니메이션이 켜져 있을 때만 훅 사용 + const pink = useKoalaMover({ + svgRef, pathSelector: "path.pink", dots: pinkDots, disabled: disableAnim + }); + const blue = useKoalaMover({ + svgRef, pathSelector: "path.blue", dots: blueDots, disabled: disableAnim + }); + + // 잔상(코알라와 독립) + const PINK_COLOR = "#FFBEC6"; + const BLUE_COLOR = "#8DDFFF"; + const HEAD_GAP = 10; + const TRAIL_LEN = 80; + const TRAIL_WIDTH = 2; + const FADE_MS = 1000; + const SPAWN_EVERY = 1; + + const pinkTrails = useTrails({ + color: PINK_COLOR, + source: pink.pos, + enabled: !disableAnim && !pink.hovered && !pink.dragging, + headGap: HEAD_GAP, + segLen: TRAIL_LEN, + width: TRAIL_WIDTH, + fadeMs: FADE_MS, + spawnEveryPx: SPAWN_EVERY, + maxSegments: 450, + }); + const blueTrails = useTrails({ + color: BLUE_COLOR, + source: blue.pos, + enabled: !disableAnim && !blue.hovered && !blue.dragging, + headGap: HEAD_GAP, + segLen: TRAIL_LEN, + width: TRAIL_WIDTH, + fadeMs: FADE_MS, + spawnEveryPx: SPAWN_EVERY, + maxSegments: 450, + }); + + return ( + + {/* 단일 SVG 캔버스 */} + + {/* 실제 이동 경로(보이지 않음) */} + + + + {/* 잔상 */} + + {pinkTrails.segments.map((s) => ( + + ))} + + + {blueTrails.segments.map((s) => ( + + ))} + + + + {/* 심볼 */} + {!disableAnim && ( + <> + {pinkDots.map((d) => ( + + {d.char} + + ))} + {blueDots.map((d) => ( + + {d.char} + + ))} + + )} + + {/* 코알라 */} + {!disableAnim && ( + <> + + + + )} + + ); +} \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.main.jsx b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.main.jsx new file mode 100644 index 00000000..9da195a7 --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.main.jsx @@ -0,0 +1,260 @@ +import React, { useState, useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import * as Styled from './Styled/DailyChallenge.dailychallenge.main.styles'; +import Ranking from './DailyChallenge.dailychallenge.ranking'; +import Background from './DailyChallenge.dailychallenge.background'; +import request from '../../Api/request'; +import axios from 'axios'; + +export default function DailyChallenge() { + const navigate = useNavigate(); + const accessToken = localStorage.getItem("accessToken"); + + const [timeLeft, setTimeLeft] = useState('00:00:00'); + const [timeLeftMs, setTimeLeftMs] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + const [showTags, setShowTags] = useState(false); + const [showLevel, setShowLevel] = useState(false); + const [tierSrc, setTierSrc] = useState('https://static.solved.ac/tier_small/0.svg'); + const [challengeData, setChallengeData] = useState(null); + const [participantsCount, setParticipantsCount] = useState(0); + const [challengeHistory, setChallengeHistory] = useState([]); + + const msToHHMMSS = (ms) => { + let total = Math.max(0, Math.floor(ms / 1000)); + const h = String(Math.floor(total / 3600)).padStart(2, '0'); + total %= 3600; + const m = String(Math.floor(total / 60)).padStart(2, '0'); + const s = String(total % 60).padStart(2, '0'); + return `${h}:${m}:${s}`; + }; + + const getMsToEndOfDay = () => { + const now = new Date(); + const end = new Date(now); + end.setHours(23, 59, 59, 999); + return end - now; + }; + + useEffect(() => { + const tick = () => { + const ms = getMsToEndOfDay(); + setTimeLeft(msToHHMMSS(ms)); + setTimeLeftMs(ms); + }; + tick(); + const id = setInterval(tick, 1000); + return () => clearInterval(id); + }, []); + + // 레벨 태그가 표시될 때 레벨 이미지도 함께 표시 + useEffect(() => { + if (showLevel && challengeData && challengeData.levelImageUrl) { + setTierSrc(challengeData.levelImageUrl); + } else if (!showLevel) { + setTierSrc('https://static.solved.ac/tier_small/0.svg'); + } + }, [showLevel, challengeData]); + + const isOneHourLeft = timeLeftMs <= 3600 * 1000; + + // 챌린지 이력 조회 + const loadChallengeHistory = async () => { + try { + let response; + if (!accessToken){ + response = (await axios.get( + `${process.env.REACT_APP_API_URL}/challenge-join-log`, + )).data; + } + else { + response = await request.get('/challenge-join-log'); + } + + const result = response.result; + const joinLogList = result.joinLogList; + const totalCount = result.totalCount; + + console.log("챌린지 이력 조회 성공:", result); + + setParticipantsCount(totalCount); + setChallengeHistory(joinLogList); + + } catch (error) { + console.error("챌린지 이력 조회 실패:", error); + } + }; + + // 자정까지 남은 시간 계산 + const getMsToMidnight = () => { + const now = new Date(); + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + return tomorrow - now; + }; + + // 자정에 페이지 새로고침 + useEffect(() => { + const msToMidnight = getMsToMidnight(); + + const midnightTimer = setTimeout(() => { + window.location.reload(); + }, msToMidnight); + + return () => { + clearTimeout(midnightTimer); + }; + }, []); + + // 컴포넌트 마운트 시 참여 여부 확인 + useEffect(() => { + loadChallengeData(); + loadChallengeHistory(); + }, []); + + // 레벨 텍스트 포맷팅 함수 (BRONZE5 -> Bronze 5) + const formatLevel = (level) => { + if (!level) return ''; + + // 숫자와 문자 분리 + const match = level.match(/^([A-Z]+)(\d+)$/); + if (!match) return level; + + const [, tier, number] = match; + // 첫 글자만 대문자, 나머지는 소문자로 변환 + const formattedTier = tier.charAt(0).toUpperCase() + tier.slice(1).toLowerCase(); + + return `${formattedTier} ${number}`; + }; + + // 챌린지 데이터 로드 (한 번만 호출) + const loadChallengeData = async () => { + if (!accessToken) return null; + if (challengeData) { + return challengeData; + } + + try { + setIsLoading(true); + const response = await request.get('/challenge/problem/today'); + if (response.isSuccess) { + const data = { + problemNumber: response.result.problemNumber, + level: response.result.level, + levelImageUrl: response.result.levelImageUrl, + algorithmList: response.result.algorithmList || [], + } + setChallengeData(data); + return data; + } + } catch (error) { + console.error('금일 챌린지 문제 조회 실패:', error); + } finally { + setIsLoading(false); + } + }; + + // 문제풀기 버튼 클릭 + const handleProblemSolve = async () => { + if (!accessToken) { + navigate('/login'); + return; + } + + const data = await loadChallengeData(); + + if (data) { + const bojLink = `https://www.acmicpc.net/problem/${data.problemNumber}`; + window.open(bojLink, '_blank'); + } + }; + + const handleTagToggle = async () => { + if (!accessToken) { + navigate('/login'); + return; + } + + await loadChallengeData(); + + setShowTags((v) => !v); + setShowLevel(false); + }; + + const handleTierToggle = async () => { + if (!accessToken) { + navigate('/login'); + return; + } + + await loadChallengeData(); + + setShowLevel((v) => !v); + setShowTags(false); + }; + + + return ( + + + + + + {timeLeft} + + + 문제 풀기 + + + 오늘은{" "} + {participantsCount} + 명이 참여하고 있어요! + + + 매일 챌린지에 참여해서 보상을 얻으세요! +
+ 1등을 3회 달성하면, 챌린지 보상으로 출석부 면제권을 획득할 수 있습니다. +
+ + + + {/* 태그 아이콘 */} + + + 태그 보기 + + + {/* 티어 아이콘 + 레벨 태그 */} + + + + 레벨 보기 + + + {showLevel && challengeData && challengeData.level && ( + + {formatLevel(challengeData.level)} + + )} + + + + + {showTags && challengeData && challengeData.algorithmList ? ( + challengeData.algorithmList.map((algorithm, index) => ( + + #{algorithm} + + )) + ) : null} + + +
+ +
+ ); +} diff --git a/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.ranking.jsx b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.ranking.jsx new file mode 100644 index 00000000..8cc733b6 --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.ranking.jsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import RankingTable from './DailyChallenge.dailychallenge.table'; +import * as itemS from './Styled/DailyChallenge.dailychallenge.ranking.styles'; + +function Ranking({ + disable = false, + challengeHistory = [], +}) { + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const formatDisplay = (d) => { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + const weekdayNames = ["일", "월", "화", "수", "목", "금", "토"]; + const weekday = weekdayNames[d.getDay()]; + return `${year}년 ${month}월 ${day}일 (${weekday})`; + }; + + // dummyRanking의 date와 동일한 포맷 ("YYYY-MM-DD") + const formatKey = (d) => { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const [selectedLanguage, setSelectedLanguage] = useState("C++"); + const [currentDate, setCurrentDate] = useState(today); + + const minDate = new Date(today); + minDate.setDate(minDate.getDate() - 6); // 최근 7일 + const prevDisabled = currentDate.getTime() <= minDate.getTime(); + const nextDisabled = currentDate.getTime() >= today.getTime(); + + const goPrev = () => { + if (prevDisabled) return; + const d = new Date(currentDate); + d.setDate(d.getDate() - 1); + setCurrentDate(d); + }; + const goNext = () => { + if (nextDisabled) return; + const d = new Date(currentDate); + d.setDate(d.getDate() + 1); + setCurrentDate(d); + }; + + return ( + + + 데일리 챌린지 순위 + *순위는 정각마다 갱신되며, 익일 00:00 기준으로 보상이 지급됩니다. + + + + + {formatDisplay(currentDate)} + + + + {["Python", "C++", "Java"].map((lang) => ( + setSelectedLanguage(lang)} + $active={selectedLanguage === lang} + > + {lang} + + ))} + + + + ); +} + +export default Ranking; \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.table.jsx b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.table.jsx new file mode 100644 index 00000000..cddb6c30 --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.table.jsx @@ -0,0 +1,104 @@ +import * as itemS from "./Styled/DailyChallenge.dailychallenge.table.styles"; +import RankingTuple from './DailyChallenge.dailychallenge.tuple'; +import rankingData from "./dummyRanking.js"; + +const DEFAULT_ROWS = [ + { rank: "1", name: "이유경", speed: "0ms", memory: "0KB", codeLength: "1000B" }, + { rank: "2", name: "이육영", speed: "0ms", memory: "0KB", codeLength: "2000B" }, + { rank: "3", name: "이규영", speed: "0ms", memory: "0KB", codeLength: "3000B" }, +]; + +export default function RankingTable({ language, date, challengeHistory = [] }) { + + const hasRecordForDate = challengeHistory.some(item => item.date === date); + const isDisabled = !hasRecordForDate; + + if (isDisabled) { + return ( + + + + 순위 + 이름 + 속도 + 메모리 + 언어 + 코드길이 + + + + + 순위는 챌린지를 완료한 후에 확인하실 수 있습니다. + + {DEFAULT_ROWS.map((row, index) => ( + + ))} + + + + ); + } + + // 언어 매핑 (UI 언어 -> API 언어) + const languageMap = { + "Python": "PYTHON", + "C++": "CPP", + "Java": "JAVA" + }; + + // challengeHistory가 있으면 실제 데이터 사용, 없으면 더미 데이터 사용 + let rows = []; + if (challengeHistory.length > 0) { + const apiLanguage = languageMap[language] || language.toUpperCase(); + rows = challengeHistory + .filter(item => + item.languageType === apiLanguage && + (!date || item.date === date) + ) + .map(item => ({ + rank: item.rank, + name: item.name, + speed: item.executionTime, + memory: item.memory, + codeLength: item.codeLength, + language: item.languageType + })); + } else { + // 기존 더미 데이터 로직 + const table = + rankingData.find( + (v) => v.language === language && (!date || v.date === date) + ) || + rankingData.find((v) => v.language === language) || + rankingData[0]; + rows = table?.ranking ?? []; + } + + return ( + + + + 순위 + 이름 + 속도 + 메모리 + 언어 + 코드길이 + + {rows.map((item, index) => ( + + ))} + + + ); +} \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.tuple.jsx b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.tuple.jsx new file mode 100644 index 00000000..4158564d --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.tuple.jsx @@ -0,0 +1,21 @@ +import { useNavigate } from 'react-router-dom'; +import * as itemS from "./Styled/DailyChallenge.dailychallengetuple.styles"; + +export default function CommunityTuple({ item, language, disable }) { + const navigate = useNavigate(); + + const linkToProfile = (id) => { + navigate(`/mypage/${id}`); + }; + + return ( + + {item.rank} + {item.name} + {item.speed} + {item.memory} + {language} + {item.codeLength} + + ); +} diff --git a/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.background.styles.js b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.background.styles.js new file mode 100644 index 00000000..2781cd9c --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.background.styles.js @@ -0,0 +1,58 @@ +// DailyChallenge.dailychallenge.background.styles.js +import styled, { css } from "styled-components"; + +/* 한 캔버스 전체 래퍼 */ +export const Wrap = styled.div` + position: absolute; + height: 120vw; + inset: 0; + overflow: hidden; + pointer-events: none; /* 배경은 기본 비활성, 코알라만 이벤트 */ + z-index: 0; +`; + +/* SVG 캔버스 */ +export const Svg = styled.svg` + position: absolute; + inset: 0; + width: 100%; + height: 100%; +`; + +/* 코알라 아이콘 */ +export const Koala = styled.img` + position: absolute; + width: 2.5rem; + height: auto; + pointer-events: auto; /* 코알라만 잡을 수 있게 */ + user-select: none; + -webkit-user-drag: none; + left: var(--x, 0%); + top: var(--y, 0%); + transform: translate(-50%, -50%) rotate(var(--rot, 0rad)); +`; + +/* 네온 효과 */ +export const neon = css` + color: ${(p) => (p.$color === "pink" ? "#ffb3c9" : "#9ee7ff")}; + text-shadow: ${(p) => + p.$color === "pink" + ? "0 0 6px rgba(255,165,196,.7), 0 0 30px rgba(255,165,196,.5)" + : "0 0 6px rgba(118,224,255,.7), 0 0 30px rgba(118,224,255,.5)"}; +`; + +/* 기호(코드 심볼) — 폰트 크기 rem로 변경 */ +export const Sym = styled.span` + position: absolute; + left: ${(p) => `${p.$xPct}%`}; + top: ${(p) => `${p.$yPct}%`}; + transform: translate(-50%, -50%); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + font-size: 1rem; /* <- px에서 rem으로 변경 */ + line-height: 1; + pointer-events: none; + transition: opacity .2s ease; + opacity: ${(p) => (p.$hidden ? 0 : 1)}; + ${neon} +`; \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.main.styles.js b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.main.styles.js new file mode 100644 index 00000000..1de3454d --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.main.styles.js @@ -0,0 +1,318 @@ +import styled from 'styled-components'; +import * as tokens from "../../../../tokens"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: auto; + background-color: #121212; + background-image: url('/img/dailychallenge_background.png'); + background-size: 100%; + background-repeat: no-repeat; +`; + +export const TitleContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 8.333rem; +` + +export const Title = styled.div` + display: flex; + width: 37.54rem; + height: 9.167rem; + background-image: url('/img/dailychallenge_title.svg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border: none; + margin-bottom: 1.542rem; +`; + +export const Timer = styled.div` + display: flex; + font-size: 2.667rem; + line-height: 2.25rem; + color: ${({ $danger }) => ($danger ? '#CE2C17' : tokens.colors.Blue_0_Main)}; + font-weight: 600; + font-family: "Pretendard"; + margin-bottom: 1.292rem; +` + +export const Btn = styled.button` + width: 9.458rem; + height: 2.792rem; + border-radius: 0.167rem; + border: none; + cursor: pointer; + color: ${tokens.colors.White}; + font-weight: 600; + font-family: "Pretendard"; + font-size: 1.417rem; + background-color: ${tokens.colors.Blue_0_Main}; + margin-bottom: 2.167rem; + transition: background-color 0.2s ease; + + &:hover:not(:disabled) { + background-color: ${tokens.colors.Blue_1}; + } + + &:disabled { + background-color: ${tokens.colors.B_Grey_6}; + cursor: not-allowed; + opacity: 0.7; + } +`; + +export const ParticipantsDescription = styled.div` + font-size: 1rem; + line-height: 1.333rem; + text-align: center; + color: ${tokens.colors.B_Grey_1}; + font-weight: 500; + font-family: "Pretendard"; + margin-bottom: 1.292rem; +` + +export const ParticiPantsHighlight = styled.span` + color: ${tokens.colors.Blue_0_Main}; +`; + +export const ChallengeDescription = styled.div` + display: flex; + font-size: 0.833rem; + line-height: 1.167rem; + text-align: center; + color: ${tokens.colors.B_Grey_6}; + font-weight: 500; + font-family: "Pretendard"; + margin-bottom: 1.292rem; +` + +export const ProblemInfoContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; +` + +export const IconContainer = styled.div` + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: center; + gap: 1.542rem; + margin-bottom: 0.583rem; +` + +export const AlgorithmTagIcon = styled.div` + display: flex; + width: 3.188rem; + height: 3.188rem; + background-image: url('/img/algorithm_tag_icon.svg'); + background-size: 3.188rem 3.188rem; + background-position: center; + background-repeat: no-repeat; + border: none; + border-radius: 0.33rem; + cursor: pointer; + &:hover { + background-color: #383838; + } +` + +export const TierIcon = styled.div` + display: flex; + width: 3.188rem; + height: 3.188rem; + background-image: url(${props => props.$src || 'https://static.solved.ac/tier_small/0.svg'}); + background-size: 1.938rem 2.5rem; + background-position: center; + background-repeat: no-repeat; + background-origin: content-box; + background-clip: content-box; + border: none; + border-radius: 0.33rem; + cursor: pointer; + &:hover { + background-color: #383838; + background-clip: padding-box; + } +` + +export const Tooltip = styled.div` + position: absolute; + left: 50%; + top: 0; + transform: translate(-50%, -90%); + opacity: 0; + pointer-events: none; + transition: opacity .15s ease, transform .15s ease; + white-space: nowrap; + z-index: 10; + + background: rgba(0,0,0,0.85); + color: #fff; + font-size: 0.667rem; + line-height: 1; + padding: 0.417rem 0.667rem; + border-radius: 0.333rem; + border: 1px solid ${tokens.colors.B_Grey_6}; + + &::before, + &::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + border: 0.333rem solid transparent; + } + + &::before { + bottom: -0.666rem; + border-top-color: ${tokens.colors.White}; + } + + &::after { + bottom: -0.625rem; + border-top-color: rgba(0,0,0,0.85); + } +`; + +export const IconWithTooltip = styled.div` + width: 3.8rem; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover ${Tooltip}, + &:focus-within ${Tooltip} { + opacity: 1; + transform: translate(-50%, calc(-100% - 0.6rem)); + pointer-events: auto; + } +`; + +export const TagContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + height: 6.15rem; + gap: 0.333rem; + position: relative; + + opacity: ${({ $show }) => ($show ? 1 : 0)}; + transform: translateY(${({ $show }) => ($show ? '0' : '-0.25rem')}); + transition: opacity 0.3s ease, transform 0.3s ease; + pointer-events: ${({ $show }) => ($show ? 'auto' : 'none')}; +`; + +export const AlgorithmTagContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + height: 6.15rem; + gap: 0.333rem; + + opacity: ${({ $show }) => ($show ? 1 : 0)}; + transform: translateY(${({ $show }) => ($show ? '0' : '-0.25rem')}); + transition: opacity 0.3s ease, transform 0.2s ease; + pointer-events: ${({ $show }) => ($show ? 'auto' : 'none')}; +`; +export const AlgorithmTag = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 0.208rem; + width: auto; + height: 1.583rem; + border-radius: 1.208rem; + background-color: #2d2d2d; + border: 1px solid ${tokens.colors.B_Grey_6}; + padding: 0.396rem 0.667rem; + + opacity: 0; + transform: scale(0.8) translateY(-0.4rem); + animation: tagFadeIn 0.4s ease forwards; + + @keyframes tagFadeIn { + to { + opacity: 1; + transform: scale(1) translateY(0); + } + } + + ${props => props.$index && ` + animation-delay: ${props.$index * 0.1}s; + `} +`; + +export const AlgorithmTagKorText = styled.div` + font-size: 0.667rem; + font-weight: 500; + color: ${tokens.colors.White}; +`; + +export const AlgorithmTagEngText = styled.div` + font-size: 0.583rem; + font-weight: 400; + color: ${tokens.colors.B_Grey_6}; +`; + +export const LevelTagContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + height: 6.15rem; + gap: 0.333rem; + + opacity: ${({ $show }) => ($show ? 1 : 0)}; + transform: translateY(${({ $show }) => ($show ? '0' : '-0.25rem')}); + transition: opacity 0.3s ease, transform 0.2s ease; + pointer-events: ${({ $show }) => ($show ? 'auto' : 'none')}; +`; + +export const LevelTag = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 0.208rem; + width: auto; + height: 1.583rem; + border-radius: 1.208rem; + background-color: #2d2d2d; + border: 1px solid ${tokens.colors.B_Grey_6}; + padding: 0.396rem 0.667rem; + + opacity: 0; + transform: scale(0.8) translateY(-0.4rem); + animation: tagFadeIn 0.4s ease forwards; + + @keyframes tagFadeIn { + to { + opacity: 1; + transform: scale(1) translateY(0); + } + } +`; + +export const LevelTagText = styled.div` + font-size: 0.667rem; + font-weight: 500; + color: ${tokens.colors.White}; +`; + +export const TierWithLevel = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; +`; \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.ranking.styles.js b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.ranking.styles.js new file mode 100644 index 00000000..c24aff97 --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.ranking.styles.js @@ -0,0 +1,100 @@ +import styled from "styled-components"; +import * as tokens from "../../../../tokens"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + width: 50rem; + margin-bottom: 269px; + @media (max-width: 600px) { + width: 32rem; + } +`; + +export const TitleContainer = styled.div` + display: flex; + align-items: center; +`; + +export const Title = styled.div` + ${tokens.typography.T3_B_24}; + color: ${tokens.colors.B_Grey_1}; +`; + +export const Notice = styled.div` + ${tokens.typography.B2_M_16}; + color: ${tokens.colors.Grey_4}; + margin-left: 28px; +`; + +export const Divider = styled.div` + height: 0.063rem; + border: none; + background-color: ${tokens.colors.B_Grey_6}; + margin: 12px 0px 26px 0; +`; + +export const DateRow = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 1rem; +`; + +export const DateText = styled.div` + ${tokens.typography.T3_SB_24}; + color: ${tokens.colors.B_Grey_1}; + display: flex; + justify-content: center; +`; + +export const NavButton = styled.button` + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + &::before { + content: ''; + width: 1rem; + height: 1rem; + background-image: ${({ $dir }) => + $dir === 'right' + ? "url('/img/arrow-r-white.svg')" + : "url('/img/arrow-l-white.svg')"}; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + } + + &:disabled { + opacity: 0.4; + cursor: default; + } +`; + + +export const LanguageContainer = styled.div` + display: flex; + align-items: center; + padding: 0 0 0.542rem 0.542rem; + gap: 1.25rem; +`; + +export const Language = styled.div` + ${tokens.typography.B2_M_16}; + color: ${({ $active }) => + $active ? tokens.colors.White : tokens.colors.Grey_4}; + + border-bottom: ${({ $active }) => + $active ? `1px solid ${tokens.colors.White}` : "1px solid transparent"}; + + &:hover { + color: ${tokens.colors.White}; + cursor: pointer; + } +`; \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.table.styles.js b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.table.styles.js new file mode 100644 index 00000000..c3361bc3 --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallenge.table.styles.js @@ -0,0 +1,53 @@ +import styled from 'styled-components'; +import * as tokens from "../../../../tokens" + + +export const Container = styled.div` +`; + +export const Table = styled.div` + display: flex; + flex-direction: column; +`; + + +export const LabelContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + background-color: #2d2d2d; + height: 2.292rem; + border-bottom: 0.04rem solid ${tokens.colors.B_Grey_4}; + + /* 컬럼 폭: 1=순위, 2=이름, 3=속도, 4=메모리, 5=언어, 6=코드길이 */ + > div:nth-child(1) { width: 80px; } /* 이름 */ + > div:nth-child(2) { width: 525px; } /* 이름 */ + > div:nth-child(4) { width: 80px; } /* 메모리 */ + > div:nth-child(6) { width: 80px; } /* 코드길이 */ +`; + +export const LabelText = styled.div` + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.White}; + width: 4.667rem; + padding-left: 20px; +`; + +export const TupleContainer = styled.div` + display: flex; + flex-direction: column; + height: 24rem; + overflow: auto; + &::-webkit-scrollbar { width: 0px; } + scrollbar-width: none; +`; + +export const OverlayText = styled.div` + position: absolute; + left: 50%; + transform: translate(-50%, 250%); + z-index: 10; + font-size: 1rem; + color: ${tokens.colors.White}; + text-align: center; +`; \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallengetuple.styles.js b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallengetuple.styles.js new file mode 100644 index 00000000..c3b75702 --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/Styled/DailyChallenge.dailychallengetuple.styles.js @@ -0,0 +1,34 @@ +import styled from 'styled-components'; +import * as tokens from "../../../../tokens" + + +export const Container = styled.div` + +`; + +export const TupleContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + border-bottom: 1px solid ${tokens.colors.B_Grey_3}; + + /* 컬럼 폭: 1=순위, 2=이름, 3=속도, 4=메모리, 5=언어, 6=코드길이 */ + > div:nth-child(1) { width: 80px; } /* 이름 */ + > div:nth-child(2) { width: 525px; } /* 이름 */ + > div:nth-child(4) { width: 80px; } /* 메모리 */ + > div:nth-child(6) { width: 80px; } /* 코드길이 */ + + filter: ${({ $disabled }) => ($disabled ? "blur(6px)" : "none")}; + pointer-events: ${({ $disabled }) => ($disabled ? "none" : "auto")}; + opacity: ${({ $disabled }) => ($disabled ? 0.8 : 1)}; +`; + +export const TupleContent = styled.div` + display: flex; + align-items: center; + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.White}; + width: 4.667rem; + padding-left: 20px; + min-height: 2.333rem; +`; \ No newline at end of file diff --git a/src/APP/user-pages/DailyChallenge/dummyRanking.js b/src/APP/user-pages/DailyChallenge/dummyRanking.js new file mode 100644 index 00000000..ffec78bb --- /dev/null +++ b/src/APP/user-pages/DailyChallenge/dummyRanking.js @@ -0,0 +1,231 @@ +const rankingData = [ + // 2025-08-20 + { + date: "2025-08-20", + language: "Python", + ranking: [ + { rank: 1, name: "이유경", speed: "0.12s", codeLength: 120 }, + { rank: 2, name: "이유경1", speed: "0.15s", codeLength: 150 }, + { rank: 3, name: "박창현", speed: "1.20s", codeLength: 80 }, + { rank: 4, name: "민중원", speed: "1.20s", codeLength: 80 } + ] + }, + { + date: "2025-08-20", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.10s", memory: "950KB", codeLength: 110 }, + { rank: 2, name: "Bob", speed: "0.14s", memory: "1020KB", codeLength: 140 }, + { rank: 3, name: "Charlie", speed: "0.20s", memory: "990KB", codeLength: 160 }, + { rank: 4, name: "David", speed: "0.25s", memory: "1005KB", codeLength: 180 } + ] + }, + { + date: "2025-08-20", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.11s", memory: "1200KB", codeLength: 130 }, + { rank: 2, name: "Frank", speed: "0.16s", memory: "1100KB", codeLength: 160 }, + { rank: 3, name: "Grace", speed: "0.22s", memory: "1150KB", codeLength: 90 }, + { rank: 4, name: "Hank", speed: "0.28s", memory: "1180KB", codeLength: 200 } + ] + }, + + // 2025-08-21 + { + date: "2025-08-21", + language: "Python", + ranking: [ + { rank: 1, name: "이유경", speed: "0.10s", codeLength: 115 }, + { rank: 2, name: "민중원", speed: "0.14s", codeLength: 145 }, + { rank: 3, name: "박창현", speed: "0.30s", codeLength: 95 }, + { rank: 4, name: "홍길동", speed: "0.50s", codeLength: 85 } + ] + }, + { + date: "2025-08-21", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.09s", memory: "930KB", codeLength: 100 }, + { rank: 2, name: "Bob", speed: "0.12s", memory: "960KB", codeLength: 120 }, + { rank: 3, name: "Charlie", speed: "0.19s", memory: "980KB", codeLength: 140 }, + { rank: 4, name: "David", speed: "0.22s", memory: "1010KB", codeLength: 160 } + ] + }, + { + date: "2025-08-21", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.13s", memory: "1210KB", codeLength: 135 }, + { rank: 2, name: "Frank", speed: "0.18s", memory: "1190KB", codeLength: 150 }, + { rank: 3, name: "Grace", speed: "0.25s", memory: "1175KB", codeLength: 170 }, + { rank: 4, name: "Hank", speed: "0.35s", memory: "1160KB", codeLength: 200 } + ] + }, + + // 2025-08-22 + { + date: "2025-08-22", + language: "Python", + ranking: [ + { rank: 1, name: "박창현", speed: "0.09s", codeLength: 110 }, + { rank: 2, name: "민중원", speed: "0.12s", codeLength: 140 }, + { rank: 3, name: "이유경", speed: "0.20s", codeLength: 100 }, + { rank: 4, name: "김철수", speed: "0.35s", codeLength: 130 } + ] + }, + { + date: "2025-08-22", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.08s", memory: "900KB", codeLength: 105 }, + { rank: 2, name: "Bob", speed: "0.11s", memory: "940KB", codeLength: 115 }, + { rank: 3, name: "Charlie", speed: "0.16s", memory: "970KB", codeLength: 125 }, + { rank: 4, name: "David", speed: "0.21s", memory: "1000KB", codeLength: 140 } + ] + }, + { + date: "2025-08-22", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.12s", memory: "1190KB", codeLength: 128 }, + { rank: 2, name: "Frank", speed: "0.17s", memory: "1180KB", codeLength: 145 }, + { rank: 3, name: "Grace", speed: "0.24s", memory: "1170KB", codeLength: 165 }, + { rank: 4, name: "Hank", speed: "0.33s", memory: "1160KB", codeLength: 185 } + ] + }, + + // 2025-08-23 + { + date: "2025-08-23", + language: "Python", + ranking: [ + { rank: 1, name: "홍길동", speed: "0.08s", codeLength: 108 }, + { rank: 2, name: "이유경", speed: "0.11s", codeLength: 112 }, + { rank: 3, name: "박창현", speed: "0.22s", codeLength: 118 }, + { rank: 4, name: "민중원", speed: "0.40s", codeLength: 130 } + ] + }, + { + date: "2025-08-23", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.07s", memory: "890KB", codeLength: 95 }, + { rank: 2, name: "Bob", speed: "0.10s", memory: "920KB", codeLength: 110 }, + { rank: 3, name: "Charlie", speed: "0.14s", memory: "950KB", codeLength: 120 }, + { rank: 4, name: "David", speed: "0.19s", memory: "970KB", codeLength: 130 } + ] + }, + { + date: "2025-08-23", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.10s", memory: "1180KB", codeLength: 122 }, + { rank: 2, name: "Frank", speed: "0.15s", memory: "1170KB", codeLength: 138 }, + { rank: 3, name: "Grace", speed: "0.22s", memory: "1160KB", codeLength: 150 }, + { rank: 4, name: "Hank", speed: "0.30s", memory: "1150KB", codeLength: 175 } + ] + }, + + // 2025-08-24 + { + date: "2025-08-24", + language: "Python", + ranking: [ + { rank: 1, name: "이유경", speed: "0.09s", codeLength: 109 }, + { rank: 2, name: "민중원", speed: "0.13s", codeLength: 115 }, + { rank: 3, name: "박창현", speed: "0.21s", codeLength: 120 }, + { rank: 4, name: "홍길동", speed: "0.34s", codeLength: 135 } + ] + }, + { + date: "2025-08-24", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.08s", memory: "905KB", codeLength: 100 }, + { rank: 2, name: "Bob", speed: "0.12s", memory: "935KB", codeLength: 118 }, + { rank: 3, name: "Charlie", speed: "0.15s", memory: "965KB", codeLength: 125 }, + { rank: 4, name: "David", speed: "0.20s", memory: "990KB", codeLength: 135 } + ] + }, + { + date: "2025-08-24", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.11s", memory: "1170KB", codeLength: 130 }, + { rank: 2, name: "Frank", speed: "0.16s", memory: "1160KB", codeLength: 145 }, + { rank: 3, name: "Grace", speed: "0.23s", memory: "1150KB", codeLength: 160 }, + { rank: 4, name: "Hank", speed: "0.32s", memory: "1140KB", codeLength: 180 } + ] + }, + + // 2025-08-25 + { + date: "2025-08-25", + language: "Python", + ranking: [ + { rank: 1, name: "민중원", speed: "0.08s", codeLength: 111 }, + { rank: 2, name: "이유경", speed: "0.12s", codeLength: 118 }, + { rank: 3, name: "박창현", speed: "0.19s", codeLength: 125 }, + { rank: 4, name: "홍길동", speed: "0.29s", codeLength: 140 } + ] + }, + { + date: "2025-08-25", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.07s", memory: "880KB", codeLength: 90 }, + { rank: 2, name: "Bob", speed: "0.11s", memory: "910KB", codeLength: 110 }, + { rank: 3, name: "Charlie", speed: "0.14s", memory: "940KB", codeLength: 120 }, + { rank: 4, name: "David", speed: "0.18s", memory: "970KB", codeLength: 130 } + ] + }, + { + date: "2025-08-25", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.10s", memory: "1160KB", codeLength: 125 }, + { rank: 2, name: "Frank", speed: "0.15s", memory: "1150KB", codeLength: 140 }, + { rank: 3, name: "Grace", speed: "0.21s", memory: "1140KB", codeLength: 155 }, + { rank: 4, name: "Hank", speed: "0.28s", memory: "1130KB", codeLength: 170 } + ] + }, + + // 2025-08-26 + { + date: "2025-08-26", + language: "Python", + ranking: [ + { rank: 1, name: "박창현", speed: "0.09s", codeLength: 112 }, + { rank: 2, name: "이유경", speed: "0.13s", codeLength: 119 }, + { rank: 3, name: "민중원", speed: "0.18s", codeLength: 127 }, + { rank: 4, name: "홍길동", speed: "0.27s", codeLength: 142 }, + { rank: 5, name: "박창현11", speed: "0.09s", codeLength: 112 }, + { rank: 6, name: "이유경212", speed: "0.13s", codeLength: 119 }, + { rank: 7, name: "민중원2", speed: "0.18s", codeLength: 127 }, + { rank: 8, name: "홍길동1", speed: "0.27s", codeLength: 142 } + ] + }, + { + date: "2025-08-26", + language: "C++", + ranking: [ + { rank: 1, name: "Alice", speed: "0.08s", memory: "895KB", codeLength: 98 }, + { rank: 2, name: "Bob", speed: "0.12s", memory: "925KB", codeLength: 115 }, + { rank: 3, name: "Charlie", speed: "0.15s", memory: "950KB", codeLength: 128 }, + { rank: 4, name: "David", speed: "0.20s", memory: "980KB", codeLength: 135 } + ] + }, + { + date: "2025-08-26", + language: "Java", + ranking: [ + { rank: 1, name: "Emma", speed: "0.11s", memory: "1150KB", codeLength: 126 }, + { rank: 2, name: "Frank", speed: "0.16s", memory: "1140KB", codeLength: 142 }, + { rank: 3, name: "Grace", speed: "0.22s", memory: "1130KB", codeLength: 158 }, + { rank: 4, name: "Hank", speed: "0.30s", memory: "1120KB", codeLength: 175 } + ] + } +]; + +export default rankingData; \ No newline at end of file diff --git a/src/APP/user-pages/EnterpriseBootcampList/Styled/EnterpriseBootcampList.enterprisebootcamplist.main.styles.js b/src/APP/user-pages/EnterpriseBootcampList/Styled/EnterpriseBootcampList.enterprisebootcamplist.main.styles.js index e2bd1908..ebcc434a 100644 --- a/src/APP/user-pages/EnterpriseBootcampList/Styled/EnterpriseBootcampList.enterprisebootcamplist.main.styles.js +++ b/src/APP/user-pages/EnterpriseBootcampList/Styled/EnterpriseBootcampList.enterprisebootcamplist.main.styles.js @@ -1,5 +1,5 @@ -import styled from 'styled-components'; -import * as tokens from "../../../../tokens" +import styled from "styled-components"; +import * as tokens from "../../../../tokens"; export const OuterContainer = styled.div` position: relative; @@ -13,7 +13,7 @@ export const Container = styled.div` export const InnerContainer = styled.div` display: flex; flex-direction: column; - align-items: center; + align-items: center; border-radius: 0.83rem; padding: 6.58rem 0; margin-bottom: 4.08rem; @@ -68,7 +68,7 @@ export const Search = styled.input` margin: 0 0.42rem; border: none; outline: none; - + &:focus { outline: none; } @@ -152,7 +152,7 @@ export const CategoryDrop = styled.div` export const SortIcon = styled.img` width: 1rem; height: 1rem; - self-items: center; + align-self: center; cursor: pointer; `; @@ -166,7 +166,7 @@ export const SortDrop = styled.div` height: 3rem; border-radius: 0.17rem; position: absolute; - box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.10); + box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.1); z-index: 99; top: -0.17rem; `; @@ -179,11 +179,11 @@ export const SortText = styled.div` height: 1.5rem; ${tokens.typography.B3_M_14}; color: ${tokens.colors.Grey_6}; - + &:hover { - background-color: rgba(102, 201, 255, 0.2); + background-color: rgba(102, 201, 255, 0.2); } - + cursor: pointer; `; @@ -206,7 +206,7 @@ export const BtnContainer = styled.div` export const Pagination = styled.div` display: flex; justify-content: center; - align-items: center; + align-items: center; padding: 0.83rem; list-style: none; `; @@ -214,24 +214,25 @@ export const Pagination = styled.div` export const PaginationArrow = styled.div` width: 1rem; height: 1rem; - background-image: url('/img/grayarrow.png'); + background-image: url("/img/grayarrow.png"); background-size: contain; background-repeat: no-repeat; - transform: ${(props) => (props.left ? 'rotate(180deg)' : 'none')}; - cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; + transform: ${(props) => (props.left ? "rotate(180deg)" : "none")}; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; opacity: ${(props) => (props.disabled ? 0.5 : 1)}; `; export const PaginationNumber = styled.div` display: flex; justify-content: center; - align-items: center; + align-items: center; margin: 0 0.21rem; width: 0.33rem; height: 0.88rem; padding: 0.42rem; cursor: pointer; - color: ${(props) => (props.active ? tokens.colors.Blue_3 : tokens.colors.B_Grey_7)}; - font-weight: ${(props) => (props.active ? 'bold' : 'normal')}; + color: ${(props) => + props.active ? tokens.colors.Blue_3 : tokens.colors.B_Grey_7}; + font-weight: ${(props) => (props.active ? "bold" : "normal")}; ${tokens.typography.B3_M_14}; -`; \ No newline at end of file +`; diff --git a/src/APP/user-pages/Inquiry/Inquiry.inquiry.main.jsx b/src/APP/user-pages/Inquiry/Inquiry.inquiry.main.jsx index 9427d19a..e8f4555a 100644 --- a/src/APP/user-pages/Inquiry/Inquiry.inquiry.main.jsx +++ b/src/APP/user-pages/Inquiry/Inquiry.inquiry.main.jsx @@ -1,233 +1,256 @@ -import React, {useState, useEffect} from 'react'; +import React, { useState, useEffect } from 'react'; import request from '../../Api/request'; import * as itemS from './Styled/Inquiry.inquiry.main.styles'; import InquiryTable from './Inquiry.inquiry.table'; -import {useNavigate} from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import useDebounce from '../../Common/useDebounce'; export default function Inquiry() { - const [isRegularMember, setIsRegularMember] = useState(false); - const navigate = useNavigate(); + const [isRegularMember, setIsRegularMember] = useState(false); + const navigate = useNavigate(); - const [posts, setPosts] = useState([]); - const [role, setRole] = useState(''); - const [categories, setCategories] = useState([{code: '', name: '전체'}]); // Default '전체' tab + const [posts, setPosts] = useState([]); + const [role, setRole] = useState(''); + const [categories, setCategories] = useState([{ code: '', name: '전체' }]); // Default '전체' tab - // api 요청 파라미터 - const [searchKeyword, setSearchKeyword] = useState(''); - const debouncedQuery = useDebounce(searchKeyword, 500); - const [sortType, setSortType] = useState('LATEST'); - const [selectedTab, setSelectedTab] = useState(''); + // api 요청 파라미터 + const [searchKeyword, setSearchKeyword] = useState(''); + const debouncedQuery = useDebounce(searchKeyword, 500); + const [sortType, setSortType] = useState('LATEST'); + const [selectedTab, setSelectedTab] = useState(''); - const [sortText, setSortText] = useState('최신순'); - const [isSortDropVisible, setIsSortDropVisible] = useState(false); // 정렬 드롭박스 열기/닫기 + const [sortText, setSortText] = useState('최신순'); + const [isSortDropVisible, setIsSortDropVisible] = useState(false); // 정렬 드롭박스 열기/닫기 - // 페이지 - const [currentPage, setCurrentPage] = useState(0); - const [totalPages, setTotalPages] = useState(0); //TODO - 임시 ) 전체 페이지 수 -> response 값으로 전체 개수 받아와야함 - const [currentPageGroup, setCurrentPageGroup] = useState(0); - const itemsPerPage = 10; // 페이지당 항목 수 + // 페이지 + const [currentPage, setCurrentPage] = useState(0); + const [totalPages, setTotalPages] = useState(0); //TODO - 임시 ) 전체 페이지 수 -> response 값으로 전체 개수 받아와야함 + const [currentPageGroup, setCurrentPageGroup] = useState(0); + const itemsPerPage = 10; // 페이지당 항목 수 - const [isTabClick, setIsTabClick] = useState(false); + const [isTabClick, setIsTabClick] = useState(false); - const pageNumbers = Array.from( - {length: Math.min(5, totalPages - currentPageGroup * 5)}, - (_, i) => currentPageGroup * 5 + i - ); + const pageNumbers = Array.from( + { length: Math.min(5, totalPages - currentPageGroup * 5) }, + (_, i) => currentPageGroup * 5 + i + ); + + useEffect(() => { + // Load `regularStudyMemberYn` from localStorage and set state + setIsRegularMember(localStorage.getItem('regularStudyMemberYn') === 'true'); + }, []); + + const fetchCategories = async () => { + try { + const response = await request.get('/inquiry/category'); + if (response.isSuccess) { + const apiCategories = response.result.categoryList; + setCategories([{ code: '', name: '전체' }, ...apiCategories]); // Add '전체' as the first tab + } else { + console.error('카테고리 목록 조회 실패:', response); + } + } catch (error) { + console.error('카테고리 목록 조회 오류', error); + } + }; + + const fetchInquiry = async () => { + try { + const response = await request.get( + `/inquiry?searchKeyword=${searchKeyword}&category=${selectedTab}&sort=${sortType}&page=${ + currentPage + 1 + }&size=${itemsPerPage}` + ); + if (response.isSuccess) { + // console.log('문의하기 목록 : ', response); + setPosts(response.result.inquiryList); + setTotalPages(Math.ceil(response.result.totalCount / itemsPerPage)); + } else { + console.error('문의하기 목록 조회 실패:', response); + } + } catch (error) { + console.error('문의하기 목록 조회 오류', error); + } + }; + + // "ROLE_ADMIN"인지 아닌지 확인하기 위해서 한번 더 api호출 + const fetchMyInfo = async () => { + try { + const response = await request.get(`/member/my-info`); + + if (response.isSuccess) { + setRole(response.result.role); + } else { + console.error('내 개인정보 조회 실패:', response); + } + } catch (error) { + console.error('내 개인정보 조회 오류', error); + } + }; + + useEffect(() => { + fetchMyInfo(); + }, []); + + useEffect(() => { + fetchCategories(); + }, []); + + useEffect(() => { + fetchInquiry(); + }, [selectedTab, sortType, currentPage, debouncedQuery]); + + const handleTabClick = (tab) => { + setSelectedTab(tab.code); + setIsTabClick(tab.code !== ''); + setCurrentPage(0); + setCurrentPageGroup(0); + }; - useEffect(() => { - // Load `regularStudyMemberYn` from localStorage and set state - setIsRegularMember(localStorage.getItem('regularStudyMemberYn') === 'true'); - }, []); - - const fetchCategories = async () => { - try { - const response = await request.get('/inquiry/category'); - if (response.isSuccess) { - const apiCategories = response.result.categoryList; - setCategories([{code: '', name: '전체'}, ...apiCategories]); // Add '전체' as the first tab - } else { - console.error('카테고리 목록 조회 실패:', response); - } - } catch (error) { - console.error('카테고리 목록 조회 오류', error); - } - }; - - const fetchInquiry = async () => { - try { - const response = await request.get( - `/inquiry?searchKeyword=${searchKeyword}&category=${selectedTab}&sort=${sortType}&page=${ - currentPage + 1 - }&size=${itemsPerPage}` - ); - if (response.isSuccess) { - // console.log('문의하기 목록 : ', response); - setPosts(response.result.inquiryList); - setTotalPages(Math.ceil(response.result.totalCount / itemsPerPage)); - } else { - console.error('문의하기 목록 조회 실패:', response); - } - } catch (error) { - console.error('문의하기 목록 조회 오류', error); - } - }; - - // "ROLE_ADMIN"인지 아닌지 확인하기 위해서 한번 더 api호출 - const fetchMyInfo = async () => { - try { - const response = await request.get(`/member/my-info`); - - if (response.isSuccess) { - setRole(response.result.role); - } else { - console.error('내 개인정보 조회 실패:', response); - } - } catch (error) { - console.error('내 개인정보 조회 오류', error); - } - }; - - useEffect(() => { - fetchMyInfo(); - }, []); - - useEffect(() => { - fetchCategories(); - }, []); - - useEffect(() => { - fetchInquiry(); - }, [selectedTab, sortType, currentPage, debouncedQuery]); - - const handleTabClick = (tab) => { - setSelectedTab(tab.code); - setIsTabClick(tab.code !== ''); - setCurrentPage(0); - setCurrentPageGroup(0); - }; - - const handleSearch = () => { - setCurrentPage(0); - setCurrentPageGroup(0); - }; - - const handlePageChange = (newPage) => { - if (newPage >= 0 && newPage < totalPages) { - setCurrentPage(newPage); - setCurrentPageGroup(Math.floor(newPage / 5)); // 페이지 그룹을 업데이트 - } - }; - - const handlePageGroupChange = (direction) => { - if (direction === 'next' && (currentPageGroup + 1) * 5 < totalPages) { - setCurrentPageGroup(currentPageGroup + 1); - setCurrentPage((currentPageGroup + 1) * 5); // 새로운 그룹의 첫 번째 페이지로 이동 - } else if (direction === 'prev' && currentPageGroup > 0) { - setCurrentPageGroup(currentPageGroup - 1); - setCurrentPage((currentPageGroup - 1) * 5); // 새로운 그룹의 첫 번째 페이지로 이동 - } - }; - - const toggleSortDrop = () => { - setIsSortDropVisible((prevState) => !prevState); - }; - - const onSortType = (type) => { - setIsSortDropVisible(false); - setSortType(type); - setSortText(type === 'LATEST' ? '최신순' : type === 'VIEW_COUNT' ? '조회수' : '좋아요'); - }; - - const handleWriteClick = () => { - navigate('/writeinquiry'); - }; - - return ( - - - - - - - 문의하기 >{' '} - {selectedTab ? categories.find((tab) => tab.code === selectedTab)?.name : '전체'} - - - - setSearchKeyword(e.target.value)} - placeholder="제목, 작성자 검색" - /> - handleSearch()} src="/img/search.svg" alt="돋보기" /> - - - - - {categories.map((tab) => - tab.code === selectedTab ? ( - handleTabClick(tab)}> - {tab.name} - - ) : ( - handleTabClick(tab)}> - {tab.name} - - ) - )} - - - - {sortText} - - {isSortDropVisible && ( - - onSortType('LATEST')}>최신순 - onSortType('VIEW_COUNT')}>조회수 - - )} - - - - - - - - handlePageGroupChange('prev')} - disabled={currentPageGroup === 0} - /> - {pageNumbers.map((pageNumber) => ( - handlePageChange(pageNumber)} - active={pageNumber === currentPage} - > - {pageNumber + 1} - - ))} - handlePageGroupChange('next')} - disabled={(currentPageGroup + 1) * 5 >= totalPages} - /> - - - {isRegularMember && role !== 'ROLE_ADMIN' ? ( - + 글쓰기 - ) : ( - - )} - - - - + const handleSearch = () => { + setCurrentPage(0); + setCurrentPageGroup(0); + }; + + const handlePageChange = (newPage) => { + if (newPage >= 0 && newPage < totalPages) { + setCurrentPage(newPage); + setCurrentPageGroup(Math.floor(newPage / 5)); // 페이지 그룹을 업데이트 + } + }; + + const handlePageGroupChange = (direction) => { + if (direction === 'next' && (currentPageGroup + 1) * 5 < totalPages) { + setCurrentPageGroup(currentPageGroup + 1); + setCurrentPage((currentPageGroup + 1) * 5); // 새로운 그룹의 첫 번째 페이지로 이동 + } else if (direction === 'prev' && currentPageGroup > 0) { + setCurrentPageGroup(currentPageGroup - 1); + setCurrentPage((currentPageGroup - 1) * 5); // 새로운 그룹의 첫 번째 페이지로 이동 + } + }; + + const toggleSortDrop = () => { + setIsSortDropVisible((prevState) => !prevState); + }; + + const onSortType = (type) => { + setIsSortDropVisible(false); + setSortType(type); + setSortText( + type === 'LATEST' ? '최신순' : type === 'VIEW_COUNT' ? '조회수' : '좋아요' ); + }; + + const handleWriteClick = () => { + navigate('/writeinquiry'); + }; + + return ( + + + + + + + 문의하기 >{' '} + {selectedTab + ? categories.find((tab) => tab.code === selectedTab)?.name + : '전체'} + + + + setSearchKeyword(e.target.value)} + placeholder="제목, 작성자 검색" + /> + handleSearch()} + src="/img/search.svg" + alt="돋보기" + /> + + + + + {categories.map((tab) => + tab.code === selectedTab ? ( + handleTabClick(tab)} + > + {tab.name} + + ) : ( + handleTabClick(tab)}> + {tab.name} + + ) + )} + + + + + {sortText} + + + {isSortDropVisible && ( + + onSortType('LATEST')}> + 최신순 + + onSortType('VIEW_COUNT')}> + 조회수 + + + )} + + + + + + + + handlePageGroupChange('prev')} + disabled={currentPageGroup === 0} + /> + {pageNumbers.map((pageNumber) => ( + handlePageChange(pageNumber)} + active={pageNumber === currentPage} + > + {pageNumber + 1} + + ))} + handlePageGroupChange('next')} + disabled={(currentPageGroup + 1) * 5 >= totalPages} + /> + + + {role !== 'ROLE_ADMIN' ? ( + + + 글쓰기 + + ) : ( + + )} + + + + + ); } diff --git a/src/APP/user-pages/Inquiry/Styled/Inquiry.inquiry.main.styles.js b/src/APP/user-pages/Inquiry/Styled/Inquiry.inquiry.main.styles.js index 4d7cdf54..37914b72 100644 --- a/src/APP/user-pages/Inquiry/Styled/Inquiry.inquiry.main.styles.js +++ b/src/APP/user-pages/Inquiry/Styled/Inquiry.inquiry.main.styles.js @@ -1,255 +1,256 @@ -import styled from 'styled-components'; -import * as tokens from '../../../../tokens'; +import styled from "styled-components"; +import * as tokens from "../../../../tokens"; export const OuterContainer = styled.div` - // background: linear-gradient(to bottom, #EFF1FD, #E8F7FF); - // position: relative; - width: 100%; + // background: linear-gradient(to bottom, #EFF1FD, #E8F7FF); + // position: relative; + width: 100%; `; export const Container = styled.div` - display: flex; - justify-content: center; + display: flex; + justify-content: center; `; export const InnerContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - background-color: ${tokens.colors.White}; - border-radius: 0.25rem; - padding: 1.4rem 15rem 4rem 15rem; - margin-top: 5.583rem; - margin-bottom: 4.083rem; - // box-shadow: 0 0.17rem 1rem 0.17rem rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + align-items: center; + background-color: ${tokens.colors.White}; + border-radius: 0.25rem; + padding: 1.4rem 15rem 4rem 15rem; + margin-top: 5.583rem; + margin-bottom: 4.083rem; + // box-shadow: 0 0.17rem 1rem 0.17rem rgba(0, 0, 0, 0.04); `; export const TopContainer = styled.div` - display: flex; - justify-content: space-between; - width: 50rem; - border-bottom: 0.04rem solid ${tokens.colors.B_Grey_2}; - /* margin-bottom: 27px; */ - @media (max-width: 600px) { - width: 100%; - } + display: flex; + justify-content: space-between; + width: 50rem; + border-bottom: 0.04rem solid ${tokens.colors.B_Grey_2}; + /* margin-bottom: 27px; */ + @media (max-width: 600px) { + width: 100%; + } `; export const HeadContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - // width: 14.58rem; - margin-bottom: 0.83rem; - @media (max-width: 600px) { - width: 100%; - } + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + // width: 14.58rem; + margin-bottom: 0.83rem; + @media (max-width: 600px) { + width: 100%; + } `; export const Head = styled.div` - ${tokens.typography.T3_B_24}; + ${tokens.typography.T3_B_24}; `; // 검색 컨테이너 export const SearchContainer = styled.div` - display: flex; - flex-direction: row; - align-items: center; - background-color: ${tokens.colors.Grey_1}; - width: 13.5rem; - height: 1.42rem; - border: 0.04rem solid ${tokens.colors.Grey_3}; - border-radius: 0.17rem; - margin-bottom: 0.75rem; + display: flex; + flex-direction: row; + align-items: center; + background-color: ${tokens.colors.Grey_1}; + width: 13.5rem; + height: 1.42rem; + border: 0.04rem solid ${tokens.colors.Grey_3}; + border-radius: 0.17rem; + margin-bottom: 0.75rem; `; export const Search = styled.input` - background-color: ${tokens.colors.Grey_1}; - width: 11.42rem; - height: 1.25rem; - ${tokens.typography.T5_SB_16}; - color: ${tokens.colors.B_Grey_6}; - margin: 0 0.42rem; - border: none; - outline: none; + background-color: ${tokens.colors.Grey_1}; + width: 11.42rem; + height: 1.25rem; + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.B_Grey_6}; + margin: 0 0.42rem; + border: none; + outline: none; - &:focus { - outline: none; - } + &:focus { + outline: none; + } - &::placeholder { - color: ${tokens.colors.B_Grey_4}; - } + &::placeholder { + color: ${tokens.colors.B_Grey_4}; + } `; export const SearchIcon = styled.img` - width: 1rem; - height: 1rem; - margin-right: 0.25rem; - cursor: pointer; + width: 1rem; + height: 1rem; + margin-right: 0.25rem; + cursor: pointer; `; export const TabSortContainer = styled.div` - display: flex; - justify-content: flex-end; - width: 50rem; - margin-bottom: 10px; - @media (max-width: 600px) { - width: 100%; - } + display: flex; + justify-content: flex-end; + width: 50rem; + margin-bottom: 10px; + @media (max-width: 600px) { + width: 100%; + } `; // 탭 컨테이너 export const TabContainer = styled.div` - display: flex; - flex-direction: row; - width: 50rem; - justify-content: flex-start; + display: flex; + flex-direction: row; + width: 50rem; + justify-content: flex-start; `; // 탭 메뉴 export const Tab = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 6.54rem; - height: 1.71rem; - color: ${tokens.colors.B_Grey_6}; - ${tokens.typography.B2_M_16}; - border: 0.04rem solid ${tokens.colors.B_Grey_4}; - border-radius: 0.17rem; - margin: 0.83rem 0.33rem 1.67rem 0; - cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + width: 6.54rem; + height: 1.71rem; + color: ${tokens.colors.B_Grey_6}; + ${tokens.typography.B2_M_16}; + border: 0.04rem solid ${tokens.colors.B_Grey_4}; + border-radius: 0.17rem; + margin: 0.83rem 0.33rem 1.67rem 0; + cursor: pointer; `; // 선택된 탭 메뉴 export const TabSelected = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 6.54rem; - height: 1.71rem; - color: ${tokens.colors.Grey_8}; - ${tokens.typography.T5_SB_16}; - border: 0.08rem solid ${tokens.colors.B_Grey_7}; - border-radius: 0.17rem; - margin: 0.83rem 0.33rem 1.67rem 0; - cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + width: 6.54rem; + height: 1.71rem; + color: ${tokens.colors.Grey_8}; + ${tokens.typography.T5_SB_16}; + border: 0.08rem solid ${tokens.colors.B_Grey_7}; + border-radius: 0.17rem; + margin: 0.83rem 0.33rem 1.67rem 0; + cursor: pointer; `; // 조회수/이름순 export const SortContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: 5.92rem; - height: 1.5rem; - border: 0.04rem solid ${tokens.colors.B_Grey_3}; - border-radius: 0.17rem; - margin-top: 0.83rem; - position: relative; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 5.92rem; + height: 1.5rem; + border: 0.04rem solid ${tokens.colors.B_Grey_3}; + border-radius: 0.17rem; + margin-top: 0.83rem; + position: relative; `; export const CategoryDrop = styled.div` - ${tokens.typography.B3_M_14}; - color: ${tokens.colors.Grey_8}; - text-align: center; - cursor: pointer; + ${tokens.typography.B3_M_14}; + color: ${tokens.colors.Grey_8}; + text-align: center; + cursor: pointer; `; export const SortIcon = styled.img` - width: 1rem; - height: 1rem; - self-items: center; - cursor: pointer; + width: 1rem; + height: 1rem; + align-self: center; + cursor: pointer; `; export const SortDrop = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: ${tokens.colors.White}; - width: 5.92rem; - /* height: 4.5rem; */ - height: 3rem; - border-radius: 0.17rem; - position: absolute; - box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.1); - z-index: 99; - top: -0.17rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: ${tokens.colors.White}; + width: 5.92rem; + /* height: 4.5rem; */ + height: 3rem; + border-radius: 0.17rem; + position: absolute; + box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.1); + z-index: 99; + top: -0.17rem; `; export const SortText = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 5.92rem; - height: 1.5rem; - ${tokens.typography.B3_M_14}; - color: ${tokens.colors.Grey_6}; + display: flex; + justify-content: center; + align-items: center; + width: 5.92rem; + height: 1.5rem; + ${tokens.typography.B3_M_14}; + color: ${tokens.colors.Grey_6}; - &:hover { - background-color: rgba(102, 201, 255, 0.2); - } + &:hover { + background-color: rgba(102, 201, 255, 0.2); + } - cursor: pointer; + cursor: pointer; `; // 페이지 export const PaginationContainer = styled.div` - display: flex; - justify-content: space-between; - width: 50rem; - margin-top: 1rem; + display: flex; + justify-content: space-between; + width: 50rem; + margin-top: 1rem; `; export const BlankBtn = styled.div` - width: 7.917rem; + width: 7.917rem; `; export const WriteBtn = styled.button` - background-color: ${tokens.colors.Blue_0_Main}; - color: ${tokens.colors.White}; - width: 7.917rem; - height: 2rem; - border: none; - border-radius: 0.16rem; - cursor: pointer; + background-color: ${tokens.colors.Blue_0_Main}; + color: ${tokens.colors.White}; + width: 7.917rem; + height: 2rem; + border: none; + border-radius: 0.16rem; + cursor: pointer; `; export const Pagination = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding: 0.83rem; - list-style: none; - // margin-top: 1.6rem; + display: flex; + justify-content: center; + align-items: center; + padding: 0.83rem; + list-style: none; + // margin-top: 1.6rem; `; export const PaginationArrow = styled.div` - width: 1rem; - height: 1rem; - background-image: url('/img/grayarrow.png'); - background-size: contain; - background-repeat: no-repeat; - transform: ${(props) => (props.left ? 'rotate(180deg)' : 'none')}; - cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; - opacity: ${(props) => (props.disabled ? 0.5 : 1)}; + width: 1rem; + height: 1rem; + background-image: url("/img/grayarrow.png"); + background-size: contain; + background-repeat: no-repeat; + transform: ${(props) => (props.left ? "rotate(180deg)" : "none")}; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + opacity: ${(props) => (props.disabled ? 0.5 : 1)}; `; export const PaginationNumber = styled.div` - display: flex; - justify-content: center; - align-items: center; - margin: 0 0.21rem; - width: 0.33rem; - height: 0.88rem; - padding: 0.42rem; - cursor: pointer; - color: ${(props) => (props.active ? tokens.colors.Blue_3 : tokens.colors.B_Grey_7)}; - font-weight: ${(props) => (props.active ? 'bold' : 'normal')}; - ${tokens.typography.B3_M_14}; + display: flex; + justify-content: center; + align-items: center; + margin: 0 0.21rem; + width: 0.33rem; + height: 0.88rem; + padding: 0.42rem; + cursor: pointer; + color: ${(props) => + props.active ? tokens.colors.Blue_3 : tokens.colors.B_Grey_7}; + font-weight: ${(props) => (props.active ? "bold" : "normal")}; + ${tokens.typography.B3_M_14}; `; diff --git a/src/APP/user-pages/Langding/Langding.landing.jsx b/src/APP/user-pages/Langding/Langding.landing.jsx index c9d1233f..cc7ce471 100644 --- a/src/APP/user-pages/Langding/Langding.landing.jsx +++ b/src/APP/user-pages/Langding/Langding.landing.jsx @@ -3,6 +3,7 @@ import * as itemS from "./Styled/Langing.landing"; import request from "../../Api/request"; import { Link } from "react-router-dom"; import axios from "axios"; +import DailyChallengeWidget from "../../components/Widget/DailyChallengeWidget/Widget.DailyChallengeWidget.main"; export default function Langding() { const [detailRecentGeneration, setDetailRecentGeneration] = useState(null); @@ -19,7 +20,7 @@ export default function Langding() { // console.log("스터디 최신 기수 api", response); setDetailRecentGeneration(response.data.result); if (response.data["isSuccess"]) { - console.log("api 연동 성공"); + // console.log("api 연동 성공"); } else { console.error("api 연동 실패:", response); } @@ -36,7 +37,7 @@ export default function Langding() { console.log("최신 기수 스터디 개수 api", response); setDetailStudyCount(response.data.result); if (response.data["isSuccess"]) { - console.log("api 연동 성공"); + // console.log("api 연동 성공"); } else { console.error("api 연동 실패:", response); } @@ -47,7 +48,7 @@ export default function Langding() { const checkLoginStatus = async () => { try { const response = await request.get("/member/info"); - console.log("로그인 멤버 정보 조회", response); + // console.log("로그인 멤버 정보 조회", response); if (response["isSuccess"]) { setIsLoggedIn(true); localStorage.setItem("memberId", response.result.memberId); @@ -120,6 +121,7 @@ export default function Langding() { + ); } diff --git a/src/APP/user-pages/Mypage/Mypage.mypage.challenge.rewardprogress.jsx b/src/APP/user-pages/Mypage/Mypage.mypage.challenge.rewardprogress.jsx new file mode 100644 index 00000000..b658bfc7 --- /dev/null +++ b/src/APP/user-pages/Mypage/Mypage.mypage.challenge.rewardprogress.jsx @@ -0,0 +1,84 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import * as ItemS from "./Styled/Mypage.mypage.challenge.rewardprogress.styles"; + +export default function RewardProgress({ + challengeRewardCount, + challengeWinCount, + regularStudyId, +}) { + const navigate = useNavigate(); + + const handleUseReward = () => { + // id 값이 없는 경우를 대비한 방어 코드 + if (!regularStudyId) { + console.error("스터디 ID가 전달되지 않았습니다."); + return; + } + + // 로컬 스토리지 키 생성 + const storageKey = `activeComponent_${regularStudyId}`; + + // 'attendance' 컴포넌트가 보이도록 로컬 스토리지에 값 설정 + localStorage.setItem(storageKey, "attendance"); + + // 페이지 이동 + navigate(`/regularstudy/${regularStudyId}`); + }; + + return ( + + + + + 누적 교환권 : {challengeRewardCount} + + + + + + + + + + + + + + + + + + + + + + + {/* + + */} + + + 보상 사용하기 + + ); +} diff --git a/src/APP/user-pages/Mypage/Mypage.mypage.challenge.table.jsx b/src/APP/user-pages/Mypage/Mypage.mypage.challenge.table.jsx new file mode 100644 index 00000000..922bbbee --- /dev/null +++ b/src/APP/user-pages/Mypage/Mypage.mypage.challenge.table.jsx @@ -0,0 +1,200 @@ +import React, { useState, useRef, useContext } from "react"; +import { useNavigate } from "react-router-dom"; +import ChallengeTuple from "./Mypage.mypage.challenge.tuple"; +import RewardProgress from "./Mypage.mypage.challenge.rewardprogress"; +import * as itemS from "./Styled/Mypage.mypage.challenge.table.styles"; +import request from "../../Api/request"; +import { AlertContext } from "../../Common/Alert/AlertContext"; + +export default function ChallengeTable({ + items, + logCount, + rewardCount, + winCount, + regularStudyId, + isMemberMatch, + fetchRewardLog, + onChangeLogType, +}) { + // const [count, setCount] = useState(inquiryCount); + const [challengeRewardCount, setChallengeRewardCount] = useState(rewardCount); + const [challengeWinCount, setChallengeWinCount] = useState(winCount); + + const [sortType, setSortType] = useState("LATEST"); + const [sortText, setSortText] = useState("전체"); + const [isSortDropVisible, setIsSortDropVisible] = useState(false); // 정렬 드롭박스 열기/닫기 + + // 스크롤 동기화를 위한 참조 + const contentRef = useRef(null); + const scrollRef = useRef(null); + const [thumbTop, setThumbTop] = useState(0); + + // 스크롤 동기화 함수 + const handleScrollSync = (e) => { + const scrollable = e.target; + const syncScroll = + scrollable === contentRef.current + ? scrollRef.current + : contentRef.current; + + if (syncScroll) { + const scrollRatio = + scrollable.scrollTop / + (scrollable.scrollHeight - scrollable.clientHeight); + + if (scrollable === contentRef.current) { + setThumbTop(scrollRatio * (scrollRef.current.clientHeight - 96)); // Thumb 위치 업데이트 + } + syncScroll.scrollTop = scrollable.scrollTop; + } + }; + + // Thumb 위치 클릭으로 콘텐츠 스크롤 제어 + const handleThumbDrag = (e) => { + const containerHeight = contentRef.current.clientHeight; + const scrollableHeight = contentRef.current.scrollHeight; + const thumbHeight = scrollRef.current.clientHeight - 96; + + const newTop = Math.min( + Math.max(0, e.clientY - scrollRef.current.getBoundingClientRect().top), + thumbHeight + ); + + setThumbTop(newTop); + contentRef.current.scrollTop = + (newTop / thumbHeight) * (scrollableHeight - containerHeight); + }; + + const toggleSortDrop = () => { + setIsSortDropVisible((prevState) => !prevState); + }; + + const onSortType = (type) => { + setIsSortDropVisible(false); + setSortType(type); + if (type === "") { + setSortText("전체"); + onChangeLogType(""); // 전체 + } else if (type === "ACQUIRED") { + setSortText("획득"); + onChangeLogType("ACQUIRED"); + } else if (type === "USED") { + setSortText("사용"); + onChangeLogType("USED"); + } + }; + + return ( + + + + + 챌린지 보상 현황 + + 챌린지 보상은 참여 중인 정규 스터디의 출석부에서 사용하실 수 + 있습니다. + + + + + + + + + {sortText} + + + {isSortDropVisible && ( + + onSortType("")} + > + 전체 + + + onSortType("ACQUIRED")} + > + 획득 + + + onSortType("USED")} + > + 사용 + + + )} + + + + + 획득/사용 + 내용 + 획득/사용일 + 총합 + + + + {items.length === 0 ? ( + + 챌린지 보상 내역이 없습니다. + + ) : ( + items.map((item, index) => ( + handleCheckChange(item.inquiryId)} + // isMemberMatch={isMemberMatch} + /> + )) + )} + + + + + {logCount > 8 && ( + + + + + + + + )} + + + + + ); +} diff --git a/src/APP/user-pages/Mypage/Mypage.mypage.challenge.tuple.jsx b/src/APP/user-pages/Mypage/Mypage.mypage.challenge.tuple.jsx new file mode 100644 index 00000000..4bf88098 --- /dev/null +++ b/src/APP/user-pages/Mypage/Mypage.mypage.challenge.tuple.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import * as itemS from "./Styled/Mypage.mypage.challenge.tuple.styles"; + +export default function ChallengeTuple({ item }) { + const formatDate = (createdTime) => { + const date = new Date(createdTime); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}.${month}.${day}`; + }; + + // content가 너무 길 경우를 대비한 함수 (선택적으로 사용) + const truncateTitle = (name) => { + if (name && name.length > 30) { + return name.slice(0, 29) + "..."; + } + return name; + }; + + const moveToDetail = (num) => { + window.open(`https://www.acmicpc.net/problem/${num}`, "_blank"); + }; + + return ( + + {item.logType} + + {/* ✅ 수정된 부분: logType에 따라 조건부 렌더링 */} + {item.logType === "사용" ? ( + // '사용'일 경우 content를 표시 + {truncateTitle(item.content)} + ) : ( + // '획득'일 경우 기존 problemList를 표시 + + {item.problemList?.map((num, idx) => ( + + moveToDetail(num)} + > + {num} 번 + + {idx !== item.problemList.length - 1 && ( + | + )} + + ))} + + )} + + {formatDate(item.logDate)} + {item.rewardCount}회 + + ); +} diff --git a/src/APP/user-pages/Mypage/Mypage.mypage.main.jsx b/src/APP/user-pages/Mypage/Mypage.mypage.main.jsx index 7858541d..5a392fae 100644 --- a/src/APP/user-pages/Mypage/Mypage.mypage.main.jsx +++ b/src/APP/user-pages/Mypage/Mypage.mypage.main.jsx @@ -5,6 +5,7 @@ import ParticipatedStudyList from "./Mypage.mypage.participatedstudylist"; import AppliedStudyList from "./Mypage.mypage.appliedstudylist"; import MyBoardTable from "./Mypage.mypage.myboard.table"; import MyInquiryTable from "./Mypage.mypage.myinquiry.table"; +import ChallengeTable from "./Mypage.mypage.challenge.table"; import * as itemS from "./Styled/Mypage.mypage.main.styles"; import request from "../../Api/request"; @@ -22,6 +23,12 @@ export default function MyPage() { const [totalCount, setTotalCount] = useState(0); // 전체 글 수 const [inquiries, setInquiries] = useState([]); // 내 문의하기 글 const [inquiryCount, setInquiryCount] = useState(0); // 내 문의하기 글 수 + const [rewardLogs, setRewardLogs] = useState([]); // 내 챌린지 보상 이력 + const [logCount, setLogCount] = useState(0); // 내 챌린지 보상 이력 항목 수 + const [logType, setLogType] = useState(""); // "" | "ACQUIRED" | "USED" + const [rewardCount, setRewardCount] = useState(0); // 누적 교환권 수 + const [winCount, setWinCount] = useState(0); // 챌린지 win 수 + const [regularStudyId, setRegularStudyId] = useState(null); // 참여중인 정규 스터디 id // 내 스터디, 내가 쓴 글 탭 변경 const [selectedTab, setSelectedTab] = useState("study"); @@ -34,12 +41,24 @@ export default function MyPage() { const totalPagesPassStudy = Math.ceil(passStudyList.length / itemsPerPage); // 참여 스터디 총 페이지 수 const totalPagesApplyStudy = Math.ceil(applyStudyList.length / itemsPerPage); // 지원 스터디 총 페이지 수 + const fetchInfo = async () => { + try { + const response = await request.get("/member/info"); + // console.log("로그인 멤버 정규스터 정보 조회", response); + if (response.isSuccess && response.result.regularStudyId !== null) { + setRegularStudyId(response.result.regularStudyId); + } + } catch (error) { + console.error("로그인 멤버 정보 조회 실패", error); + } + }; + const fetchMyInfo = async () => { try { const response = await request.get(`/member/${handle}/info`); if (response.isSuccess) { - console.log("나의 정보 조회 성공", response); + // console.log("나의 정보 조회 성공", response); setMyInfoData(response.result); } else { console.error("나의 정보 조회 실패:", response); @@ -54,7 +73,7 @@ export default function MyPage() { const response = await request.get(`/member/${handle}/study`); if (response.isSuccess) { - console.log("나의 스터디 조회 성공", response); + // console.log("나의 스터디 조회 성공", response); setPassStudyList(response.result.passStudyList); setApplyStudyList(response.result.applyStudyList); } else { @@ -68,7 +87,7 @@ export default function MyPage() { const fetchBoard = async () => { try { const response = await request.get(`/member/${handle}/board`); - console.log("내 게시글 목록 조회 성공", response); + // console.log("내 게시글 목록 조회 성공", response); if (response.isSuccess) { setBoards(response.result.boardList); @@ -86,7 +105,7 @@ export default function MyPage() { const fetchinquiry = async () => { try { const response = await request.get(`/member/${handle}/inquiry`); - console.log("내 문의하기 목록 조회 성공", response); + // console.log("내 문의하기 목록 조회 성공", response); if (response.isSuccess) { setInquiries(response.result.inquiryList); @@ -99,7 +118,50 @@ export default function MyPage() { } }; + const fetchRewardLog = async (type = "") => { + try { + const url = type + ? `/challenge/reward/log?logType=${type}` + : `/challenge/reward/log`; + const response = await request.get(url); + // console.log("보상 로그 조회 성공", response); + if (response.isSuccess) { + setRewardLogs(response.result.rewardLogList); + setLogCount(response.result.totalCount); + } else { + console.error("보상 로그 조회 실패:", response); + } + } catch (error) { + console.error("보상 로그 조회 오류", error); + } + }; + + const fetchRewardStatus = async () => { + try { + const response = await request.get(`/challenge/reward/status`); + // console.log("내 챌린지 보상 현황 조회 성공", response); + + if (response.isSuccess) { + setRewardCount(response.result.rewardCount); + setWinCount(response.result.winCount); + } else { + console.error("내 챌린지 보상 현황 조회 실패:", response); + } + } catch (error) { + console.error("내 챌린지 보상 현황 조회 오류", error); + } + }; + + useEffect(() => { + fetchRewardStatus(); + }, []); + useEffect(() => { + fetchRewardLog(logType); + }, [handle, logType]); // logType이 바뀌면 자동 호출 + + useEffect(() => { + fetchInfo(); fetchMyInfo(); fetchMyStudy(); fetchBoard(); @@ -198,6 +260,17 @@ export default function MyPage() { isMemberMatch={isMemberMatch} fetchinquiry={fetchinquiry} /> + ) : selectedTab === "challenge" ? ( + ) : null} diff --git a/src/APP/user-pages/Mypage/Mypage.mypage.myinfo.jsx b/src/APP/user-pages/Mypage/Mypage.mypage.myinfo.jsx index 8a35137c..8fd13533 100644 --- a/src/APP/user-pages/Mypage/Mypage.mypage.myinfo.jsx +++ b/src/APP/user-pages/Mypage/Mypage.mypage.myinfo.jsx @@ -44,6 +44,12 @@ export default function MyInfo({ + handleTabClick("challenge")} + active={activeTab === "challenge"} + > + 챌린지 보상 + handleTabClick("study")} active={activeTab === "study"} diff --git a/src/APP/user-pages/Mypage/Mypage.mypage.studylist.jsx b/src/APP/user-pages/Mypage/Mypage.mypage.studylist.jsx index 990308d4..c93cfb6b 100644 --- a/src/APP/user-pages/Mypage/Mypage.mypage.studylist.jsx +++ b/src/APP/user-pages/Mypage/Mypage.mypage.studylist.jsx @@ -1,71 +1,85 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; +import React from "react"; +import { useNavigate } from "react-router-dom"; import * as itemS from "./Styled/Mypage.mypage.studylist.styles"; -export default function StudyList({ studyList }){ +export default function StudyList({ studyList }) { + const navigate = useNavigate(); - const navigate = useNavigate(); - - const moveToDetail = (type, id) => { - if (type === '정규') { - navigate(`/regularstudy/${id}`); - } else if (type === '자율') { - navigate(`/study/${id}`); - } - }; - + const moveToDetail = (type, id) => { + if (type === "정규") { + navigate(`/regularstudy/${id}`); + } else if (type === "자율") { + navigate(`/study/${id}`); + } + }; - // 스터디 제목 글자수 자르기 - const truncateStudyName = (name) => { - if (name.length > 12) { - return name.slice(0, 11) + '...'; - } - return name; - } + // 스터디 제목 글자수 자르기 + const truncateStudyName = (name) => { + if (name.length > 12) { + return name.slice(0, 11) + "..."; + } + return name; + }; - // createdTime에서 날짜 부분만 추출 - const formatDate = (dateTime) => { - return dateTime.split('T')[0]; // 'T'를 기준으로 split하여 앞부분(날짜)만 반환 - } + // createdTime에서 날짜 부분만 추출 + const formatDate = (dateTime) => { + // dateTime 값이 null, undefined, 빈 문자열 등 falsy 값일 경우 + if (!dateTime) { + return "날짜 정보 없음"; // 혹은 빈 문자열 '' 을 반환 + } + // 유효한 값일 때만 split 실행 + return dateTime.split("T")[0]; + }; return ( - - - - + + + + - - - - moveToDetail(studyList.studyType, studyList.studyId)}> - {truncateStudyName(studyList.studyName)} - - {studyList.studyType} - + + + + + moveToDetail(studyList.studyType, studyList.studyId) + } + > + {truncateStudyName(studyList.studyName)} + + {studyList.studyType} + - - - - {/* {studyList.memberCount} */} - {studyList.memberCount}명 - - - - - {studyList.studyType === "자율" ? ( - - - {studyList.leaderName} - - ) : ( - - )} - - {formatDate(studyList.createdTime)} - - - + + + + {/* {studyList.memberCount} */} + {studyList.memberCount}명 + + - - ) -}; + + {studyList.studyType === "자율" ? ( + + + {studyList.leaderName} + + ) : ( + + )} + + + {formatDate(studyList.createdTime)} + + + + + + ); +} diff --git a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.rewardprogress.styles.js b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.rewardprogress.styles.js new file mode 100644 index 00000000..04e7b28c --- /dev/null +++ b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.rewardprogress.styles.js @@ -0,0 +1,268 @@ +import * as tokens from "../../../../tokens"; +import styled, { css, keyframes } from "styled-components"; + +const Anima = keyframes` + 0% { + transform: translate(-100%, -100%) rotateZ(-45deg); + } + 70% { + transform: translate(100%, 100%) rotateZ(-45deg); + } + 100% { + transform: translate(100%, 100%) rotateZ(-45deg); + } +`; + +const fillAnimation = (target) => keyframes` + 0% { + width: 0%; + } + 100% { + width: ${target}%; + } +`; + +// 150% 커졌다가 100% 크기 유지 +// const targetPop = keyframes` +// 0% { +// transform: scale(1); +// opacity: 0; +// } +// 30% { +// transform: scale(1.5); +// opacity: 1; +// } +// 100% { +// transform: scale(1); +// opacity: 1; +// } +// `; + +// 0 -> 150 -> 100 +const growIn = keyframes` + 0% { + transform: scale(0); + opacity: 0; + } + 70% { + transform: scale(1.5); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +`; + +export const Effect = styled.div` + ${({ $isSelected }) => + !$isSelected && + css` + position: absolute; + top: 0; + left: 0; + width: 200%; + height: 200%; + opacity: 0.9; + background: linear-gradient( + to bottom, + transparent 0%, + white 50%, + transparent 100% + ); + animation: ${Anima} 2s ease-in-out infinite; + pointer-events: none; + `} +`; + +export const Container = styled.div` + position: relative; + width: 20.54rem; /* 493px */ + height: 3.79rem; /* 91px */ + background: #dfe8f1; + border-radius: 0 0.42rem 0.42rem 0.42rem; /* 10px */ + display: flex; + align-items: center; + padding: 0 0.5rem; /* 12px */ + box-sizing: border-box; +`; + +export const Badge = styled.div` + position: absolute; + top: -1rem; /* -24px */ + left: 0.33rem; /* 8px */ + height: 1.17rem; /* 28px */ + padding: 0 0.5rem; /* 12px */ + background: #dfe8f1; + display: flex; + align-items: center; + gap: 0.25rem; /* 6px */ + border-radius: 0.42rem 0.42rem 0 0; /* 10px */ +`; + +export const TriangleL = styled.div` + position: absolute; + top: -0.75rem; /* -18px */ + left: 0; + width: 0.38rem; /* 9.2px */ + height: 0.76rem; /* 18.2px */ + background: #dfe8f1; + clip-path: polygon(99% 0, 100% 0, 100% 100%, 0% 100%); +`; + +export const TriangleR = styled.div` + position: absolute; + top: -0.75rem; /* -18px */ + /* right: 14.36rem; */ + right: 14.04rem; + /* right: 13.74rem; */ + width: 0.42rem; /* 10px */ + height: 0.93rem; /* 22.2px */ + background: #dfe8f1; + clip-path: polygon(0 0, 1% 0, 100% 100%, 0% 100%); +`; + +export const BadgeText = styled.span` + ${tokens.typography.B2_M_16}; + color: ${tokens.colors.B_Grey_7}; +`; + +export const ProgressBarWrapper = styled.div` + position: relative; + top: 0.08rem; /* 2px */ + left: 0.58rem; /* 14px */ + width: 10.5rem; /* 252px */ + height: 3rem; +`; + +export const ProgressBackground = styled.div` + position: absolute; + top: 1.417rem; + left: 0; + width: 100%; + height: 0.167rem; + background: #d2d9e5; + border-radius: 0.083rem; +`; + +export const ProgressFill = styled.div` + position: absolute; + top: 1.417rem; + left: 0; + + height: 0.167rem; + background: linear-gradient( + 90deg, + rgba(0, 165, 255, 1) 74%, + rgba(0, 165, 255, 0) 100% + ); + border-radius: 0.083rem; + /* + ${({ $challengeWinCount }) => + $challengeWinCount === 0 && + css` + width: 7%; + `} + + ${({ $challengeWinCount }) => + $challengeWinCount === 1 && + css` + width: 42%; + `} + + ${({ $challengeWinCount }) => + $challengeWinCount === 2 && + css` + width: 75%; + `} */ + ${({ $challengeWinCount }) => { + const widths = { + 0: 7, + 1: 42, + 2: 75, + }; + const targetWidth = widths[$challengeWinCount] ?? 0; + return css` + animation: ${fillAnimation(targetWidth)} 1.8s ease-in-out forwards; + `; + }} +`; + +export const TargetIcon = styled.img` + position: absolute; + top: 0.5rem; + transform: translateX(-50%); + animation: ${growIn} 1s ease-in-out 1.8s forwards; + ${({ $challengeWinCount }) => { + if ($challengeWinCount === 0) return "left: 3%;"; + if ($challengeWinCount === 1) return "left: 38%;"; + if ($challengeWinCount === 2) return "left: 71%;"; + return ""; + }} + width: 0.96rem; + height: 2rem; + opacity: 0; + z-index: 3; + pointer-events: none; +`; + +export const Icon = styled.div` + position: absolute; + top: 0; + transform: translateX(-50%); + ${({ $position }) => $position && `left: ${$position};`} + + display: flex; // 반짝임 효과 + justify-content: center; + align-items: center; + overflow: hidden; + position: absolute; + border-radius: 0.21rem; + box-shadow: 0 0.167rem 0.625rem 0 rgba(45, 54, 59, 0.25); + + ${({ $size }) => + $size === "S" && + css` + width: 1rem; + height: 1rem; + `} + + ${({ $size }) => + $size === "M" && + css` + top: 0.733rem; + width: 1.334rem; // 2rem + height: 1.334rem; + `} + + ${({ $size }) => + $size === "L" && + css` + top: 0.4rem; + width: 2rem; // 3rem + height: 2rem; + `} +`; + +export const IconImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; /* 또는 contain */ +`; + +export const IconS = styled.img` + width: 0.583rem; + height: 0.583rem; +`; + +export const Button = styled.button` + background-color: ${tokens.colors.B_Grey_7}; + color: ${tokens.colors.White}; + ${tokens.typography.B2_M_16}; + width: 6.667rem; + height: 2rem; + border: none; + border-radius: 0.167rem; + margin-left: auto; + cursor: pointer; +`; diff --git a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.table.styles.js b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.table.styles.js new file mode 100644 index 00000000..75b9f264 --- /dev/null +++ b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.table.styles.js @@ -0,0 +1,241 @@ +import styled from "styled-components"; +import * as tokens from "../../../../tokens"; + +export const Container = styled.div` + @media (max-width: 600px) { + width: 32rem; + } +`; + +export const Table = styled.div` + display: flex; + flex-direction: column; +`; + +export const TableContainerWrapper = styled.div` + display: flex; + flex-direction: row; +`; + +export const TabBtnContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + width: 100%; + border-bottom: 0.04rem solid ${tokens.colors.B_Grey_3}; + padding-bottom: 0.5rem; + margin-top: 4rem; + margin-bottom: 0.83rem; +`; + +export const TabBox = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; +`; + +export const TabHead = styled.div` + ${tokens.typography.T3_B_24}; + position: relative; + + &::after { + content: ""; + position: absolute; + bottom: 0; // 컴포넌트의 바닥에 + left: 0; + width: 100%; + height: 2px; // 선 두께 + background-color: currentColor; // 텍스트 컬러를 따라감 (또는 tokens.colors.Black 등 지정 가능) + } +`; + +export const TabBody = styled.div` + ${tokens.typography.B2_M_16}; +`; + +export const SortTableContainer = styled.div` + display: flex; + flex-direction: column; + align-items: end; +`; + +// 정렬 +export const SortContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + background-color: ${tokens.colors.White}; + color: ${tokens.colors.Grey_8}; + width: ${({ logCount }) => (logCount > 8 ? "5rem" : "5.92rem")}; + height: 1.5rem; + border: 0.04rem solid ${tokens.colors.B_Grey_3}; + border-radius: 0.17rem; + margin-right: ${({ logCount }) => (logCount > 8 ? "1.1rem" : "0")}; + /* margin-right: 1.1rem; // 스크롤 있을 때 마진. 스크롤 없다면 마진 없이 너비 늘리기 */ + margin-bottom: 0.96rem; + position: relative; +`; + +export const CategoryDrop = styled.div` + ${tokens.typography.B3_M_14}; + color: ${tokens.colors.Grey_8}; + text-align: center; + cursor: pointer; +`; + +export const SortIcon = styled.img` + width: 1rem; + height: 1rem; + align-self: center; + cursor: pointer; +`; + +export const SortDrop = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: ${tokens.colors.White}; + width: ${({ logCount }) => (logCount > 8 ? "5rem" : "5.92rem")}; + height: 4.5rem; + border-radius: 0.17rem; + position: absolute; + box-shadow: 0 0.08rem 0.42rem 0.08rem rgba(58, 107, 135, 0.1); + z-index: 99; + top: -0.17rem; +`; + +export const SortText = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: ${({ logCount }) => (logCount > 8 ? "5rem" : "5.92rem")}; + height: 1.5rem; + ${tokens.typography.B3_M_14}; + color: ${tokens.colors.Grey_6}; + + &:hover { + background-color: #dfe8f1; + } + + cursor: pointer; +`; + +export const SortDivider = styled.div` + width: 100%; + height: 1px; + background-color: ${tokens.colors.B_Grey_3}; +`; + +// 카테고리 파트 시작 +export const TableContainer = styled.div` + display: flex; + position: relative; + flex-direction: column; + box-shadow: 0 0.17rem 0.42rem 0 rgba(77, 114, 158, 0.25); + border-radius: 0 0 0.32rem 0.32rem; +`; + +export const CategoryContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + background-color: #dfe8f1; + width: 50rem; + height: 2.292rem; + border-bottom: 0.04rem solid ${tokens.colors.B_Grey_4}; + border-radius: 0.32rem 0.32rem 0 0; + @media (max-width: 600px) { + width: 100%; + } +`; + +export const CategoryStatus = styled.div` + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.Black}; + text-align: center; + width: 8.2rem; +`; + +export const CategoryTitle = styled.div` + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.Black}; + text-align: center; + width: 21rem; +`; + +export const CategoryDate = styled.div` + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.Black}; + text-align: center; + width: 3.5rem; + margin-left: 10rem; +`; + +export const CategoryView = styled.div` + ${tokens.typography.T5_SB_16}; + color: ${tokens.colors.Black}; + width: 1.4rem; + margin-left: 3.333rem; + margin-right: 1.25rem; +`; + +// 카테고리 파트 끝 + +export const TupleContainerWrapper = styled.div` + display: flex; +`; + +export const TupleContainer = styled.div` + flex: 1; + overflow: auto; + max-height: 19rem; + display: flex; + flex-direction: column; + scrollbar-width: none; +`; + +export const ScrollbarContainer = styled.div` + display: flex; + flex-direction: column; + margin-left: 0.833rem; +`; + +export const ScrollTopArrow = styled.img` + margin-top: 2.292rem; +`; + +export const ScrollBottomArrow = styled.img``; + +export const ScrollbarWrapper = styled.div` + overflow-y: auto; + height: 18.417rem; + width: 0.25rem; + background-color: ${tokens.colors.B_Grey_3}; + border-radius: 0.125rem; + margin: 0.042rem 0; +`; + +export const ScrollbarThumb = styled.div` + width: 100%; + height: 96px; + background: ${tokens.colors.B_Grey_6}; + border-radius: 0.125rem; + position: relative; + top: 0; + cursor: pointer; +`; + +export const NoItemsContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + ${tokens.typography.B2_M_16}; + color: ${tokens.colors.B_Grey_5}; + min-height: 2.333rem; +`; diff --git a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.tuple.styles.js b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.tuple.styles.js new file mode 100644 index 00000000..c29e56ac --- /dev/null +++ b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.challenge.tuple.styles.js @@ -0,0 +1,124 @@ +import styled from "styled-components"; +import * as tokens from "../../../../tokens"; + +export const Container = styled.div``; + +// 튜플 파트 시작 +export const TupleContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + background-color: ${tokens.colors.White}; + width: 50rem; + min-height: 2.333rem; + border-bottom: 0.04rem solid ${tokens.colors.B_Grey_3}; + + /* &:hover { + background-color: ${(props) => + props.temp === "true" ? "inherit" : tokens.colors.B_Grey_2}; + cursor: ${(props) => + props.temp === "true" + ? "default" + : props["data-delete-yn"] + ? "not-allowed" + : "pointer"}; + } */ + @media (max-width: 600px) { + width: 100%; + } +`; + +// export const Blank = styled.div` +// width: 0.875rem; +// `; + +export const TupleStatus = styled.div` + display: flex; + justify-content: center; + align-items: center; + ${tokens.typography.B2_M_16}; + color: ${tokens.colors.Black}; + width: 8.2rem; + /* min-height: 2.333rem; */ +`; + +export const TupleTitleBox = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + /* min-height: 2.333rem; */ + width: 21rem; + /* margin-left: 3.3rem; + margin-right: 8.117rem; */ +`; + +// export const TupleTitle = styled.span` +// ${tokens.typography.B2_M_16}; +// display: flex; +// flex-direction: row; +// color: ${(props) => +// props["data-delete-yn"] ? tokens.colors.Sub_3 : tokens.colors.Black}; +// `; + +export const TupleTitle = styled.div` + ${tokens.typography.B2_M_16}; + display: flex; + flex-wrap: wrap; + align-items: center; +`; + +export const ProblemBlock = styled.div` + display: flex; + align-items: center; + min-width: 4rem; // 문제 번호 블럭 최소 너비 + justify-content: flex-start; +`; + +export const ProblemNumber = styled.span` + width: 3rem; + text-align: ${({ $index }) => + $index === 0 ? "right" : $index === 1 ? "center" : "left"}; + margin-right: ${({ $index }) => ($index === 0 ? "0.2rem" : "0")}; + margin-left: ${({ $index }) => ($index === 2 ? "0.2rem" : "0")}; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +`; + +export const Divider = styled.span` + width: 1rem; + text-align: center; + user-select: none; +`; + +export const TupleDate = styled.div` + display: flex; + justify-content: center; + align-items: center; + ${tokens.typography.B2_M_16}; + color: ${tokens.colors.Black}; + /* width: 5.25rem; */ + width: 3.5rem; + margin-left: 10rem; + /* min-height: 2.333rem; */ +`; + +export const TupleView = styled.div` + display: flex; + justify-content: center; + align-items: center; + ${tokens.typography.B2_M_16}; + color: ${tokens.colors.Black}; + /* width: 3rem; + margin-left: 0.833rem; + margin-right: 2.43rem; */ + width: 1.4rem; + margin-left: 3.333rem; + margin-right: 1.25rem; + /* min-height: 2.333rem; */ +`; + +// 튜플 파트 끝 diff --git a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.table.styles.js b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.table.styles.js index e9396aa5..efc9b067 100644 --- a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.table.styles.js +++ b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.table.styles.js @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled from "styled-components"; import * as tokens from "../../../../tokens"; export const Container = styled.div` @@ -28,7 +28,7 @@ export const TabBtnContainer = styled.div` justify-content: flex-start; align-items: center; width: 100%; - border-bottom: 0.04rem solid ${tokens.colors.B_Grey_2}; + border-bottom: 0.04rem solid ${tokens.colors.B_Grey_3}; padding-bottom: 0.5rem; margin-top: 4rem; margin-bottom: 1.83rem; @@ -44,7 +44,7 @@ export const TabBox = styled.div` export const Tab = styled.div` ${tokens.typography.T3_B_24}; - text-decoration: ${(props) => (props.active ? 'underline' : 'none')}; + text-decoration: ${(props) => (props.active ? "underline" : "none")}; cursor: pointer; `; @@ -62,7 +62,7 @@ export const CategoryContainer = styled.div` flex-direction: row; justify-content: center; align-items: center; - background-color: #DFE8F1; + background-color: #dfe8f1; width: 50rem; height: 2.292rem; border-bottom: 0.04rem solid ${tokens.colors.B_Grey_4}; @@ -156,8 +156,7 @@ export const ScrollTopArrow = styled.img` margin-top: 2.292rem; `; -export const ScrollBottomArrow = styled.img` -`; +export const ScrollBottomArrow = styled.img``; export const ScrollbarWrapper = styled.div` overflow-y: auto; @@ -216,7 +215,7 @@ export const AllCheck = styled.input` } &:checked::before { - content: '✔'; + content: "✔"; color: #fff; font-size: 0.583rem; display: flex; diff --git a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.tuple.styles.js b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.tuple.styles.js index e9a0f73c..3429c9dc 100644 --- a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.tuple.styles.js +++ b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myboard.tuple.styles.js @@ -1,11 +1,7 @@ -import styled from 'styled-components'; -import * as tokens from "../../../../tokens" - - -export const Container = styled.div` - -`; +import styled from "styled-components"; +import * as tokens from "../../../../tokens"; +export const Container = styled.div``; // 튜플 파트 시작 export const TupleContainer = styled.div` @@ -13,20 +9,22 @@ export const TupleContainer = styled.div` flex-direction: row; justify-content: center; align-items: center; + background-color: ${tokens.colors.White}; width: 50rem; border-bottom: 0.04rem solid ${tokens.colors.B_Grey_3}; - // &:hover { - // background-color: ${tokens.colors.B_Grey_2}; - // cursor: ${(props) => (props['data-delete-yn'] ? 'not-allowed' : 'pointer')}; - // } &:hover { - background-color: ${(props) => (props.temp === 'true' ? 'inherit' : tokens.colors.B_Grey_2)}; - cursor: ${(props) => (props.temp === 'true' ? 'default' : props['data-delete-yn'] ? 'not-allowed' : 'pointer')}; + background-color: ${(props) => + props.temp === "true" ? "inherit" : tokens.colors.B_Grey_2}; + cursor: ${(props) => + props.temp === "true" + ? "default" + : props["data-delete-yn"] + ? "not-allowed" + : "pointer"}; } `; - export const CheckBox = styled.input` width: 0.875rem; height: 0.875rem; @@ -46,7 +44,7 @@ export const CheckBox = styled.input` } &:checked::before { - content: '✔'; + content: "✔"; color: #fff; font-size: 0.583rem; display: flex; @@ -74,7 +72,7 @@ export const InquiryCheckBox = styled.input` } &:checked::before { - content: '✔'; + content: "✔"; color: #fff; font-size: 0.583rem; display: flex; @@ -83,7 +81,6 @@ export const InquiryCheckBox = styled.input` } `; - export const Blank = styled.div` width: 0.875rem; `; @@ -112,17 +109,16 @@ export const TupleTitleBox = styled.div` // cursor: pointer; `; - export const DeletedIcon = styled.img` width: 0.833rem; height: 0.833rem; margin-right: 0.16rem; `; - export const TupleTitle = styled.span` ${tokens.typography.B2_M_16}; - color: ${(props) => (props['data-delete-yn']? tokens.colors.Sub_3 : tokens.colors.Black)}; + color: ${(props) => + props["data-delete-yn"] ? tokens.colors.Sub_3 : tokens.colors.Black}; `; export const HighlightedText = styled.span` @@ -131,17 +127,17 @@ export const HighlightedText = styled.span` `; export const NewIcon = styled.div` - display: flex; - justify-content: center; - align-items: center; - ${tokens.typography.B2_M_16}; - background-color: rgba(251, 170, 132, 0.2); - color: ${tokens.colors.Sub_3}; - width: 2.17rem; - height: 0.88rem; - border: none; - border-radius: 0.17rem; - margin-left: 0.17rem; + display: flex; + justify-content: center; + align-items: center; + ${tokens.typography.B2_M_16}; + background-color: rgba(251, 170, 132, 0.2); + color: ${tokens.colors.Sub_3}; + width: 2.17rem; + height: 0.88rem; + border: none; + border-radius: 0.17rem; + margin-left: 0.17rem; `; export const TupleWriter = styled.div` @@ -202,7 +198,8 @@ export const TupleProcess = styled.div` `; export const ProcessingYNBox = styled.div` - background-color: ${({ solvedYn }) => (solvedYn ? tokens.colors.Blue_0_Main : tokens.colors.Grey_4)}; + background-color: ${({ solvedYn }) => + solvedYn ? tokens.colors.Blue_0_Main : tokens.colors.Grey_4}; color: ${tokens.colors.White}; width: 3.167rem; height: 0.875rem; @@ -214,4 +211,3 @@ export const ProcessingYNBox = styled.div` `; // 튜플 파트 끝 - diff --git a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myinfo.styles.js b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myinfo.styles.js index 3efa3b12..c3593546 100644 --- a/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myinfo.styles.js +++ b/src/APP/user-pages/Mypage/Styled/Mypage.mypage.myinfo.styles.js @@ -57,7 +57,8 @@ export const TabBox = styled.div` flex-direction: row; align-items: center; justify-content: space-between; - width: ${(props) => (props.isInquiryVisible ? "13.34rem" : "8rem")}; + /* width: ${(props) => (props.isInquiryVisible ? "13.34rem" : "8rem")}; */ + width: ${(props) => (props.isInquiryVisible ? "20.84rem" : "15.5rem")}; height: 2rem; `; diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.attendance.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.attendance.jsx index fc2b3989..a7cac79f 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.attendance.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.attendance.jsx @@ -1,198 +1,437 @@ -import React, { useEffect, useState } from 'react' -import * as itemS from "../RegularStudy/Styled/RegularStudy.regularstudy.attendance.styles" -import RegularStudyCheckAttendanceHistoryModal from "./RegularStudy.regularstudy.checkattendancehistorymodal"; +import React, { useEffect, useState, useContext } from 'react'; +import * as itemS from '../RegularStudy/Styled/RegularStudy.regularstudy.attendance.styles'; +import RegularStudyCheckAttendanceHistoryModal from './RegularStudy.regularstudy.checkattendancehistorymodal'; import request from '../../Api/request'; import { useParams } from 'react-router-dom'; import AttendanceModal from './RegularStudy.regularstudy.modal'; +import { AlertContext } from '../../Common/Alert/AlertContext'; +import { ConfirmContext } from '../../Common/Confirm/ConfirmContext'; -export default function RegularStudyAttendance() { - const { id } = useParams(); //해당 스터디의 ID를 받아온다 +export default function RegularStudyAttendance({ memberRole, endYn }) { + const { id } = useParams(); const [currentTab, setCurrentTab] = useState('문제 인증'); - const [data, setData] = useState({}); // 초기 데이터 상태를 빈 객체로 설정 + const [data, setData] = useState({}); const [week, setWeek] = useState(0); - const [showAuthModal, setShowAuthModal] = useState(false); // 출석 인증을 위한 모달창 + const [showAuthModal, setShowAuthModal] = useState(false); const [showCertificationBtn, setShowCertificationBtn] = useState(false); const [noticeMessage, setNoticeMessage] = useState(null); const [attendanceRequestList, setAttendanceRequestList] = useState([]); const [attendanceRequesterName, setAttendanceRequesterName] = useState(null); - const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); // 출석부 인증내역 조회를 위한 모달창 + const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); + const [accumulatedTickets, setAccumulatedTickets] = useState(1); + + // 챌린지 보상 모드 상태 + const [isChallengeRewardMode, setIsChallengeRewardMode] = useState(false); + const [selectedAttendances, setSelectedAttendances] = useState([]); + const [currentUser, setCurrentUser] = useState(null); + const [originalAttendanceData, setOriginalAttendanceData] = useState([]); + + const { alert } = useContext(AlertContext); + const { confirm } = useContext(ConfirmContext); useEffect(() => { const fetchAttendance = async () => { try { const response = await request.get(`study/${id}/attendance`); - // console.log("정규스터디 출석부 조회: ", response); - - if (response["isSuccess"]) { + if (response['isSuccess']) { + // console.log('출석 데이터 조회: ', response.result); + setOriginalAttendanceData(response.result.attendanceList); const transformedData = transformData(response.result.attendanceList); setData(transformedData); - console.log("정규스터디 출석부 성공"); } } catch (error) { - console.error("정규스터디 출석부 조회 오류", error); setShowCertificationBtn(false); - if(error?.response?.data?.code === "NOTICE") { + if (error?.response?.data?.code === 'NOTICE') { setNoticeMessage(error.response.data.message); } } }; + const fetchWeek = async () => { try { const response = await request.get('/week/current'); - // console.log("현재 주차 정보 조회: ", response); - if(response["isSuccess"]){ + if (response['isSuccess']) { setShowCertificationBtn(true); setWeek(response.result.week); } else { setShowCertificationBtn(false); } } catch (error) { - console.error("현재 주차 정보 조회 실패: ", error); - if(error?.response?.data?.code === "NOTICE") { + if (error?.response?.data?.code === 'NOTICE') { setNoticeMessage(error.response.data.message); } - if (error?.response?.data?.code === "ATTENDANCE_ENDED") { - setWeek(8) + if (error?.response?.data?.code === 'ATTENDANCE_ENDED') { + setWeek(8); } } }; + + const fetchCurrentUser = async () => { + try { + const response = await request.get('/member/my-info'); + if (response['isSuccess']) { + setCurrentUser({ + handle: response.result.handle, + name: response.result.name, + }); + } + } catch (error) {} + }; + fetchAttendance(); fetchWeek(); + fetchCurrentUser(); }, [id]); + useEffect(() => { + if (originalAttendanceData.length > 0) { + // console.log('원본 출석 데이터:', originalAttendanceData); + const transformedData = transformData(originalAttendanceData); + setData(transformedData); + } + }, [ + isChallengeRewardMode, + selectedAttendances, + currentUser, + originalAttendanceData, + ]); + + const fetchChallengeRewardLog = async () => { + try { + const response = await request.get('/challenge/reward/status'); + // console.log('챌린지 보상 이력 조회: ', response); + if (response['isSuccess']) { + setAccumulatedTickets(response.result.rewardCount); + } + } catch (error) { + // console.error('챌린지 보상 이력 조회 오류', error); + } + }; + const fetchAttendanceRequestList = async (handle) => { try { const response = await request.get(`/attendance-request/${id}/${handle}`); - // console.log("출석 요청 내역 목록 조회: ", response); - - if (response["isSuccess"]) { + if (response['isSuccess']) { setAttendanceRequestList(response.result.attendanceRequestList || []); } } catch (error) { - console.error("출석 요청 내역 목록 조회 오류", error); + // console.error('출석 요청 내역 목록 조회 오류', error); } }; const transformData = (attendanceList) => { const data = { - '문제 인증': [['문제 인증', '1주차', '2주차', '3주차', '4주차', '5주차', '6주차', '7주차', '8주차']], - '블로그 포스팅': [['블로그 포스팅', '1주차', '2주차', '3주차', '4주차', '5주차', '6주차', '7주차', '8주차']], - '주말 모의테스트': [['주말 모의테스트', '1주차', '2주차', '3주차', '4주차', '5주차', '6주차', '7주차', '8주차']] + '문제 인증': [ + [ + '문제 인증', + '1주차', + '2주차', + '3주차', + '4주차', + '5주차', + '6주차', + '7주차', + '8주차', + ], + ], + '블로그 포스팅': [ + [ + '블로그 포스팅', + '1주차', + '2주차', + '3주차', + '4주차', + '5주차', + '6주차', + '7주차', + '8주차', + ], + ], + '주말 모의테스트': [ + [ + '주말 모의테스트', + '1주차', + '2주차', + '3주차', + '4주차', + '5주차', + '6주차', + '7주차', + '8주차', + ], + ], }; - + if (attendanceList.length === 0) { - Object.keys(data).forEach(key => { - const emptyRow = Array(data[key][0].length).fill(""); - emptyRow[0] = "학생이 없습니다"; + Object.keys(data).forEach((key) => { + const emptyRow = Array(data[key][0].length).fill(''); + emptyRow[0] = '학생이 없습니다'; data[key].push(emptyRow); }); return data; } - + const students = {}; - - attendanceList.forEach(({ name, handle, problemYN, blogYN, workbookYN, week }) => { - const uniqueKey = `${name}-${handle}`; - if (!students[uniqueKey]) { - students[uniqueKey] = { - '문제 인증': Array(9).fill(""), - '블로그 포스팅': Array(9).fill(""), - '주말 모의테스트': Array(9).fill("") - }; - ['문제 인증', '블로그 포스팅', '주말 모의테스트'].forEach((key) => { - students[uniqueKey][key][0] = ( -
{/* handle을 데이터 속성으로 저장 (표시 X) */} - {name} -
- ); - }); - } - - // week와 YN 필드들이 null인 경우 빈 값 유지 - if (week !== null) { - if (problemYN !== null) { - students[uniqueKey]['문제 인증'][week] = problemYN ? ( - - ) : ( - - ); - } - - if (blogYN !== null) { - students[uniqueKey]['블로그 포스팅'][week] = blogYN ? ( - - ) : ( - - ); + const filteredAttendanceList = + isChallengeRewardMode && currentUser + ? attendanceList.filter((item) => item.handle === currentUser.handle) + : attendanceList; + + filteredAttendanceList.forEach( + ({ + attendanceId, + name, + handle, + problemYN, + problemRewardYn, + blogYN, + blogRewardYn, + workbookYN, + workbookRewardYn, + week, + }) => { + const uniqueKey = `${name}-${handle}`; + if (!students[uniqueKey]) { + students[uniqueKey] = { + '문제 인증': Array(9).fill(''), + '블로그 포스팅': Array(9).fill(''), + '주말 모의테스트': Array(9).fill(''), + }; + ['문제 인증', '블로그 포스팅', '주말 모의테스트'].forEach((key) => { + students[uniqueKey][key][0] = ( +
{name}
+ ); + }); } - - if (workbookYN !== null) { - students[uniqueKey]['주말 모의테스트'][week] = workbookYN ? ( - - ) : ( - - ); + + if (week !== null) { + if (problemYN !== null) { + const isSelected = selectedAttendances.some( + (item) => + item.attendanceId === attendanceId && + item.attendanceType === 'PROBLEM' + ); + + students[uniqueKey]['문제 인증'][week] = problemYN ? ( + + + {isChallengeRewardMode && problemRewardYn && ( + + )} + + ) : ( + handleAttendanceClick(attendanceId, 'PROBLEM') + : undefined + } + /> + ); + } + + if (blogYN !== null) { + const isSelected = selectedAttendances.some( + (item) => + item.attendanceId === attendanceId && + item.attendanceType === 'BLOG' + ); + + students[uniqueKey]['블로그 포스팅'][week] = blogYN ? ( + + + {isChallengeRewardMode && blogRewardYn && ( + + )} + + ) : ( + handleAttendanceClick(attendanceId, 'BLOG') + : undefined + } + /> + ); + } + + if (workbookYN !== null) { + const isSelected = selectedAttendances.some( + (item) => + item.attendanceId === attendanceId && + item.attendanceType === 'WORKBOOK' + ); + + students[uniqueKey]['주말 모의테스트'][week] = workbookYN ? ( + + + {isChallengeRewardMode && workbookRewardYn && ( + + )} + + ) : ( + handleAttendanceClick(attendanceId, 'WORKBOOK') + : undefined + } + /> + ); + } } } - }); - - Object.keys(students).forEach(uniqueKey => { + ); + + Object.keys(students).forEach((uniqueKey) => { data['문제 인증'].push(students[uniqueKey]['문제 인증']); data['블로그 포스팅'].push(students[uniqueKey]['블로그 포스팅']); data['주말 모의테스트'].push(students[uniqueKey]['주말 모의테스트']); }); - + return data; }; const extractText = (element) => { - if (typeof element === "string") return element; + if (typeof element === 'string') return element; if (React.isValidElement(element)) { - return React.Children.map(element.props.children, child => - typeof child === "string" ? child : "" - ).join(""); + return React.Children.map(element.props.children, (child) => + typeof child === 'string' ? child : '' + ).join(''); } - return ""; + return ''; }; const getHandle = (element) => { - return element?.props?.["data-handle"] || null; + return element?.props?.['data-handle'] || null; }; - + const Table = ({ currentTab, onArrowClick, data }) => ( {data[currentTab]?.map((row, rowIndex) => ( {row.map((cell, colIndex) => ( - { - setIsHistoryModalOpen(true); - const extractedText = extractText(data[currentTab][rowIndex][0]); - const handle = getHandle(data[currentTab][rowIndex][0]); - fetchAttendanceRequestList(handle); - handleName(extractedText); - } : undefined} + onClick={ + rowIndex !== 0 && colIndex === 0 && !isChallengeRewardMode + ? () => { + setIsHistoryModalOpen(true); + const extractedText = extractText( + data[currentTab][rowIndex][0] + ); + const handle = getHandle(data[currentTab][rowIndex][0]); + fetchAttendanceRequestList(handle); + handleName(extractedText); + } + : undefined + } > {rowIndex === 0 && colIndex === 0 ? ( -
- 왼쪽 onArrowClick('prev')} - /> - {currentTab} - 오른쪽 onArrowClick('next')} - /> -
+
+ 왼쪽 onArrowClick('prev')} + /> + {currentTab} + 오른쪽 onArrowClick('next')} + /> +
) : ( cell )} @@ -207,7 +446,10 @@ export default function RegularStudyAttendance() { const handleArrowClick = (direction) => { const tabs = Object.keys(data); const currentIndex = tabs.indexOf(currentTab); - const newIndex = direction === 'next' ? (currentIndex + 1) % tabs.length : (currentIndex - 1 + tabs.length) % tabs.length; + const newIndex = + direction === 'next' + ? (currentIndex + 1) % tabs.length + : (currentIndex - 1 + tabs.length) % tabs.length; setCurrentTab(tabs[newIndex]); }; @@ -222,23 +464,190 @@ export default function RegularStudyAttendance() { const handleCloseHistoryModal = () => setIsHistoryModalOpen(false); const handleName = (name) => { setAttendanceRequesterName(name); - } + }; + + const toggleChallengeRewardMode = () => { + const newMode = !isChallengeRewardMode; + setIsChallengeRewardMode(newMode); + + if (newMode) { + fetchChallengeRewardLog(); + } else { + setSelectedAttendances([]); + } + }; + + const handleAttendanceClick = (attendanceId, attendanceType) => { + // console.log('아이콘 클릭:', { attendanceId, attendanceType }); + // console.log('현재 선택된 항목들:', selectedAttendances); + + setSelectedAttendances((prev) => { + const existingIndex = prev.findIndex( + (item) => + item.attendanceId === attendanceId && + item.attendanceType === attendanceType + ); + + if (existingIndex !== -1) { + // console.log('항목 제거:', { attendanceId, attendanceType }); + return prev.filter((_, index) => index !== existingIndex); + } else { + // console.log('항목 추가:', { attendanceId, attendanceType }); + return [...prev, { attendanceId, attendanceType }]; + } + }); + }; + + const applyChallengeReward = async () => { + if (selectedAttendances.length === 0) { + alert('선택된 출석 항목이 없습니다.'); + return; + } + + const confirmation = await confirm( + `${selectedAttendances.length}개의 챌린지 보상을 사용하시겠습니까? 적용 후에는 취소가 불가능합니다.` + ); + + if (confirmation) { + try { + const requestData = { + requestList: selectedAttendances, + }; + + // console.log('챌린지 보상 요청 데이터:', requestData); + // console.log('선택된 출석 항목들:', selectedAttendances); + + const response = await request.post('/challenge/reward', requestData); + + if (response['isSuccess']) { + alert('챌린지 보상이 적용되었습니다.'); + setSelectedAttendances([]); + setIsChallengeRewardMode(false); + + const attendanceResponse = await request.get( + `study/${id}/attendance` + ); + if (attendanceResponse['isSuccess']) { + setOriginalAttendanceData(attendanceResponse.result.attendanceList); + } + + fetchChallengeRewardLog(); + } + } catch (error) { + // console.error('챌린지 보상 적용 오류:', error); + // console.error('에러 응답:', error.response); + // if (error.response && error.response.data) { + // console.error('서버 응답 데이터:', error.response.data); + // console.error('서버 에러 메시지:', error.response.data.message); + // console.error('서버 에러 코드:', error.response.data.code); + // } + // alert('챌린지 보상 적용 중 오류가 발생했습니다.'); + } + } + }; + + // 출석부 데이터가 정상적으로 있는지 확인 + const hasValidAttendanceData = Object.keys(data).length > 0 && !noticeMessage; return ( 출석부 - *이름을 클릭하면 주차별 출석 인증 내역을 확인할 수 있습니다. - {Object.keys(data).length > 0 ? ( - + + {/* 정상적인 데이터가 있고 챌린지 보상 모드가 아닐 때만 표시 */} + {!isChallengeRewardMode && hasValidAttendanceData && ( +
+ + *이름을 클릭하면 주차별 출석 인증 내역을 확인할 수 있습니다. + + {!endYn && memberRole === 'MEMBER' && ( + + 챌린지 보상 사용하기 + + )} +
+ )} + + {/* 챌린지 보상 모드일 때만 표시 */} + {isChallengeRewardMode && ( +
+ + *출석부의 [X]를 클릭하여 챌린지 보상을 사용할 수 있습니다. + + + + + {accumulatedTickets} + 챌린지 교환권 + + +
+ )} + + {/* 테이블 또는 에러 메시지 표시 */} + {hasValidAttendanceData ? ( +
) : ( {noticeMessage} )} + + {/* 버튼 영역 */} - {showCertificationBtn && 출석 인증하기} + {showCertificationBtn && + !isChallengeRewardMode && + hasValidAttendanceData && ( + + 출석 인증하기 + + )} + {isChallengeRewardMode && ( + <> + + 목록으로 돌아가기 + + + 적용하기 + + + )} - - {showAuthModal && } - {isHistoryModalOpen && } + + {showAuthModal && ( + + )} + {isHistoryModalOpen && ( + + )} - ) -} \ No newline at end of file + ); +} diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculum.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculum.jsx index 7c3c2183..57351b89 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculum.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculum.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import * as itemS from "./Styled/RegularStudy.regularstudy.curriculum.styles"; +import * as itemS from './Styled/RegularStudy.regularstudy.curriculum.styles'; import request from '../../Api/request'; import { useNavigate, useParams } from 'react-router-dom'; @@ -14,14 +14,14 @@ export default function RegularStudyCurriculum() { const fetchCurriculumList = useCallback(async () => { try { const responseCurriculum = await request.get(`/study/${id}/curriculum`); - console.log("정규스터디 커리큘럼 목록 조회: ", responseCurriculum); - if (responseCurriculum["isSuccess"]) { - console.log("정규 스터디 조회 성공"); + // console.log("정규스터디 커리큘럼 목록 조회: ", responseCurriculum); + if (responseCurriculum['isSuccess']) { + // console.log("정규 스터디 조회 성공"); setCurriculumList(responseCurriculum.result.curriculumList); } } catch (error) { - console.error("정규스터디 커리큘럼 목록 조회 실패:", error); - if (error?.response?.data?.code === "NOTICE") { + console.error('정규스터디 커리큘럼 목록 조회 실패:', error); + if (error?.response?.data?.code === 'NOTICE') { setError(error.response.data.message); } } finally { @@ -32,7 +32,7 @@ export default function RegularStudyCurriculum() { const fetchCurrentWeek = useCallback(async () => { try { const responseCurrentWeek = await request.get('/week/current'); - console.log("현재 주차 정보 조회: ", responseCurrentWeek); + // console.log("현재 주차 정보 조회: ", responseCurrentWeek); if (responseCurrentWeek.isSuccess) { setCurrentWeek(responseCurrentWeek.result.week); // 현재 주차 상태 업데이트 } @@ -48,41 +48,44 @@ export default function RegularStudyCurriculum() { const handleCurriculumClick = (curriculumId) => { navigate(`/curriculumcheck/${curriculumId}`); - console.log("내가선택한 커리큘럼 아이디", curriculumId); + // console.log("내가선택한 커리큘럼 아이디", curriculumId); }; if (loading) return
Loading...
; - if (error) return ( - - 커리큘럼 - {error} - - ); + if (error) + return ( + + 커리큘럼 + {error} + + ); return ( 커리큘럼 - {curriculumList.map(curriculum => ( + {curriculumList.map((curriculum) => ( = currentWeek} // 현재 주차인지 확인하여 props로 전달 > - handleCurriculumClick(curriculum.curriculumId)}> + handleCurriculumClick(curriculum.curriculumId)} + > {curriculum.title} - {curriculum.week === currentWeek && ( // 현재 주차일 경우에만 이미지를 표시 + {curriculum.week === currentWeek && ( // 현재 주차일 경우에만 이미지를 표시 진행 중 )} - + 주차 {curriculum.week}주차 - ))} + ))} ); -} \ No newline at end of file +} diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculumcheck.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculumcheck.jsx index 3fe463c0..2ba6d8a9 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculumcheck.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.curriculumcheck.jsx @@ -1,57 +1,56 @@ import React, { useContext, useEffect, useState } from 'react'; -import * as itemS from "./Styled/RegularStudy.regularstudy.curriculumcheck.styles" -import request from '../../Api/request' +import * as itemS from './Styled/RegularStudy.regularstudy.curriculumcheck.styles'; +import request from '../../Api/request'; import { useParams } from 'react-router-dom'; import hljs from 'highlight.js'; import 'highlight.js/styles/atom-one-dark-reasonable.css'; import MarkdownContent from './RegularStudy.regularstudy.markdowncontent'; - export default function RegularStudyCurriculumCheck() { - const { curriculumId } = useParams(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [title, setTitle] = useState(''); - const [week, setWeek] = useState(0); - const [content, setContent] = useState(''); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await request.get(`/curriculum/${curriculumId}`); - console.log("커리큘럼 내용: ", response); - if (response["isSuccess"]) { - setData(response.result); - setTitle(response.result.title); - setWeek(response.result.week); - setContent(response.result.content); - } else { - setError('Failed to fetch data'); - } - } catch (error) { - setError('An error occurred while fetching data'); - } finally { - setLoading(false); + const { curriculumId } = useParams(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [title, setTitle] = useState(''); + const [week, setWeek] = useState(0); + const [content, setContent] = useState(''); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await request.get(`/curriculum/${curriculumId}`); + // console.log("커리큘럼 내용: ", response); + if (response['isSuccess']) { + setData(response.result); + setTitle(response.result.title); + setWeek(response.result.week); + setContent(response.result.content); + } else { + setError('Failed to fetch data'); } - }; - fetchData(); - }, [curriculumId]); - - useEffect(() => { - // 코드블록에 하이라이트 적용 - if (content) { - document.querySelectorAll('pre').forEach((block) => { - hljs.highlightBlock(block); - }); + } catch (error) { + setError('An error occurred while fetching data'); + } finally { + setLoading(false); } - }, [content]); + }; + fetchData(); + }, [curriculumId]); + + useEffect(() => { + // 코드블록에 하이라이트 적용 + if (content) { + document.querySelectorAll('pre').forEach((block) => { + hljs.highlightBlock(block); + }); + } + }, [content]); - if (loading) return
Loading...
; - if (error) return
{error}
; + if (loading) return
Loading...
; + if (error) return
{error}
; - return ( - + return ( + {title} @@ -60,9 +59,9 @@ export default function RegularStudyCurriculumCheck() { {week}주차 - + - - ); + + ); } diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.home.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.home.jsx index 5f473c68..e06643e1 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.home.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.home.jsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react' -import * as itemS from "../RegularStudy/Styled/RegularStudy.regularstudy.home.styles" +import React, { useEffect, useState } from 'react'; +import * as itemS from '../RegularStudy/Styled/RegularStudy.regularstudy.home.styles'; import request from '../../Api/request'; import { useParams } from 'react-router-dom'; import hljs from 'highlight.js'; @@ -13,17 +13,17 @@ export default function RegularStudyHome() { const fetchRegularStudyHome = async () => { try { const response = await request.get(`study/${id}/home`); - console.log("정규스터디 홈 정보 조회", response); + // console.log("정규스터디 홈 정보 조회", response); SetRegularStudyHome(response.result); - if (response["isSuccess"]) { - console.log("정규스터디 홈 정보 조회 성공"); - } else { - console.error("정규스터디 홈 정보 조회 실패:", response); - } + if (response['isSuccess']) { + console.log('정규스터디 홈 정보 조회 성공'); + } else { + console.error('정규스터디 홈 정보 조회 실패:', response); + } } catch (err) { - console.error("정규스터디 홈 정보 조회 오류", err); + console.error('정규스터디 홈 정보 조회 오류', err); } - } + }; fetchRegularStudyHome(); }, []); @@ -41,5 +41,5 @@ export default function RegularStudyHome() { - ) + ); } diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.main.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.main.jsx index 4986f86f..6aea0c05 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.main.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.main.jsx @@ -1,14 +1,30 @@ -import React, { useEffect, useState } from 'react' -import * as itemS from "./Styled/RegularStudy.regularstudy.main.styles"; -import RegularStudySideBar from "./RegularStudy.regularstudy.sidebar"; -import RegularStudyHome from "./RegularStudy.regularstudy.home" -import RegularStudyAttendance from "./RegularStudy.regularstudy.attendance" -import RegularStudyCurriculum from "./RegularStudy.regularstudy.curriculum" +import React, { useEffect, useState } from 'react'; +import * as itemS from './Styled/RegularStudy.regularstudy.main.styles'; +import RegularStudySideBar from './RegularStudy.regularstudy.sidebar'; +import RegularStudyHome from './RegularStudy.regularstudy.home'; +import RegularStudyAttendance from './RegularStudy.regularstudy.attendance'; +import RegularStudyCurriculum from './RegularStudy.regularstudy.curriculum'; import RegularStudyMocktest from './RegularStudy.regularstudy.mocktest'; import { useParams } from 'react-router-dom'; +import request from '../../Api/request'; export default function RegularStudyMain() { - const { id } = useParams(); // 정규스터디 ID 가져오기 + const { id } = useParams(); // 정규스터디 ID 가져오기 + const [regularStudyInfo, setRegularStudyInfo] = useState(null); + + const fetchRegularStudyInfo = async () => { + try { + const response = await request.get(`study/${id}/info`); + // console.log('정규 스터디 조회 정보: ', response); + if (response['isSuccess']) { + setRegularStudyInfo(response.result); + } else { + console.error('정규 스터디 조회 실패:', response); + } + } catch (err) { + console.error('정규스터디 정보 조회 오류', err); + } + }; const getInitialComponent = () => { const savedComponent = localStorage.getItem(`activeComponent_${id}`); @@ -18,15 +34,21 @@ export default function RegularStudyMain() { const [activeComponent, setActiveComponent] = useState(getInitialComponent); useEffect(() => { + fetchRegularStudyInfo(); localStorage.setItem(`activeComponent_${id}`, activeComponent); }, [activeComponent, id]); const renderComponent = () => { switch (activeComponent) { - case 'home': + case 'home': return ; case 'attendance': - return ; + return ( + + ); case 'curriculum': return ; case 'mocktest': @@ -34,13 +56,22 @@ export default function RegularStudyMain() { default: return ; } + }; + + if (!regularStudyInfo) { + return
Loading regular study info...
; } + return ( - + {renderComponent()} - ) + ); } diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.markdowncontent.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.markdowncontent.jsx index 34ba7243..94ded60e 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.markdowncontent.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.markdowncontent.jsx @@ -14,40 +14,46 @@ const removeExtraLineBreaks = (htmlContent) => { return htmlContent .replace(/>\s*\n\s*<') // 태그 사이의 줄바꿈 제거 .replace(/\n{2,}/g, '\n') // 두 개 이상의 연속된 줄바꿈 제거 - .replace(/
\s*(.*?)<\/summary>\s*/g, '
$1') + .replace( + /
\s*(.*?)<\/summary>\s*/g, + '
$1' + ) .replace(/\s*<\/details>/g, '
'); }; const preprocessMarkdownContent = (content) => { // 공백이 포함된 텍스트에도 Markdown 문법 적용 const patterns = [ - { regex: /(\s|^)\s*_(.*?)_\s*(\s|$)/g, wrap: "_" }, // 이탈릭 (underscore 사용) - { regex: /(\s|^)\s*\*\*(.*?)\*\*\s*(\s|$)/g, wrap: "**" }, // 볼드 (별표 사용) - { regex: /(\s|^)\s*~~(.*?)~~\s*(\s|$)/g, wrap: "~~" }, // 취소선 (물결표 사용) - { regex: /(\s|^)___(.*?)___(\s|$)/g, wrap: "___" }, // 굵은 기울임체 (underscore) + { regex: /(\s|^)\s*_(.*?)_\s*(\s|$)/g, wrap: '_' }, // 이탈릭 (underscore 사용) + { regex: /(\s|^)\s*\*\*(.*?)\*\*\s*(\s|$)/g, wrap: '**' }, // 볼드 (별표 사용) + { regex: /(\s|^)\s*~~(.*?)~~\s*(\s|$)/g, wrap: '~~' }, // 취소선 (물결표 사용) + { regex: /(\s|^)___(.*?)___(\s|$)/g, wrap: '___' }, // 굵은 기울임체 (underscore) ]; let processedContent = content; patterns.forEach(({ regex, wrap }) => { - processedContent = processedContent.replace(regex, (match, prefix, text, suffix) => { - const trimmedText = text.trim(); // 텍스트 양쪽 공백 제거 - return `${prefix || ""}${wrap}${trimmedText}${wrap}${suffix || ""}`; // 공백 보존 - }); + processedContent = processedContent.replace( + regex, + (match, prefix, text, suffix) => { + const trimmedText = text.trim(); // 텍스트 양쪽 공백 제거 + return `${prefix || ''}${wrap}${trimmedText}${wrap}${suffix || ''}`; // 공백 보존 + } + ); }); return processedContent; }; export default function MarkdownContent({ markdownContent }) { - console.log(markdownContent); + // console.log(markdownContent); useEffect(() => { // 코드블록에 하이라이트 적용 document.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block); + hljs.highlightElement(block); }); }, [markdownContent]); // markdownContent가 변경될 때마다 하이라이트 적용 - + const renderPreview = () => { const processedContent = preprocessMarkdownContent(markdownContent); @@ -61,5 +67,7 @@ export default function MarkdownContent({ markdownContent }) { return { __html: finalContent }; }; - return ; -} \ No newline at end of file + return ( + + ); +} diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.mocktest.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.mocktest.jsx index c32bf282..448a7795 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.mocktest.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.mocktest.jsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useState } from 'react' -import * as itemS from "./Styled/RegularStudy.regularstudy.mocktest.styles" +import React, { useCallback, useEffect, useState } from 'react'; +import * as itemS from './Styled/RegularStudy.regularstudy.mocktest.styles'; import Select, { components } from 'react-select'; import request from '../../Api/request'; import { useParams } from 'react-router-dom'; @@ -10,17 +10,21 @@ export default function RegularStudyMocktest() { const [currentWeek, setCurrentWeek] = useState(1); // 만약 기수가 다를 경우 1주차를 디폴트로 const [weekData, setWeekData] = useState({}); const [workbookId, setWorkbookId] = useState(null); - const [errorMesssage, setErrorMesssage] = useState("준비 중입니다."); + const [errorMesssage, setErrorMesssage] = useState('준비 중입니다.'); const WeeksSelect = ({ value, onChange }) => { const CustomDropdownIndicator = (props) => { return ( - triangle-icon + triangle-icon ); }; - + const options = [ { value: '1', label: '1주차' }, { value: '2', label: '2주차' }, @@ -31,16 +35,25 @@ export default function RegularStudyMocktest() { { value: '7', label: '7주차' }, { value: '8', label: '8주차' }, ]; - - const defaultValue = options.find(option => option.value === currentWeek?.toString()) || options[0]; - + + const defaultValue = + options.find((option) => option.value === currentWeek?.toString()) || + options[0]; + return ( option.value === value.toString()) : defaultValue} - onChange={selectedOption => onChange(selectedOption.value)} + value={ + value + ? options.find((option) => option.value === value.toString()) + : defaultValue + } + onChange={(selectedOption) => onChange(selectedOption.value)} placeholder="주차 선택" - components={{ DropdownIndicator: CustomDropdownIndicator, IndicatorSeparator: null }} + components={{ + DropdownIndicator: CustomDropdownIndicator, + IndicatorSeparator: null, + }} isSearchable={false} /> ); @@ -49,7 +62,7 @@ export default function RegularStudyMocktest() { const fetchCurrentWeek = useCallback(async () => { try { const responseCurrentWeek = await request.get('/week/current'); - console.log("현재 주차 정보 조회: ", responseCurrentWeek); + // console.log("현재 주차 정보 조회: ", responseCurrentWeek); if (responseCurrentWeek.isSuccess) { const currentWeekValue = responseCurrentWeek.result.week; setCurrentWeek(currentWeekValue); // 상태 업데이트 @@ -64,18 +77,18 @@ export default function RegularStudyMocktest() { const fetchQuestions = async () => { try { const response = await request.get(`/study/${id}/workbook`); - console.log("워크북 모의테스트 조회: ", response); + // console.log("워크북 모의테스트 조회: ", response); if (response.isSuccess) { const { workbookList } = response.result; const newWeekData = {}; - workbookList.forEach(workbook => { + workbookList.forEach((workbook) => { const { week, problemList, workbookId } = workbook; - const problems = problemList.map(problem => ({ + const problems = problemList.map((problem) => ({ id: problem.number.toString(), title: problem.name, levelImg: problem.levelUrl, baekjoonUrl: `https://www.acmicpc.net/problem/${problem.number}`, - workbookId: workbookId // workbookId 추가 + workbookId: workbookId, // workbookId 추가 })); newWeekData[week] = problems; @@ -92,7 +105,7 @@ export default function RegularStudyMocktest() { } } catch (error) { console.error('API error:', error); - if(error?.response?.data?.code === "NOTICE") { + if (error?.response?.data?.code === 'NOTICE') { setErrorMesssage(error.response.data.message); } return { isSuccess: false }; @@ -105,18 +118,18 @@ export default function RegularStudyMocktest() { // 현재 주차를 먼저 가져온다 const currentWeekResponse = await fetchCurrentWeek(); if (!currentWeekResponse) return; // 실패 시 처리 - + // 문제 데이터를 가져온다 const questionsResponse = await fetchQuestions(); if (questionsResponse?.isSuccess) { // currentWeek가 업데이트된 후에만 초기 week 상태 설정 - setWeek(currentWeekResponse); + setWeek(currentWeekResponse); } } catch (error) { console.error('데이터 초기화 중 오류:', error); } }; - + fetchData(); }, [id]); // id가 변경될 때마다 호출 const hasWeekData = weekData[week] && weekData[week].length > 0; @@ -141,11 +154,26 @@ export default function RegularStudyMocktest() { {row.id} - {row.title} + + {row.title} + - level + level ))} @@ -157,7 +185,6 @@ export default function RegularStudyMocktest() { {errorMesssage} )} - - ) -} \ No newline at end of file + ); +} diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.modal.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.modal.jsx index 96ca1803..4b84a14e 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.modal.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.modal.jsx @@ -6,124 +6,145 @@ import { AlertContext } from '../../Common/Alert/AlertContext'; import { ConfirmContext } from '../../Common/Confirm/ConfirmContext'; export default function AttendanceModal({ week, onClose }) { - const { id } = useParams(); // 스터디 id이다 - const [problemItems, setProblemItems] = useState([{ id: 1, title: '' }]); - const [blogUrl, setBlogUrl] = useState(''); - const { alert } = useContext(AlertContext); - const { confirm } = useContext(ConfirmContext); + const { id } = useParams(); // 스터디 id이다 + const [problemItems, setProblemItems] = useState([{ id: 1, title: '' }]); + const [blogUrl, setBlogUrl] = useState(''); + const { alert } = useContext(AlertContext); + const { confirm } = useContext(ConfirmContext); - useEffect(() => { - const fetchData = async () => { - try { - const response = await request.get(`/study/${id}/attendance-request`); - if (response.isSuccess) { - const { problemUrlList, blogUrl } = response.result; - setProblemItems( - problemUrlList.length > 0 - ? problemUrlList.map((url, index) => ({ id: index + 1, title: url })) - : [{ id: 1, title: '' }] - ); - setBlogUrl(blogUrl || ''); - } - } catch (error) { - console.error('데이터 로드 실패:', error); - } - }; - fetchData(); - }, [id]) - - const handleCloseModal = () => { - onClose(); // 모달 닫기 + useEffect(() => { + const fetchData = async () => { + try { + const response = await request.get(`/study/${id}/attendance-request`); + if (response.isSuccess) { + const { problemUrlList, blogUrl } = response.result; + setProblemItems( + problemUrlList.length > 0 + ? problemUrlList.map((url, index) => ({ + id: index + 1, + title: url, + })) + : [{ id: 1, title: '' }] + ); + setBlogUrl(blogUrl || ''); + } + } catch (error) { + console.error('데이터 로드 실패:', error); + } }; + fetchData(); + }, [id]); - const handleAddProblemItem = () => { - // 새로운 문제 인증 항목 추가 - const nextId = problemItems.length + 1; - setProblemItems([...problemItems, { id: nextId, title: '' }]); - }; + const handleCloseModal = () => { + onClose(); // 모달 닫기 + }; - const handleProblemTitleChange = (id, newTitle) => { - setProblemItems(problemItems.map(item => - item.id === id ? { ...item, title: newTitle } : item - )); - }; + const handleAddProblemItem = () => { + // 새로운 문제 인증 항목 추가 + const nextId = problemItems.length + 1; + setProblemItems([...problemItems, { id: nextId, title: '' }]); + }; - const handleBlogUrlChange = (e) => { - setBlogUrl(e.target.value); - }; + const handleProblemTitleChange = (id, newTitle) => { + setProblemItems( + problemItems.map((item) => + item.id === id ? { ...item, title: newTitle } : item + ) + ); + }; + + const handleBlogUrlChange = (e) => { + setBlogUrl(e.target.value); + }; - const handleSubmit = async () => { - const confirmation = await confirm("인증정보를 올리시겠습니까?"); - if(confirmation) { - try { - const problemUrlList = problemItems.map(item => item.title); - const response = await request.post('/attendance-request', { - studyId: parseInt(id), - problemUrlList: problemUrlList, - blogUrl: blogUrl - }); - if (response["isSuccess"]) { - console.log('출석 인증 요청 성공:', response.data); - alert("출석 인증 요청에 성공했습니다."); - onClose(); // 성공 시 모달 닫기 - } - } catch (error) { - console.error('출석 인증 요청 실패:', error); - } + const handleSubmit = async () => { + const confirmation = await confirm('인증정보를 올리시겠습니까?'); + if (confirmation) { + try { + const problemUrlList = problemItems.map((item) => item.title); + const response = await request.post('/attendance-request', { + studyId: parseInt(id), + problemUrlList: problemUrlList, + blogUrl: blogUrl, + }); + if (response['isSuccess']) { + // console.log('출석 인증 요청 성공:', response.data); + alert('출석 인증 요청에 성공했습니다.'); + onClose(); // 성공 시 모달 닫기 } - }; + } catch (error) { + console.error('출석 인증 요청 실패:', error); + } + } + }; - return ( - - e.stopPropagation()}> - - {week}주차 출석 인증 - X - + return ( + + e.stopPropagation()}> + + {week}주차 출석 인증 + X + - - - - 백준에서 푼 문제는 인증하지 않아도 되며,
블로그 URL은 홈 경로가 아닌 특정 글의 URL을 입력해 주세요. -
+ + + + 백준에서 푼 문제는 인증하지 않아도 되며,
+ 블로그 URL은 홈 경로가 아닌 특정 글의 URL을 입력해 주세요. +
- {problemItems.map((item) => ( - - 문제 인증({item.id}) - - - handleProblemTitleChange(item.id, e.target.value)} - /> - - - ))} + {problemItems.map((item) => ( + + 문제 인증({item.id}) + + + + handleProblemTitleChange(item.id, e.target.value) + } + /> + + + ))} - + - - 블로그 url - - - - - + + 블로그 url + + + + + - - 인증 정보 올리기 - -
-
-
- ); -} \ No newline at end of file + + 인증 정보 올리기 + + +
+
+ ); +} diff --git a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.sidebar.jsx b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.sidebar.jsx index 51e297cf..8b6dda82 100644 --- a/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.sidebar.jsx +++ b/src/APP/user-pages/RegularStudy/RegularStudy.regularstudy.sidebar.jsx @@ -1,36 +1,22 @@ import React, { useEffect, useState } from 'react'; -import * as itemS from "../RegularStudy/Styled/RegularStudy.regularstudy.sidebar.styles"; +import * as itemS from '../RegularStudy/Styled/RegularStudy.regularstudy.sidebar.styles'; import request from '../../Api/request'; import { useNavigate, useParams } from 'react-router-dom'; -export default function RegularStudySideBar({ setActiveComponent, activeComponent }) { - const { id } = useParams(); // 파라미터로 받는 해당 정규스터디의 studyId +export default function RegularStudySideBar({ + setActiveComponent, + activeComponent, + regularStudyInfo, +}) { const navigate = useNavigate(); - const [regularStudyInfo, setRegularStudyInfo] = useState(null); - - useEffect(() => { - const fetchRegularStudyInfo = async () => { - try { - const response = await request.get(`study/${id}/info`); - console.log("정규 스터디 사이드 바 조회 정보: ", response); - setRegularStudyInfo(response.result); - if (response["isSuccess"]) { - console.log("정규 스터디 조회 성공"); - } else { - console.error("정규 스터디 조회 실패:", response); - } - } catch (err) { - console.error("정규스터디 정보 조회 오류", err); - } - }; - fetchRegularStudyInfo(); - }, [id]); const handleApplicationClick = () => { if (regularStudyInfo.answerYN) { navigate(`/writingapplication/answer/${regularStudyInfo.answerId}`); } else { - navigate(`/writingapplication/application/${regularStudyInfo.applicationId}`); + navigate( + `/writingapplication/application/${regularStudyInfo.applicationId}` + ); } }; @@ -40,14 +26,35 @@ export default function RegularStudySideBar({ setActiveComponent, activeComponen {regularStudyInfo ? ( <> - 스터디 이미지 + 스터디 이미지 - {regularStudyInfo.studyName} 정규스터디 아이콘 + {regularStudyInfo.studyName}{' '} + 정규스터디 아이콘 - 사람아이콘 + 사람아이콘 {regularStudyInfo.memberCount}명 @@ -62,13 +69,15 @@ export default function RegularStudySideBar({ setActiveComponent, activeComponen onClick={() => setActiveComponent('curriculum')} isActive={activeComponent === 'curriculum'} > - 커리큘럼 + 커리큘럼{' '} + setActiveComponent('mocktest')} isActive={activeComponent === 'mocktest'} > - 모의테스트 + 모의테스트{' '} + setActiveComponent('attendance')} @@ -77,7 +86,9 @@ export default function RegularStudySideBar({ setActiveComponent, activeComponen 출석부 {regularStudyInfo.applicationId === null ? ( - 지원 기간이 아닙니다. + + 지원 기간이 아닙니다. + ) : ( 지원하기 )} diff --git a/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.attendance.styles.js b/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.attendance.styles.js index 5428646e..559fe2ad 100644 --- a/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.attendance.styles.js +++ b/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.attendance.styles.js @@ -1,5 +1,5 @@ -import styled from "styled-components"; -import * as tokens from "../../../../tokens"; +import styled from 'styled-components'; +import * as tokens from '../../../../tokens'; export const Container = styled.div` display: flex; @@ -18,7 +18,7 @@ export const Container = styled.div` export const Title = styled.div` display: flex; margin-top: 4.17rem; - margin-bottom: 0.792rem; + margin-bottom: 0.25rem; width: 100%; ${tokens.typography.T1_SB_32}; color: ${tokens.colors.Grey_8}; @@ -28,12 +28,115 @@ export const Title = styled.div` export const BlueComment = styled.div` display: flex; - width: 100%; - margin-bottom: 0.5rem; + align-items: center; ${tokens.typography.B3_M_14}; + line-height: 0.875rem; + letter-spacing: 0; + text-align: left; + vertical-align: middle; color: ${tokens.colors.Blue_0_Main}; `; +export const GrayBox = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 6.667rem; + height: 1.75rem; + border-radius: 0.167rem; + background-color: #dfe8f1; + color: ${tokens.colors.B_Grey_7}; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif; + font-weight: 700; + font-size: 0.583rem; + line-height: 0.875rem; + letter-spacing: 0; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; + } +`; + +// 누적 교환권 관련 스타일 추가 +export const TicketContainer = styled.div` + display: flex; + margin-right: 0.417rem; + position: relative; +`; + +export const TicketBox = styled.div` + display: flex; + align-items: center; + background-color: ${tokens.colors.Grey_3}; + width: 2.792rem; + height: 0.792rem; + border-radius: 0.396rem; + cursor: pointer; + position: relative; + + &:hover .tooltip { + visibility: visible; + opacity: 1; + } +`; + +export const TicketIcon = styled.img` + margin-left: -0.308rem; + margin-right: -0.108rem; + width: 2.2rem; +`; + +export const TicketCount = styled.div` + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + font-size: 0.583rem; + line-height: 0.875rem; + color: ${tokens.colors.Grey_6}; +`; + +// 툴팁 스타일 추가 +export const Tooltip = styled.div` + position: absolute; + left: 26%; + bottom: 1.433rem; + transform: translateX(-50%); + background-color: ${tokens.colors.Grey_8}; + color: white; + width: 5rem; + height: 1.5rem; + border-radius: 0.375rem; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif; + font-weight: 500; + font-size: 0.71rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + visibility: hidden; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 1000; + + &::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 0.275rem solid transparent; + border-right: 0.275rem solid transparent; + border-top: 0.375rem solid ${tokens.colors.Grey_8}; + } +`; export const CanNotEnterContainer = styled.div` display: flex; @@ -55,12 +158,16 @@ export const StyledTable = styled.table` export const StyledTd = styled.td` color: ${tokens.colors.Grey_7}; ${tokens.typography.T5_SB_16}; - border: 0.042rem solid #B9C4D2; - padding: 0.333rem; + border: 0.042rem solid #b9c4d2; + padding: 0.583rem; text-align: center; - - cursor: ${({ rowIndex, colIndex }) => (rowIndex !== 0 && colIndex === 0 ? 'pointer' : 'default')}; - transition: ${({ rowIndex, colIndex }) => (rowIndex !== 0 && colIndex === 0 ? 'background-color 0.2s ease-in-out, color 0.2s ease-in-out' : 'none')}; + + cursor: ${({ rowIndex, colIndex }) => + rowIndex !== 0 && colIndex === 0 ? 'pointer' : 'default'}; + transition: ${({ rowIndex, colIndex }) => + rowIndex !== 0 && colIndex === 0 + ? 'background-color 0.2s ease-in-out, color 0.2s ease-in-out' + : 'none'}; background-color: ${({ rowIndex, colIndex }) => { if (rowIndex === 0 && colIndex === 0) return 'rgba(0, 165, 255, 0.05)'; @@ -68,21 +175,24 @@ export const StyledTd = styled.td` if (colIndex === 0) return 'rgba(216, 216, 216, 0.05)'; return 'white'; }}; - border-top: ${({ rowIndex }) => (rowIndex === 0 ? 'none' : '0.042rem solid #B9C4D2')}; - border-left: ${({ colIndex }) => (colIndex === 0 ? 'none' : '0.042rem solid #B9C4D2')}; - border-right: ${({ colIndex }) => (colIndex === 8 ? 'none' : '0.042rem solid #B9C4D2')}; - width: ${({ colIndex }) => (colIndex === 0 ? '7.333rem' : 'auto')}; - height: ${({ rowIndex }) => (rowIndex === 0 ? '1.875rem' : '2.5rem')}; + border-top: ${({ rowIndex }) => + rowIndex === 0 ? 'none' : '0.042rem solid #B9C4D2'}; + border-left: ${({ colIndex }) => + colIndex === 0 ? 'none' : '0.042rem solid #B9C4D2'}; + border-right: ${({ colIndex }) => + colIndex === 8 ? 'none' : '0.042rem solid #B9C4D2'}; + width: ${({ colIndex }) => (colIndex === 0 ? '7.333rem' : '3.208rem')}; + height: ${({ rowIndex }) => (rowIndex === 0 ? '2.292rem' : '3.042rem')}; ${({ rowIndex, colIndex }) => - rowIndex !== 0 && colIndex === 0 && + rowIndex !== 0 && + colIndex === 0 && ` &:hover { background-color: ${tokens.colors.Blue_0_Main}; color: ${tokens.colors.B_Grey_1}; } - ` - } + `} `; export const ImgIcon = styled.img` @@ -90,6 +200,25 @@ export const ImgIcon = styled.img` height: 1.96rem; `; +// 아이콘 컨테이너 추가 - 상대적 위치 설정 +export const IconContainer = styled.div` + position: relative; + display: inline-block; + width: 1.96rem; + height: 1.96rem; +`; + +// 스탬프 이미지 스타일 추가 +export const StampIcon = styled.img` + position: absolute; + top: -0.6rem; + right: -0.6rem; + width: 1.506rem; + height: 1.506rem; + z-index: 2; + pointer-events: none; /* 클릭 이벤트 차단 */ +`; + export const BtnContainer = styled.div` display: flex; justify-content: flex-end; @@ -97,8 +226,20 @@ export const BtnContainer = styled.div` margin-top: 1.5rem; `; -export const CertificationBtn = styled.button` - width: 7.33rem; +export const PreviousBtn = styled.button` + margin-right: 0.708rem; + width: 6.667rem; + height: 2rem; + border-radius: 0.167rem; + border: none; + cursor: pointer; + color: ${tokens.colors.B_Grey_7}; + ${tokens.typography.T5_SB_16} + background-color: #DFE8F1; +`; + +export const ApiTriggerBtn = styled.button` + width: 6.667rem; height: 2rem; border-radius: 0.167rem; border: none; @@ -106,4 +247,4 @@ export const CertificationBtn = styled.button` color: ${tokens.colors.White}; ${tokens.typography.T5_SB_16} background-color: ${tokens.colors.B_Grey_7}; -`; \ No newline at end of file +`; diff --git a/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.sidebar.styles.js b/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.sidebar.styles.js index 6cb70bc9..09de560b 100644 --- a/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.sidebar.styles.js +++ b/src/APP/user-pages/RegularStudy/Styled/RegularStudy.regularstudy.sidebar.styles.js @@ -1,5 +1,5 @@ -import styled from "styled-components"; -import * as tokens from "../../../../tokens"; +import styled from 'styled-components'; +import * as tokens from '../../../../tokens'; export const Container = styled.div` display: flex; @@ -22,7 +22,8 @@ export const InnerContainer = styled.div` margin-bottom: 4.17rem; `; -export const StudyImgContainer = styled.div` /*해당 스터디의 이미지를 감싸주는 컨테이너*/ +export const StudyImgContainer = styled.div` + /*해당 스터디의 이미지를 감싸주는 컨테이너*/ display: flex; flex-direction: column; width: 14.29rem; @@ -30,11 +31,10 @@ export const StudyImgContainer = styled.div` /*해당 스터디의 이미지를 border-radius: 0.33rem; background-color: ${tokens.colors.Grey_4}; @media (max-width: 600px) { - } `; -export const TitleContainer = styled.div` +export const TitleContainer = styled.div` display: flex; flex-direction: row; max-width: 14.29rem; @@ -42,7 +42,11 @@ export const TitleContainer = styled.div` ${tokens.typography.T3_B_24}; margin-top: 0.67rem; align-items: center; - + + /* 줄바꿈 허용 */ + width: 100%; + word-wrap: break-word; + word-break: break-all; `; export const CountAndOnlineContainer = styled.div` @@ -85,7 +89,8 @@ export const LinkContainer = styled.div` export const styledLink = styled.div` display: flex; flex-direction: row; - color: ${(props) => (props.isActive ? tokens.colors.Blue_0_Main : tokens.colors.Grey_7)}; + color: ${(props) => + props.isActive ? tokens.colors.Blue_0_Main : tokens.colors.Grey_7}; ${tokens.typography.T5_SB_16}; padding-top: 0.71rem; padding-bottom: 0.75rem; @@ -97,7 +102,8 @@ export const styledLink = styled.div` export const ThirdstyledLink = styled.div` display: flex; flex-direction: row; - color: ${(props) => (props.isActive ? tokens.colors.Blue_0_Main : tokens.colors.Grey_7)}; + color: ${(props) => + props.isActive ? tokens.colors.Blue_0_Main : tokens.colors.Grey_7}; ${tokens.typography.T5_SB_16}; padding-top: 0.71rem; padding-bottom: 0.75rem; @@ -112,7 +118,8 @@ export const ArrowImg = styled.img` height: 1rem; `; -export const Btn = styled.button` /*지원하기 버튼*/ +export const Btn = styled.button` + /*지원하기 버튼*/ width: 14.33rem; height: 2rem; border-radius: 0.167rem; @@ -128,7 +135,8 @@ export const Btn = styled.button` /*지원하기 버튼*/ } `; -export const AnnouncementBlock = styled.button` /*지원 기간이 아닙니다 전용 박스*/ +export const AnnouncementBlock = styled.button` + /*지원 기간이 아닙니다 전용 박스*/ width: 14.33rem; height: 2rem; border-radius: 0.167rem; @@ -141,4 +149,4 @@ export const AnnouncementBlock = styled.button` /*지원 기간이 아닙니다 @media (max-width: 600px) { width: 100%; } -`; \ No newline at end of file +`; diff --git a/src/App.js b/src/App.js index 00de100d..e7994563 100644 --- a/src/App.js +++ b/src/App.js @@ -31,6 +31,9 @@ import BoardDetail from "./APP/user-pages/BoardDetail/BoardDetail.boarddetail.ma import WriteSelfStudy from "./APP/user-pages/WriteSelfStudy/WriteSelfStudy.writeselfstudy.main"; import InquiryBoardDetail from "./APP/user-pages/InquiryBoardDetail/InquiryBoardDetail.inquiryboarddetail.main"; import WriteInquiry from "./APP/user-pages/WriteInquiry/WriteInquiry.writeinquiry.main"; + +import DailyChallenge from "./APP/user-pages/DailyChallenge/DailyChallenge.dailychallenge.main"; + import ScrollToTop from "./APP/Common/ScrollToTop"; import useInterval from "./APP/Common/UseInterval"; import { refreshToken } from "./APP/Api/refreshToken"; @@ -85,12 +88,15 @@ function App() { const hideHeader = window.location.pathname .toLowerCase() .startsWith("/write"); + const darkHeader = window.location.pathname + .toLowerCase() + .startsWith("/dailychallenge"); return ( - {!hideHeader &&
} + {!hideHeader &&
} } /> @@ -193,6 +199,12 @@ function App() { />{" "} {/* 새 문의하기 글쓰기 */} {/* 새 문의하기 글쓰기 */} + + {/* 데일리 챌린지 메인 */} + } + />{" "} {!hideHeader &&
}