+
안녕하세요, {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 (
-
+
);
};
-
+
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}
+
-
+
))}
@@ -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}주차 출석 인증
-
-
+ return (
+
+ e.stopPropagation()}>
+
+ {week}주차 출석 인증
+
+
-
-
-
- 백준에서 푼 문제는 인증하지 않아도 되며,
블로그 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 && }