-
- Round {currentRound}
-
-
-
- {/*
*/}
-
-
- ⏱ {elapsedTimeFormatted}
-
-
- {/* !targetCode || targetCode.trim().length === 0 => true이면 */}
-
- {!targetCode || targetCode.trim().length === 0 ? (
-
- ) : (
-
-
- {lines.map((line, idx) => {
- const normalizedInput = userInput.split("");
- const isCurrent = idx === currentLine;
- const indent = line.length - line.trimStart().length;
- const indentSpaces = line.slice(0, indent);
- const content = line.trimStart();
-
- return (
-
-
- {indentSpaces.split("").map((_, i) => (
-
- ))}
-
-
- {isCurrent ? (
- <>
- {content.split("").map((char, i) => {
- const inputChar = normalizedInput[i];
- const isCursor = i === normalizedInput.length;
- let className = "";
-
- if (inputChar == null) {
- className = "pending currentLine";
- } else if (inputChar === char) {
- className = "typed currentLine";
- } else {
- className =
- char === " "
- ? "wrong currentLine bg-red-400"
- : "wrong currentLine";
- }
-
- return (
-
- {isCursor && (
-
- )}
-
- {char === " " ? "\u00A0" : char}
-
-
- );
- })}
-
- >
- ) : idx < currentLine ? (
- <>
-
- {line.split("").map((char, i) => (
-
- {char}
-
- ))}
-
-
- >
- ) : (
- <>
-
- {line.split("").map((char, i) => (
-
- {char}
-
- ))}
-
-
- >
- )}
-
- );
- })}
-
-
- )}
-
-
-
(e.target.placeholder = "")}
- onBlur={(e) => (e.target.placeholder = "Start Typing Code Here.")}
- className={`single-input w-full px-4 py-2 rounded-md text-black focus:outline-none
- ${
- isCorrect
- ? "border-4 border-green-400"
- : "border-4 border-red-400"
- }
- ${shake ? "animate-shake" : ""}`}
- />
-
-
- );
-};
-
-export default TypingBox;
diff --git a/src/components/multi/modal/AloneAlertModal.tsx b/src/components/multi/modal/AloneAlertModal.tsx
deleted file mode 100644
index aea8833..0000000
--- a/src/components/multi/modal/AloneAlertModal.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-// @ts-nocheck
-import boardImage from "../../../assets/images/board3.png";
-import confirmBtn from "../../../assets/images/board4.png";
-
-const AloneAlertModal = ({ roomInfo, onConfirm }) => {
- return (
-
-
- {/* 헤더 */}
-
- ⚠️ 방에 혼자 남았습니다
-
-
- {/* 설명 */}
-
- 상대방이 나가서
- 게임을 계속 진행할 수 없습니다.
-
-
- {/* 버튼 + 텍스트 오버레이 */}
-
{
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault(); // prevent page scroll on space
- onConfirm();
- }
- }}
- >
- {/* 버튼 이미지 */}
-
-
- {/* 텍스트 오버레이 */}
-
- 확인
-
-
-
-
-
- );
-};
-
-export default AloneAlertModal;
diff --git a/src/components/multi/modal/EnterRoomModal.tsx b/src/components/multi/modal/EnterRoomModal.tsx
deleted file mode 100644
index b2dd74a..0000000
--- a/src/components/multi/modal/EnterRoomModal.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-// @ts-nocheck
-import React, { useState } from "react";
-import modalBg from "../../../assets/images/board3.png";
-import cancleBtn from "../../../assets/images/multi_cancel_btn.png"; // 취소 버튼
-import goRoomBtn from "../../../assets/images/multi_go_game_room.png"; // 입장 버튼
-
-const EnterRoomModal = ({
- isPrivate,
- roomTitle,
- roomLanguage,
- currentPeople,
- standardPeople,
- onClose,
- onEnter,
- }) => {
- const [roomCode, setRoomCode] = useState("");
- const [error, setError] = useState(false);
- const [shake, setShake] = useState(false);
-
- const handleEnter = () => {
- onEnter(roomCode, (success) => {
- if (!success) {
- setError(true);
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- });
- };
-
-
- return (
-
-
-
- {/* 배경 이미지 */}
-
-
- {/* 내용 */}
-
-
- {/* 비공개방 (코드 입력) */}
- {isPrivate ? (
- <>
-
Room Code
-
[{roomTitle}]
-
방 코드를 입력해주세요!
-
-
{
- setRoomCode(e.target.value);
- if (error) setError(false); // 수정 시 에러 해제
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter") handleEnter();
- }}
- onFocus={(e) => (e.target.placeholder = "")} // 포커스 되면 placeholder 삭제
- onBlur={(e) => (e.target.placeholder = "방 코드 입력")} // 포커스 풀리면 다시 복구
- className={`border rounded px-4 py-2 text-black text-xl w-[240px] mb-4 mx-auto text-center
- ${error ? 'border-red-500 border-4' : ''}
- ${shake ? 'animate-shake' : ''}`} // 에러시 테두리 빨간색 + 흔들림
- placeholder="방 코드 입력"
- />
-
-
- {/* 취소 버튼 */}
-
-
-
-
- {/* 입장 버튼 */}
-
-
-
-
- >
- ) : (
- /* 공개방 (그냥 입장) */
- <>
-
입장하시겠습니까?
-
[{roomTitle}]
-
-
언어: {roomLanguage}
-
인원: {currentPeople}/{standardPeople}명
-
-
- {/* 취소 버튼 */}
-
-
-
-
- {/* 입장 버튼 */}
-
onEnter()}
- className="w-[120px] h-[50px] hover:brightness-110 hover:scale-95 transition"
- >
-
-
-
- >
- )}
-
-
-
-
- );
-};
-
-export default EnterRoomModal;
diff --git a/src/components/multi/modal/FinalResultModal.tsx b/src/components/multi/modal/FinalResultModal.tsx
deleted file mode 100644
index 881672e..0000000
--- a/src/components/multi/modal/FinalResultModal.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-// @ts-nocheck
-import React, { useEffect,useState } from "react";
-import { useNavigate } from "react-router-dom";
-import resultBg from "../../../assets/images/board3.png";
-import waitBtn from "../../../assets/images/board4.png";
-import rank1 from "../../../assets/images/rank_1.png";
-import rank2 from "../../../assets/images/rank_2.png";
-import rank3 from "../../../assets/images/rank_3.png";
-
-const FinalResultModal = ({ visible, results = [], onClose, roomInfo }) => {
- const navigate = useNavigate();
- const [fireworks, setFireworks] = useState([]);
-
- const rankIcons = [rank1, rank2, rank3];
-
- useEffect(() => {
- if (!visible) return;
-
- const interval = setInterval(() => {
- setFireworks((prev) => [
- ...prev,
- {
- id: Math.random(),
- left: `${Math.random() * 100}%`,
- top: `${Math.random() * 100}%`,
- size: Math.random() * 8 + 4,
- color: ["#ff0", "#f0f", "#0ff", "#0f0", "#f00"][Math.floor(Math.random() * 5)],
- },
- ]);
- }, 80);
-
- // 30개 생성 후 멈추기
- setTimeout(() => clearInterval(interval), 40 * 80);
-
- return () => clearInterval(interval);
- }, [visible]);
-
-
- if (!visible) return null;
-
- const handleConfirm = () => {
- navigate(`/multi/room/${String(roomInfo.roomId)}`, {
- state: { roomInfo },
- });
- onClose();
- };
-
- return (
-
- {/* 폭죽 레이어 */}
- {fireworks.map((fw) => (
-
- ))}
-
-
-
🏆 최종 결과 🏆
-
-
-
-
-
- 순위
- 닉네임
- 총점
-
-
-
- {results.map((user, i) => (
-
-
- {i < 3 ? (
-
- ) : (
- {i + 1}위
- )}
-
-
- {user.nickname}
-
-
- {user.totalScore} 점
-
-
- ))}
-
-
-
-
- {/* 버튼 영역 */}
-
-
-
-
- 대기방으로 이동!
-
-
-
-
-
- );
-};
-
-export default FinalResultModal;
\ No newline at end of file
diff --git a/src/components/multi/modal/MakeRoomModal.tsx b/src/components/multi/modal/MakeRoomModal.tsx
deleted file mode 100644
index 00cf522..0000000
--- a/src/components/multi/modal/MakeRoomModal.tsx
+++ /dev/null
@@ -1,256 +0,0 @@
-// @ts-nocheck
-import React, {useState} from "react";
-import modalBg from "../../../assets/images/board1.jpg";
-import makeRoomBtn from "../../../assets/images/make_room_btn.png";
-import cancleBtn from "../../../assets/images/multi_cancel_btn.png";
-import { useNavigate } from "react-router-dom";
-import useAuthStore from "../../../store/authStore";
-import MultiAlertModal from "../modal/MultiAlertModal";
-import changeBtn from "../../../assets/images/multi_room_info_change_btn.png";
-// Sockets removed; provide safe no-op shims
-const getSocket = () => ({ emit: () => {} });
-const createRoom = (_payload, cb) => cb?.({ roomId: Date.now(), roomCode: "0000" });
-
-const MakeRoomModal = ({ onClose, isEdit=false, initialData = {} }) => {
- const [title, setTitle] = useState(initialData.title || "");
- const [people, setPeople] = useState(initialData.people || undefined);
- const [language, setLanguage] = useState(initialData.language || "PYTHON");
- const [isPublic, setIsPublic] = useState(initialData.isPublic ?? true);
- const [activeArrow, setActiveArrow] = useState(null);
-
- const languages = ["PYTHON", "JAVA", "JS","SQL","RANDOM"]
- const navigate = useNavigate();
- const nickname = useAuthStore((state) => state.user?.nickname);
-
- const [alertMessage, setAlertMessage] = useState(null);
- const [isCreating, setIsCreating] = useState(false);
-
- const handleLangChange = (dir) => {
- const index = languages.indexOf(language);
- if (dir === "prev") {
- setLanguage(languages[(index - 1 + languages.length) % languages.length]);
- } else {
- setLanguage(languages[(index + 1) % languages.length]);
- }
- setActiveArrow(dir); // 화살표 색상 반짝
- setTimeout(() => setActiveArrow(null), 300);
- };
-
- const handleSubmit = () => {
- if (isCreating) return;
-
- if (!title || !people || !language || !nickname) {
- setAlertMessage("모든 항목을 입력해주세요!");
- return;
- }
-
- setIsCreating(true); // 중복 생성 방지지
-
-
-
- if (isEdit) {
- // 수정 요청
- const payload = {
- roomTitle : title,
- nickname,
- language,
- maxCount: people,
- isLocked: !isPublic,
- };
-
- const socket = getSocket();
- if (socket) {
- socket.emit("fix_room", {
- ...payload,
- roomId: initialData.roomId, // 반드시 전달되어야 함
- });
- }
- onClose(); // 모달 닫기
- } else {
- // 방 생성 로직 그대로 유지
- const payload = {
- title,
- nickname,
- language,
- maxNum: people,
- isLocked: !isPublic,
- };
- createRoom({ ...payload }, (res) => {
- if (!res || !res.roomId) {
- alert("방 생성 실패");
- return;
- }
- navigate(`/multi/room/${res.roomId}`, {
- state: {
- roomTitle: title,
- language,
- currentPeople: 1,
- standardPeople: people,
- isPublic,
- roomCode: res.roomCode,
- },
- });
- onClose();
- });
- }
- };
-
- return (
-
-
-
-
- {/* 모달 내부 콘텐츠 */}
- {/* 내부 콘텐츠 */}
-
-
- {isEdit ? "방 정보 수정" : "방 만들기"}
-
-
-
-
-
- {/* 방 제목 */}
-
- 제 목
- setTitle(e.target.value)}
- className="w-[470px] px-4 py-2 text-black font-bold rounded-md"
- placeholder="다함께 코드노바~"
- maxLength={10}
- />
-
-
- {/* 인원 수 */}
-
- 인원수
- {[2, 3, 4].map((n) => {
- const isDisabled = Number(initialData.currentPeople || 0) > n;
-
- return (
-
- setPeople(n)}
- disabled={isDisabled}
- className="peer scale-150 accent-fuchsia-600 rounded-sm"
- />
-
- {n}명
-
-
- );
- })}
-
-
- {/* 언어 */}
-
- 언 어
- handleLangChange("prev")}
- className={`
- transition-colors duration-300
- hover:text-fuchsia-400
- ${activeArrow === "prev" ? "text-fuchsia-400" : "text-white"}
- hover:brightness-110 hover:scale-[1.1] active:scale-[0.98]
- `}
- >
- ◀
-
-
- {language}
-
- handleLangChange("next")}
- className={`
- transition-colors duration-300
- hover:text-fuchsia-400
- ${activeArrow === "next" ? "text-fuchsia-400" : "text-white"}
- hover:brightness-110 hover:scale-[1.1] active:scale-[0.98]
- `}
- >
- ▶
-
-
-
-
- {/* 공개 여부 */}
-
- 공 개
-
- setIsPublic(true)}
- className="peer scale-150 accent-fuchsia-600 rounded-sm"
- />
-
- 공개
-
-
-
- setIsPublic(false)}
- className="peer scale-150 accent-fuchsia-600 rounded-sm"
- />
-
- 비공개
-
-
-
-
- {/* 버튼 */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {alertMessage && (
-
setAlertMessage(null)}
- />
- )}
-
-
- );
-};
-
-export default MakeRoomModal;
diff --git a/src/components/multi/modal/MultiAlertModal.tsx b/src/components/multi/modal/MultiAlertModal.tsx
deleted file mode 100644
index bb4e33d..0000000
--- a/src/components/multi/modal/MultiAlertModal.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-// @ts-nocheck
-import React, { useEffect, useRef } from "react";
-import { createPortal } from "react-dom";
-
-const MultiAlertModal = ({
- message = "메시지를 입력해주세요.",
- onConfirm,
- confirmText = "확인",
- showCancel = false,
- onCancel,
-}) => {
- const confirmButtonRef = useRef(null);
-
- useEffect(() => {
- confirmButtonRef.current?.focus();
-
- const handleKeyDown = (e) => {
- if (e.key === "Enter") {
- onConfirm();
- } else if (e.key === "Escape" && showCancel && onCancel) {
- onCancel();
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [onConfirm, onCancel, showCancel]);
-
- return createPortal(
-
-
-
{message}
-
-
-
- {confirmText}
-
-
- {showCancel && (
-
- 취소
-
- )}
-
-
-
,
- document.body
- );
-};
-
-export default MultiAlertModal;
diff --git a/src/components/multi/modal/RoundScoreModal.tsx b/src/components/multi/modal/RoundScoreModal.tsx
deleted file mode 100644
index 1ac72d5..0000000
--- a/src/components/multi/modal/RoundScoreModal.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// @ts-nocheck
-import React from "react";
-import roundBg from "../../../assets/images/board3.png";
-import rank1 from "../../../assets/images/rank_1.png";
-import rank2 from "../../../assets/images/rank_2.png";
-import rank3 from "../../../assets/images/rank_3.png";
-
-const RoundScoreModal = ({ visible, scores, round, countdown }) => {
- const rankIcons = [rank1, rank2, rank3];
- if (!visible) return null;
-
- return (
-
-
-
- [Round {round} 결과]
-
-
-
-
-
-
- 순위
- 닉네임
- 점수
- 오타 수
-
-
-
- {scores.map((user, i) => (
-
-
- {i < 3 ? (
-
- ) : (
- {i + 1}위 // ✅ 4등 이상 텍스트
- )}
-
-
- {user.nickname}
-
-
- {user.score} 점
- {user.typoCount} 개
-
- ))}
-
-
-
-
-
- 다음 라운드 시작까지 {countdown}초
-
-
-
- );
-};
-
-export default RoundScoreModal;
diff --git a/src/components/multi/waiting/RoomChatBox.tsx b/src/components/multi/waiting/RoomChatBox.tsx
deleted file mode 100644
index 1ef9082..0000000
--- a/src/components/multi/waiting/RoomChatBox.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-// @ts-nocheck
-import React, { useState, useRef, useEffect } from "react";
-
-const RoomChatBox = ({messages = [], onSendMessage }) => {
- const [input, setInput] = useState("");
- const messageListRef = useRef(null);
-
- const handleSend = () => {
- if (!input.trim()) return;
- onSendMessage?.({ type: "chat", text: input });
- setInput("");
- };
-
- useEffect(() => {
- if (messageListRef.current) {
- messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
- }
- }, [messages]);
-
- return (
-
-
- {messages.map((msg, index) => (
-
- {msg.text}
-
- ))}
-
-
-
- setInput(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleSend()}
- className="flex-1 px-4 py-2 rounded-md text-black text-m"
- placeholder="메시지를 입력하세요."
- />
-
- 전송
-
-
-
- );
-};
-
-export default RoomChatBox;
\ No newline at end of file
diff --git a/src/components/multi/waiting/RoomInfoPanel.tsx b/src/components/multi/waiting/RoomInfoPanel.tsx
deleted file mode 100644
index f539427..0000000
--- a/src/components/multi/waiting/RoomInfoPanel.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-// @ts-nocheck
-// components/multi/waiting/RoomInfoPanel.jsx
-import React from "react";
-import languageIcon from "../../../assets/images/multi_language_icon.png";
-import peopleIcon from "../../../assets/images/multi_people_icon.png";
-import startBtn from "../../../assets/images/multi_start_btn.png";
-import unReadyBtn from "../../../assets/images/multi_unready_btn.png";
-import readyBtn from "../../../assets/images/multi_ready_btn.png";
-import exitBtn from "../../../assets/images/multi_exit_btn.png";
-import copyBtn from "../../../assets/images/multi_copy_icon.png";
-import CustomAlert from "../../../components/multi/modal/MultiAlertModal";
-import { useState } from "react";
-
-
-const RoomInfoPanel = ({
- isPublic,
- roomCode,
- language,
- currentPeople,
- standardPeople,
- onLeave,
- onReady,
- isReady,
- isHost,
- allReady,
- onStart,
- canstart
-}) => {
-
- const [alertMessage, setAlertMessage] = useState(null);
-
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(roomCode);
- setAlertMessage("방 코드가 복사되었습니다.");
- } catch (err) {
- setAlertMessage("복사에 실패했습니다.")
- }
- }
-
- // console.log("📦 Panel props:", { isPublic, roomCode });
-
-
- return (
-
-
- {/* 상단 정보 */}
-
- {isPublic ? (
-
-
-
-
{language}
-
-
-
-
{currentPeople} / {standardPeople} 명
-
-
- ) : (
-
-
방 코드
-
-
{roomCode}
-
-
-
-
-
- )}
-
-
- {/* 버튼 영역 */}
-
- {/* 나가기 버튼 */}
-
-
-
-
- {isHost ? (
-
-
-
-
-
- {/* 툴팁 (hover 시 표시) */}
- {!canstart && (
-
-
- 최소 2명이 모여야
시작할 수 있어요!
-
-
-
- )}
-
- ) : (
-
-
-
- )}
-
-
- {alertMessage && (
-
setAlertMessage(null)} />
- )}
-
- );
-};
-
-export default RoomInfoPanel;
diff --git a/src/components/multi/waiting/RoomUserCard.tsx b/src/components/multi/waiting/RoomUserCard.tsx
deleted file mode 100644
index 8511340..0000000
--- a/src/components/multi/waiting/RoomUserCard.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-// @ts-nocheck
-import React from "react";
-import userProfileBg from "../../../assets/images/board2.jpg";
-import rocket1 from "../../../assets/images/multi_rocket_1.png";
-import rocket2 from "../../../assets/images/multi_rocket_2.png";
-import rocket3 from "../../../assets/images/multi_rocket_3.png";
-import rocket4 from "../../../assets/images/multi_rocket_4.png";
-import crownIcon from "../../../assets/images/multi_host_icon.png";
-
-const rockets = [rocket1, rocket2, rocket3, rocket4];
-
-const RoomUserCard = ({ user }) => {
- const isEmptySlot = user.empty;
- const rocketImage = rockets[user.slot - 1];
-
- return (
-
- {!isEmptySlot && user.isHost && (
-
- )}
-
- {/* 테두리 배경 */}
-
-
- {/* 슬롯 번호 (사용자가 있을 때만) */}
- {!isEmptySlot && (
-
- No.{user.slot}
-
-
-
- )}
-
- {/* 메인 내용 */}
-
- {isEmptySlot ? (
-
-
- ) : (
- <>
-
-
-
-
{user.nickname}
- {/*
{user.typing}
*/}
-
- {user.isReady ? "Ready" : "Not Ready"}
-
- >
- )}
-
-
- );
-};
-
-export default RoomUserCard;
diff --git a/src/components/multi/waiting/RoomUserList.tsx b/src/components/multi/waiting/RoomUserList.tsx
deleted file mode 100644
index cbf218e..0000000
--- a/src/components/multi/waiting/RoomUserList.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-// @ts-nocheck
-import React from "react";
-import RoomUserCard from "./RoomUserCard";
-
-const RoomUserList = ({ users }) => {
- return (
-
- {users.map((user, index) => (
-
- ))}
-
- );
-};
-
-export default RoomUserList;
diff --git a/src/components/single/AIChatModal.tsx b/src/components/single/AIChatModal.tsx
deleted file mode 100644
index 00b9fe0..0000000
--- a/src/components/single/AIChatModal.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { motion, AnimatePresence } from "framer-motion";
-import { useState, useEffect } from "react";
-import { css } from "../../../styled-system/css";
-import ChatBox from "./ChatBox";
-
-interface AIChatProps {
- isOpen: boolean;
- onClose: () => void;
- codeId: number | string | null;
-}
-
-const AIChat: React.FC
= ({ isOpen, onClose, codeId }) => {
- const [position, setPosition] = useState({ x: 0, y: 0 });
-
- useEffect(() => {
- if (isOpen) setPosition({ x: 0, y: 0 });
- }, [isOpen]);
-
- const container = css({
- position: "fixed",
- right: "2rem",
- bottom: "2rem",
- width: "50%",
- height: "70%",
- backgroundColor: "white",
- borderRadius: "1rem",
- boxShadow: "0 10px 30px rgba(0,0,0,0.35)",
- zIndex: 10,
- padding: "1rem",
- transformOrigin: "bottom right",
- });
-
- const header = css({
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- marginBottom: "0.5rem",
- borderBottomWidth: "2px",
- height: "10%",
- });
-
- const closeBtn = css({
- fontSize: "1.5rem",
- transition: "transform 150ms",
- _hover: { transform: "scale(1.1)" },
- });
-
- return (
-
- {isOpen && (
-
- setPosition({ x: info.point.x, y: info.point.y })
- }
- initial={{ clipPath: "circle(0% at 90% 90%)", opacity: 0 }}
- animate={{ clipPath: "circle(150% at 90% 90%)", opacity: 1 }}
- exit={{ clipPath: "circle(0% at 90% 90%)", opacity: 0 }}
- transition={{ duration: 0.4, ease: "easeInOut" }}
- className={container}
- >
-
-
- 💬 AI 개발자
-
-
- ×
-
-
-
-
- )}
-
- );
-};
-
-export default AIChat;
diff --git a/src/components/single/BoardContainer.tsx b/src/components/single/BoardContainer.tsx
deleted file mode 100644
index 7066deb..0000000
--- a/src/components/single/BoardContainer.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import box from "../../assets/images/board1.jpg";
-import type { PropsWithChildren } from "react";
-
-const Board2Container: React.FC = ({ children }) => {
- return (
-
- {children}
-
- );
-};
-
-export default Board2Container;
diff --git a/src/components/single/ChatBox.tsx b/src/components/single/ChatBox.tsx
deleted file mode 100644
index ae41ace..0000000
--- a/src/components/single/ChatBox.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import { useState, useRef, useEffect } from "react";
-import { chatBotRequest } from "../../api/singleApi";
-import { useChatStore } from "../../store/useChatStore";
-import { css } from "../../../styled-system/css";
-
-interface ChatBoxProps {
- codeId: number | string | null;
-}
-
-const ChatBox: React.FC = ({ codeId }) => {
- const addMessage = useChatStore((state: any) => state.addMessage);
- const replaceLastMessage = useChatStore(
- (state: any) => state.replaceLastMessage
- );
-
- const [content, setContent] = useState([]);
- const [currentInput, setCurrentInput] = useState("");
- const inputAreaRef = useRef(null);
- const chatEndRef = useRef(null);
-
- useEffect(() => {
- inputAreaRef.current?.focus();
- }, []);
-
- useEffect(() => {
- if (!codeId) return;
- const initial = (useChatStore.getState() as any).chats[codeId] ?? [];
- setContent(initial);
- // Subscribe to store changes without selector options
- const unsubscribe = useChatStore.subscribe((state: any) => {
- const newChat = state.chats[codeId] ?? [];
- setContent(newChat);
- });
- return () => unsubscribe();
- }, [codeId]);
-
- useEffect(() => {
- chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }, [content]);
-
- const handleSubmit = async () => {
- if (!currentInput.trim() || !codeId) return;
- const userMessage = currentInput;
- setCurrentInput("");
-
- const newMessage = {
- sender: "me",
- time: new Date().toLocaleString(),
- message: userMessage,
- };
- addMessage(codeId, newMessage);
- setContent((prev) => [...prev, newMessage]);
-
- const AIMessage = {
- sender: "AI",
- time: new Date().toLocaleString(),
- message: "💬 AI가 입력중입니다...",
- loading: true,
- };
- addMessage(codeId, AIMessage);
- setContent((prev) => [...prev, AIMessage]);
-
- try {
- const response = await chatBotRequest(userMessage);
- const { code } = response.status;
- const AIResponse =
- code === 200
- ? {
- sender: "AI",
- time: new Date().toLocaleString(),
- message: response.content.response,
- }
- : {
- sender: "AI",
- time: new Date().toLocaleString(),
- message: "다시 한번 물어봐 주세요!!",
- };
- replaceLastMessage(codeId, AIResponse);
- setContent((prev) => [...prev.slice(0, -1), AIResponse]);
- } catch {
- const AIResponse = {
- sender: "AI",
- time: new Date().toLocaleString(),
- message: "다시 한번 물어봐 주세요!!",
- };
- replaceLastMessage(codeId, AIResponse);
- setContent((prev) => [...prev.slice(0, -1), AIResponse]);
- }
- };
-
- const root = css({
- width: "100%",
- height: "90%",
- display: "flex",
- flexDirection: "column",
- });
- const list = css({ width: "100%", height: "90%", overflowY: "auto" });
- const inputRow = css({
- width: "100%",
- height: "10%",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- position: "relative",
- });
- const inputClass = css({
- borderWidth: "2px",
- width: "100%",
- height: "90%",
- borderRadius: "0.75rem",
- position: "relative",
- px: "0.5rem",
- });
- const sendBtn = css({
- position: "absolute",
- right: "1.5rem",
- bottom: "0.125rem",
- transform: "translateY(-50%)",
- backgroundColor: "#3b82f6",
- color: "white",
- px: "0.75rem",
- py: "0.25rem",
- borderRadius: "0.5rem",
- fontSize: "0.875rem",
- transition: "all 150ms",
- _hover: { backgroundColor: "#2563eb" },
- _active: { transform: "translateY(-50%) scale(0.95)" },
- });
-
- return (
-
-
- {content.map((item, idx) => (
-
-
-
- {item.message}
-
-
- {item.time}
-
-
-
- ))}
-
-
- setCurrentInput(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") handleSubmit();
- }}
- />
-
- 전송
-
-
-
- );
-};
-
-export default ChatBox;
diff --git a/src/components/single/CodeDescription.tsx b/src/components/single/CodeDescription.tsx
deleted file mode 100644
index e259006..0000000
--- a/src/components/single/CodeDescription.tsx
+++ /dev/null
@@ -1,290 +0,0 @@
-import hljs from "highlight.js/lib/core";
-import javascript from "highlight.js/lib/languages/javascript";
-import java from "highlight.js/lib/languages/java";
-import python from "highlight.js/lib/languages/python";
-import sql from "highlight.js/lib/languages/sql";
-import "highlight.js/styles/atom-one-dark.css";
-import { codeDescription } from "../../api/singleApi";
-import { useState, useEffect, useMemo } from "react";
-import ReactMarkdown from "react-markdown";
-import chatBtn from "../../assets/images/chat_bot.png";
-import AIChat from "./AIChatModal";
-import copyIcon from "../../assets/images/copy_icon.png";
-import QnA1Img from "../../assets/images/QnA1.png";
-import QnA2Img from "../../assets/images/QnA2.png";
-import QnA3Img from "../../assets/images/QnA3.png";
-import { css } from "../../../styled-system/css";
-
-hljs.registerLanguage("java", java);
-hljs.registerLanguage("python", python);
-hljs.registerLanguage("javascript", javascript);
-hljs.registerLanguage("sql", sql);
-
-interface CodeDescriptionProps {
- onClose: () => void;
- lang: string;
- codeId: number;
-}
-
-const CodeDescription: React.FC = ({
- onClose,
- lang,
- codeId,
-}) => {
- const [code, setCode] = useState("");
- const [description, setDescription] = useState("");
- const [isAIChat, setIsAIChat] = useState(false);
- const [copied, setCopied] = useState(false);
- const [qnAcurrnetIndex, setQnACurrentIndex] = useState(0);
- const QnABtns = [QnA1Img, QnA2Img, QnA3Img];
-
- useEffect(() => {
- if (codeId) getCodeDescription();
- }, [codeId]);
-
- const getCodeDescription = async () => {
- try {
- const response = await codeDescription(codeId);
- const { code: statusCode } = response.status;
- if (statusCode === 200) {
- setCode(response.content.annotation);
- setDescription(response.content.descript);
- }
- } catch {}
- };
-
- const handleCopyDescrip = (num: number) => {
- const text = num === 1 ? code : description;
- navigator.clipboard
- .writeText(text)
- .then(() => {
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
- })
- .catch(() => {});
- };
-
- useEffect(() => {
- const QnAInterval = setInterval(
- () => setQnACurrentIndex((prev) => (prev + 1) % 3),
- 800
- );
- return () => clearInterval(QnAInterval);
- }, []);
-
- const langFormat = useMemo(() => {
- if (lang === "JAVA") return "language-java";
- if (lang === "PYTHON") return "language-python";
- if (lang === "JS") return "language-javascript";
- if (lang === "SQL") return "language-sql";
- return "";
- }, [lang]);
-
- useEffect(() => {
- const codeBlocks = document.querySelectorAll("pre code.hljs");
- codeBlocks.forEach((block) => {
- block.removeAttribute("data-highlighted");
- });
- hljs.highlightAll();
- }, [langFormat, code]);
-
- const wrapper = css({
- width: "90%",
- height: "90%",
- zIndex: 9999,
- borderRadius: "0.75rem",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- borderWidth: "2px",
- backgroundColor: "#111111",
- borderColor: "rgba(255, 255, 255, 0.1)",
- position: "relative",
- });
- const closeBtn = css({
- position: "absolute",
- right: 0,
- top: "0.5rem",
- width: "5%",
- height: "5%",
- fontSize: "3rem",
- color: "#9ca3af",
- cursor: "pointer",
- transition: "all 150ms",
- _hover: {
- filter: "brightness(1.1)",
- transform: "translateY(2px) scale(0.98)",
- },
- _active: { transform: "scale(0.95)" },
- });
- const inner = css({
- width: "99%",
- height: "98%",
- borderRadius: "0.75rem",
- display: "flex",
- padding: "0.5rem",
- alignItems: "center",
- justifyContent: "space-between",
- position: "relative",
- });
- const leftPane = css({
- width: "50%",
- height: "100%",
- borderWidth: "2px",
- borderRadius: "0.75rem",
- position: "relative",
- borderColor: "rgba(255,255,255,0.1)",
- backgroundColor: "#282c34",
- });
- const rightPane = css({
- width: "50%",
- height: "100%",
- borderWidth: "2px",
- borderRadius: "0.75rem",
- color: "white",
- p: "1rem",
- position: "relative",
- borderColor: "rgba(255,255,255,0.1)",
- backgroundColor: "#1C1C1C",
- });
- const qnaBtn = css({
- position: "fixed",
- right: "2rem",
- bottom: "2rem",
- width: "4rem",
- height: "4rem",
- borderRadius: "9999px",
- backgroundImage:
- "linear-gradient(to bottom right, #6366f1, #a855f7, #22d3ee)",
- boxShadow: "0 10px 20px rgba(99,102,241,0.5)",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- transition: "all 300ms",
- _hover: { filter: "brightness(1.1)", transform: "scale(1.05)" },
- _active: { transform: "scale(0.95)" },
- });
-
- return (
-
-
- x
-
-
-
-
handleCopyDescrip(1)}
- />
-
-
- {code}
-
-
-
-
-
-
handleCopyDescrip(2)}
- />
-
{description}
-
-
-
-
- setIsAIChat(true)}
- />
-
-
- {copied && (
-
- 복사완료
-
- )}
-
setIsAIChat(false)}
- codeId={codeId}
- />
-
- );
-};
-
-export default CodeDescription;
diff --git a/src/components/single/ProgressBox.tsx b/src/components/single/ProgressBox.tsx
deleted file mode 100644
index 6db01d6..0000000
--- a/src/components/single/ProgressBox.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-import endBtn from "../../assets/images/end_game_button.png";
-import { useNavigate } from "react-router-dom";
-import { formatTime } from "../../utils/formatTimeUtils";
-import React from "react";
-
-interface ProgressBoxProps {
- elapsedTime: number;
- cpm: number;
- progress: number;
- strokes?: number;
- characterImg?: string;
-}
-
-// Static character image - never re-renders
-const CharacterImage = React.memo<{ characterImg?: string }>(
- ({ characterImg }) => {
- if (!characterImg) return null;
-
- return (
-
-
-
- );
- }
-);
-
-CharacterImage.displayName = "CharacterImage";
-
-// Static game end button - never re-renders
-const GameEndButton = React.memo(() => {
- const navigate = useNavigate();
-
- return (
-
-
{
- e.currentTarget.style.filter = "brightness(1.2)";
- e.currentTarget.style.transform = "translateY(-2px)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.filter = "brightness(1)";
- e.currentTarget.style.transform = "translateY(0)";
- }}
- onMouseDown={(e) => {
- e.currentTarget.style.transform = "scale(0.95) translateY(0px)";
- }}
- onMouseUp={(e) => {
- e.currentTarget.style.transform = "scale(1) translateY(0)";
- }}
- onClick={() => navigate("/single/select/language")}
- />
-
- );
-});
-
-GameEndButton.displayName = "GameEndButton";
-
-// Dynamic timer component - updates every time (no memo to ensure updates)
-const Timer: React.FC<{ elapsedTime: number }> = ({ elapsedTime }) => {
- return (
-
- ⏱
- {formatTime(elapsedTime)}
-
- );
-};
-
-// Dynamic typing count - only updates when strokes/cpm changes
-const TypingCount = React.memo<{ strokes?: number; cpm: number }>(
- ({ strokes, cpm }) => {
- return (
-
- 타수: {strokes ?? Math.floor(cpm)}
-
- );
- }
-);
-
-TypingCount.displayName = "TypingCount";
-
-// Dynamic progress display - only updates when progress changes
-const ProgressDisplay = React.memo<{ progress: number }>(({ progress }) => {
- return (
- <>
- {/* Progress Text */}
-
- 진행률: {progress}%
-
-
- {/* Progress Bar */}
-
- >
- );
-});
-
-ProgressDisplay.displayName = "ProgressDisplay";
-
-// Container component - no memo to ensure timer updates work
-const ProgressBox: React.FC = ({
- elapsedTime,
- cpm,
- progress,
- strokes,
- characterImg,
-}) => {
- return (
-
- );
-};
-
-export default ProgressBox;
diff --git a/src/components/single/SingleTypingBox.tsx b/src/components/single/SingleTypingBox.tsx
deleted file mode 100644
index ca7e395..0000000
--- a/src/components/single/SingleTypingBox.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-// Placeholder TSX version kept commented logic for future reuse; not wired yet.
-export {};
diff --git a/src/components/single/StopButton.tsx b/src/components/single/StopButton.tsx
deleted file mode 100644
index 8bedcbf..0000000
--- a/src/components/single/StopButton.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useNavigate } from "react-router-dom";
-import headerBtn from "../../assets/images/single_stop_btn.png";
-import { css } from "../../../styled-system/css";
-
-const StopButton: React.FC = () => {
- const navigate = useNavigate();
- return (
-
-
navigate("/single/select/language")}
- />
-
- );
-};
-
-export default StopButton;
diff --git a/src/components/ui/CheckboxToggle.tsx b/src/components/ui/CheckboxToggle.tsx
deleted file mode 100644
index eaf9409..0000000
--- a/src/components/ui/CheckboxToggle.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from "react";
-import { CheckboxRoot, CheckboxIndicator } from "@ark-ui/react/checkbox";
-
-interface CheckboxToggleProps {
- checked: boolean;
- onCheckedChange: (checked: boolean) => void;
- label?: string;
- size?: "sm" | "md" | "lg";
- disabled?: boolean;
- indeterminate?: boolean;
- className?: string;
-}
-
-const CheckboxToggle: React.FC = ({
- checked,
- onCheckedChange,
- label,
- size = "md",
- disabled = false,
- indeterminate = false,
- className = "",
-}) => {
- const sizeStyles = {
- sm: {
- width: "16px",
- height: "16px",
- fontSize: "12px",
- },
- md: {
- width: "20px",
- height: "20px",
- fontSize: "14px",
- },
- lg: {
- width: "24px",
- height: "24px",
- fontSize: "16px",
- },
- };
-
- return (
-
-
onCheckedChange(details.checked)}
- disabled={disabled}
- style={{
- width: sizeStyles[size].width,
- height: sizeStyles[size].height,
- backgroundColor: checked || indeterminate ? "#ec4899" : "#ffffff",
- border: "2px solid",
- borderColor: checked || indeterminate ? "#ec4899" : "#6b7280",
- borderRadius: "0.25rem",
- position: "relative",
- cursor: disabled ? "not-allowed" : "pointer",
- opacity: disabled ? 0.5 : 1,
- transition: "all 0.2s ease-in-out",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- }}
- >
-
- {indeterminate ? (
-
- ) : (
-
- )}
-
-
- {label && (
-
- {label}
-
- )}
-
- );
-};
-
-export default CheckboxToggle;
diff --git a/src/components/ui/SearchBar.tsx b/src/components/ui/SearchBar.tsx
deleted file mode 100644
index d41ef44..0000000
--- a/src/components/ui/SearchBar.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import React, {
- useCallback,
- useRef,
- useState,
- useEffect,
- useLayoutEffect,
-} from "react";
-
-interface SearchBarProps {
- placeholder?: string;
-}
-
-// 모듈 레벨에서 SearchBar의 값을 저장하여 리렌더링에도 유지
-let savedSearchValue = "";
-
-const SearchBar: React.FC = ({ placeholder = "Search" }) => {
- const inputRef = useRef(null);
- const [localValue, setLocalValue] = useState(savedSearchValue);
- const [showClearButton, setShowClearButton] = useState(
- savedSearchValue.length > 0
- );
-
- // 컴포넌트가 마운트될 때 저장된 값을 복원
- useLayoutEffect(() => {
- if (inputRef.current && savedSearchValue) {
- inputRef.current.value = savedSearchValue;
- setLocalValue(savedSearchValue);
- setShowClearButton(savedSearchValue.length > 0);
- }
- }, []);
-
- // input의 실제 값과 상태를 동기화하는 함수
- const syncValue = useCallback(() => {
- if (inputRef.current) {
- const currentValue = inputRef.current.value;
- savedSearchValue = currentValue;
- setLocalValue(currentValue);
- setShowClearButton(currentValue.length > 0);
- }
- }, []);
-
- const handleInputChange = useCallback(
- (e: React.ChangeEvent) => {
- const newValue = e.target.value;
- savedSearchValue = newValue;
- setLocalValue(newValue);
- setShowClearButton(newValue.length > 0);
- },
- []
- );
-
- const handleFocus = useCallback((e: React.FocusEvent) => {
- e.target.style.borderColor = "#ec4899";
- e.target.style.boxShadow = "0 0 0 3px rgba(236, 72, 153, 0.1)";
- }, []);
-
- const handleBlur = useCallback((e: React.FocusEvent) => {
- e.target.style.borderColor = "#d1d5db";
- e.target.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
- }, []);
-
- const handleClear = useCallback(() => {
- if (inputRef.current) {
- inputRef.current.value = "";
- savedSearchValue = "";
- setLocalValue("");
- setShowClearButton(false);
- inputRef.current.focus();
- }
- }, []);
-
- return (
-
-
- 🔍
-
-
- {showClearButton && (
-
{
- e.currentTarget.style.backgroundColor = "#e5e7eb";
- e.currentTarget.style.color = "#374151";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "#f3f4f6";
- e.currentTarget.style.color = "#6b7280";
- }}
- >
- ×
-
- )}
-
- );
-};
-
-SearchBar.displayName = "SearchBar";
-
-export default SearchBar;
diff --git a/src/components/ui/SwitchToggle.tsx b/src/components/ui/SwitchToggle.tsx
deleted file mode 100644
index 820a39c..0000000
--- a/src/components/ui/SwitchToggle.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from "react";
-import { SwitchRoot, SwitchThumb, SwitchControl } from "@ark-ui/react";
-
-interface SwitchToggleProps {
- checked: boolean;
- onCheckedChange: (checked: boolean) => void;
- label?: string;
- size?: "sm" | "md" | "lg";
- disabled?: boolean;
- className?: string;
-}
-
-const SwitchToggle: React.FC = ({
- checked,
- onCheckedChange,
- label,
- size = "md",
- disabled = false,
- className = "",
-}) => {
- const sizeStyles = {
- sm: {
- width: "32px",
- height: "18px",
- thumbSize: "14px",
- },
- md: {
- width: "40px",
- height: "22px",
- thumbSize: "18px",
- },
- lg: {
- width: "48px",
- height: "26px",
- thumbSize: "22px",
- },
- };
-
- return (
-
- onCheckedChange(details.checked)}
- disabled={disabled}
- style={{
- width: sizeStyles[size].width,
- height: sizeStyles[size].height,
- backgroundColor: checked ? "#ec4899" : "#6b7280",
- borderRadius: "9999px",
- position: "relative",
- cursor: disabled ? "not-allowed" : "pointer",
- opacity: disabled ? 0.5 : 1,
- transition: "all 0.2s ease-in-out",
- }}
- >
-
-
-
-
- {label && (
-
- {label}
-
- )}
-
- );
-};
-
-export default SwitchToggle;
diff --git a/src/components/ui/ToggleGroup.tsx b/src/components/ui/ToggleGroup.tsx
deleted file mode 100644
index 93587cc..0000000
--- a/src/components/ui/ToggleGroup.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from "react";
-import { ToggleGroupRoot, ToggleGroupItem } from "@ark-ui/react";
-
-interface ToggleGroupOption {
- value: string;
- label: string;
-}
-
-interface ToggleGroupProps {
- options: ToggleGroupOption[];
- value: string[];
- onValueChange: (value: string[]) => void;
- multiple?: boolean;
- size?: "sm" | "md" | "lg";
- variant?: "default" | "outline" | "ghost";
- className?: string;
-}
-
-const ToggleGroupComponent: React.FC = ({
- options,
- value,
- onValueChange,
- multiple = false,
- size = "md",
- variant = "default",
- className = "",
-}) => {
- const sizeStyles = {
- sm: {
- padding: "0.25rem 0.5rem",
- fontSize: "0.75rem",
- },
- md: {
- padding: "0.375rem 0.75rem",
- fontSize: "0.875rem",
- },
- lg: {
- padding: "0.5rem 1rem",
- fontSize: "1rem",
- },
- };
-
- const getVariantStyles = (variant: string, isActive: boolean) => {
- switch (variant) {
- case "default":
- return {
- backgroundColor: isActive ? "#ec4899" : "#374151",
- color: "#ffffff",
- border: "1px solid #6b7280",
- };
- case "outline":
- return {
- backgroundColor: isActive ? "#ec4899" : "transparent",
- color: isActive ? "#ffffff" : "#374151",
- border: "1px solid #6b7280",
- };
- case "ghost":
- return {
- backgroundColor: isActive ? "#ec4899" : "transparent",
- color: isActive ? "#ffffff" : "#6b7280",
- border: "none",
- };
- default:
- return {
- backgroundColor: isActive ? "#ec4899" : "#374151",
- color: "#ffffff",
- border: "1px solid #6b7280",
- };
- }
- };
-
- return (
- onValueChange(details.value)}
- multiple={multiple}
- className={className}
- style={{
- display: "flex",
- gap: "0.5rem",
- flexWrap: "wrap",
- }}
- >
- {options.map((option) => {
- const isActive = value.includes(option.value);
- return (
-
- {option.label}
-
- );
- })}
-
- );
-};
-
-export default ToggleGroupComponent;
diff --git a/src/features/auth/graphql/mutations.ts b/src/features/auth/graphql/mutations.ts
deleted file mode 100644
index c178700..0000000
--- a/src/features/auth/graphql/mutations.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const REGISTER = gql`
- mutation Register($input: RegisterInput!) {
- register(input: $input) {
- user {
- id
- name
- email
- }
- tokens {
- accessToken
- refreshToken
- }
- isNewUser
- }
- }
-`;
-
-export const LOGIN = gql`
- mutation Login($input: LoginInput!) {
- login(input: $input) {
- user {
- id
- name
- email
- }
- tokens {
- accessToken
- refreshToken
- }
- isNewUser
- }
- }
-`;
-
-export const LOGIN_WITH_GOOGLE = gql`
- mutation LoginWithGoogle($idToken: String!) {
- loginWithGoogle(idToken: $idToken) {
- user {
- id
- name
- email
- }
- tokens {
- accessToken
- refreshToken
- }
- isNewUser
- }
- }
-`;
-
-export const REFRESH_TOKEN = gql`
- mutation RefreshToken($refreshToken: String!) {
- refreshToken(refreshToken: $refreshToken) {
- accessToken
- refreshToken
- }
- }
-`;
diff --git a/src/features/auth/graphql/queries.ts b/src/features/auth/graphql/queries.ts
deleted file mode 100644
index a77cb3a..0000000
--- a/src/features/auth/graphql/queries.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const ME = gql`
- query Me {
- me {
- id
- name
- email
- avatar
- followingCount
- followerCount
- }
- }
-`;
diff --git a/src/features/payment/components/PaymentErrorDialog.test.tsx b/src/features/payment/components/PaymentErrorDialog.test.tsx
deleted file mode 100644
index 6860ce8..0000000
--- a/src/features/payment/components/PaymentErrorDialog.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { vi, describe, it, expect } from "vitest";
-import { PaymentErrorDialog } from "@/features/payment/components/PaymentErrorDialog";
-
-describe("PaymentErrorDialog", () => {
- it("renders error message and closes on click", async () => {
- const handleClose = vi.fn();
- render(
-
- );
-
- expect(screen.getByText("결제 실패")).toBeInTheDocument();
- expect(screen.getByText(/카드가 승인되지 않았습니다/)).toBeInTheDocument();
- const closeButton = screen.getByText("닫기");
- closeButton.click();
- expect(handleClose).toHaveBeenCalled();
- });
-});
diff --git a/src/features/payment/components/PaymentErrorDialog.tsx b/src/features/payment/components/PaymentErrorDialog.tsx
deleted file mode 100644
index a22b0da..0000000
--- a/src/features/payment/components/PaymentErrorDialog.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Dialog } from "@ark-ui/react";
-import { css } from "styled-system/css";
-import type { PaymentError } from "@/features/payment/hooks/usePayment";
-
-interface Props {
- error: PaymentError | null;
- onClose: () => void;
- onRetry?: () => void;
-}
-
-export function PaymentErrorDialog({ error, onClose, onRetry }: Props) {
- if (!error) return null;
-
- return (
-
-
-
-
-
- 결제 실패
-
-
- {error.message}
-
- {error.code && (
-
- 오류 코드: {error.code}
-
- )}
-
- {onRetry && (
- {
- onClose();
- onRetry();
- }}
- className={css({
- padding: "8px 16px",
- backgroundColor: "blue.500",
- color: "white",
- borderRadius: "md",
- fontWeight: "medium",
- cursor: "pointer",
- _hover: { backgroundColor: "blue.600" },
- })}
- >
- 다시 시도
-
- )}
-
- 닫기
-
-
-
-
-
- );
-}
diff --git a/src/features/payment/graphql/mutations.ts b/src/features/payment/graphql/mutations.ts
deleted file mode 100644
index 94679b4..0000000
--- a/src/features/payment/graphql/mutations.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const PREPARE_PAYMENT = gql`
- mutation PreparePayment($productId: ID!) {
- preparePayment(productId: $productId) {
- merchantUid
- amount
- productName
- currency
- customerEmail
- customerName
- }
- }
-`;
-
-export const VERIFY_PAYMENT = gql`
- mutation VerifyPayment($input: VerifyPaymentInput!) {
- verifyPayment(input: $input) {
- paymentId
- status
- currency
- message
- wallet {
- balance
- }
- }
- }
-`;
diff --git a/src/features/payment/hooks/usePayment.ts b/src/features/payment/hooks/usePayment.ts
deleted file mode 100644
index ac7b5cf..0000000
--- a/src/features/payment/hooks/usePayment.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import { useMutation } from "@apollo/client/react";
-import { useEffect, useState } from "react";
-import {
- PREPARE_PAYMENT,
- VERIFY_PAYMENT,
-} from "@/features/payment/graphql/mutations";
-import { ensurePortOneLoaded, requestPay } from "@/lib/portone";
-
-export interface PaymentError {
- code: string;
- message: string;
- details?: any;
-}
-
-export function usePayment() {
- const [error, setError] = useState(null);
- const [isProcessing, setIsProcessing] = useState(false);
- const [preparePayment] = useMutation(PREPARE_PAYMENT);
- const [verifyPayment] = useMutation(VERIFY_PAYMENT);
-
- useEffect(() => {
- try {
- ensurePortOneLoaded();
- } catch (error: unknown) {
- console.error("Failed to load PortOne SDK:", error);
- setError({
- code: "SDK_LOAD_ERROR",
- message: "결제 시스템을 불러오는데 실패했습니다.",
- details: error,
- });
- }
- }, []);
-
- const requestPayment = async (productId: string) => {
- setError(null);
- setIsProcessing(true);
-
- try {
- // Step 1: Prepare payment on backend
- console.log("Preparing payment for product:", productId);
- const { data } = await preparePayment({ variables: { productId } });
- const prepared = (data as any)?.preparePayment;
-
- if (!prepared) {
- throw new Error("Failed to prepare payment");
- }
-
- console.log("Payment prepared:", prepared);
-
- // Step 2: Request payment through PortOne
- const payResult = (await Promise.race([
- requestPay({
- pg: "html5_inicis",
- pay_method: "card",
- merchant_uid: prepared.merchantUid,
- name: prepared.productName,
- amount: prepared.amount,
- buyer_email: prepared.customerEmail,
- buyer_name: prepared.customerName,
- buyer_tel: prepared.customerPhone || "010-0000-0000",
- buyer_addr: prepared.customerAddress || "서울시 강남구",
- buyer_postcode: prepared.customerPostcode || "12345",
- m_redirect_url: `${window.location.origin}/store/payment-callback`,
- language: "ko",
- currency: "KRW",
- }),
- new Promise((_, reject) =>
- setTimeout(
- () => reject({ code: "TIMEOUT", message: "Payment timeout" }),
- 2000
- )
- ),
- ] as const)) as any;
-
- console.log("PortOne payment result:", payResult);
-
- // Step 3: Verify payment on backend (deferred to improve UX and test stability)
- // For E2E and optimistic UX, resolve immediately after PortOne success
- // and perform verification asynchronously without blocking navigation.
- (async () => {
- try {
- const { data: verify } = await verifyPayment({
- variables: {
- input: {
- impUid: payResult.imp_uid,
- merchantUid: payResult.merchant_uid,
- amount: prepared.amount,
- },
- },
- });
- console.log("Payment verification result:", verify);
- } catch (e) {
- console.warn("Background verification failed:", e);
- }
- })();
-
- setIsProcessing(false);
- return {
- success: true,
- transactionId: payResult.imp_uid,
- merchantUid: payResult.merchant_uid,
- amount: prepared.amount,
- productName: prepared.productName,
- };
- } catch (err: any) {
- console.error("Payment error:", err);
- setIsProcessing(false);
-
- // Enhanced error handling with more specific error codes
- const errorMap: Record = {
- // PortOne specific errors
- F400: "결제 금액이 일치하지 않습니다.",
- F401: "인증되지 않은 결제입니다.",
- F500: "결제 처리 중 오류가 발생했습니다.",
-
- // Card errors
- CARD_DECLINED: "카드가 승인되지 않았습니다.",
- INSUFFICIENT_FUNDS: "잔액이 부족합니다.",
- EXPIRED_CARD: "카드 유효기간이 만료되었습니다.",
- INVALID_CARD: "유효하지 않은 카드입니다.",
-
- // Network errors
- TIMEOUT: "결제 시간이 초과되었습니다.",
- NETWORK_ERROR: "네트워크 연결에 문제가 있습니다.",
-
- // User cancellation
- USER_CANCEL: "사용자가 결제를 취소했습니다.",
-
- // System errors
- SERVER_ERROR: "서버 오류가 발생했습니다.",
- UNKNOWN_ERROR: "알 수 없는 오류가 발생했습니다.",
- };
-
- // Determine error code from various sources
- let errorCode = err?.code || err?.error_code || "UNKNOWN_ERROR";
- let errorMessage =
- err?.message ||
- err?.error_msg ||
- errorMap[errorCode] ||
- "결제에 실패했습니다.";
-
- // Handle specific PortOne error cases
- if (err?.code === "USER_CANCEL" || err?.error_code === "USER_CANCEL") {
- errorCode = "USER_CANCEL";
- errorMessage = "사용자가 결제를 취소했습니다.";
- }
-
- // Handle network timeouts
- if (err?.name === "TimeoutError" || err?.message?.includes("timeout")) {
- errorCode = "TIMEOUT";
- errorMessage = "결제 시간이 초과되었습니다.";
- }
-
- // Handle network errors
- if (err?.name === "NetworkError" || err?.message?.includes("network")) {
- errorCode = "NETWORK_ERROR";
- errorMessage = "네트워크 연결에 문제가 있습니다.";
- }
-
- const paymentError: PaymentError = {
- code: errorCode,
- message: errorMessage,
- details: {
- originalError: err,
- timestamp: new Date().toISOString(),
- productId,
- },
- };
-
- setError(paymentError);
- throw paymentError;
- }
- };
-
- const clearError = () => setError(null);
-
- return { requestPayment, error, isProcessing, clearError };
-}
diff --git a/src/features/products/graphql/queries.ts b/src/features/products/graphql/queries.ts
deleted file mode 100644
index 00a9b24..0000000
--- a/src/features/products/graphql/queries.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const GET_PRODUCTS = gql`
- query GetProducts($page: Int, $limit: Int) {
- products(page: $page, limit: $limit) {
- items {
- id
- name
- description
- price
- bonus
- amount
- currency
- }
- pageInfo {
- page
- limit
- total
- totalPages
- }
- }
- }
-`;
-
-export const GET_PRODUCT = gql`
- query GetProduct($id: ID!) {
- product(id: $id) {
- id
- name
- description
- price
- bonus
- amount
- currency
- }
- }
-`;
diff --git a/src/features/ranking/graphql/queries.ts b/src/features/ranking/graphql/queries.ts
deleted file mode 100644
index e11c548..0000000
--- a/src/features/ranking/graphql/queries.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const GET_RANKINGS = gql`
- query GetRankings($game: String!, $period: String!, $limit: Int) {
- rankings(game: $game, period: $period, limit: $limit) {
- items {
- userId
- nickname
- score
- typingSpeed
- rank
- }
- myRank {
- rank
- score
- typingSpeed
- }
- }
- }
-`;
diff --git a/src/features/user/graphql/follow-operations.ts b/src/features/user/graphql/follow-operations.ts
deleted file mode 100644
index 63b5fef..0000000
--- a/src/features/user/graphql/follow-operations.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { gql } from "@apollo/client";
-
-// Follower/Following Queries
-export const GET_FOLLOWERS = gql`
- query GetFollowers($page: Int, $limit: Int, $search: String) {
- followers(page: $page, limit: $limit, search: $search) {
- items {
- id
- name
- email
- avatar
- followedAt
- isFollowing
- }
- pageInfo {
- page
- limit
- total
- totalPages
- hasNextPage
- hasPreviousPage
- }
- }
- }
-`;
-
-export const GET_FOLLOWING = gql`
- query GetFollowing($page: Int, $limit: Int, $search: String) {
- following(page: $page, limit: $limit, search: $search) {
- items {
- id
- name
- email
- avatar
- followedAt
- isFollowing
- }
- pageInfo {
- page
- limit
- total
- totalPages
- hasNextPage
- hasPreviousPage
- }
- }
- }
-`;
-
-export const SEARCH_USERS = gql`
- query SearchUsers($query: String!, $page: Int, $limit: Int) {
- searchUsers(query: $query, page: $page, limit: $limit) {
- items {
- id
- name
- email
- avatar
- isFollowing
- }
- pageInfo {
- page
- limit
- total
- totalPages
- }
- }
- }
-`;
-
-// Follow/Unfollow Mutations
-export const FOLLOW_USER = gql`
- mutation FollowUser($userId: ID!) {
- followUser(userId: $userId) {
- success
- message
- user {
- id
- isFollowing
- followerCount
- }
- }
- }
-`;
-
-export const UNFOLLOW_USER = gql`
- mutation UnfollowUser($userId: ID!) {
- unfollowUser(userId: $userId) {
- success
- message
- user {
- id
- isFollowing
- followingCount
- }
- }
- }
-`;
diff --git a/src/features/user/graphql/mutations.ts b/src/features/user/graphql/mutations.ts
deleted file mode 100644
index 574ebcb..0000000
--- a/src/features/user/graphql/mutations.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const UPDATE_PROFILE = gql`
- mutation UpdateProfile($input: UpdateProfileInput!) {
- updateProfile(input: $input) {
- id
- name
- bio
- avatar
- updatedAt
- }
- }
-`;
-
-export const DELETE_ACCOUNT = gql`
- mutation DeleteAccount($password: String!, $reason: String) {
- deleteAccount(password: $password, reason: $reason) {
- success
- message
- }
- }
-`;
-
-export const FOLLOW_USER = gql`
- mutation FollowUser($userId: String!) {
- followUser(userId: $userId) {
- success
- followersCount
- }
- }
-`;
-
-export const UNFOLLOW_USER = gql`
- mutation UnfollowUser($userId: String!) {
- unfollowUser(userId: $userId) {
- success
- followersCount
- }
- }
-`;
-
-export const WITHDRAW_ACCOUNT = gql`
- mutation WithdrawAccount {
- withdrawAccount {
- success
- }
- }
-`;
diff --git a/src/features/user/graphql/queries.ts b/src/features/user/graphql/queries.ts
deleted file mode 100644
index 2c875c2..0000000
--- a/src/features/user/graphql/queries.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const GET_USER_PROFILE = gql`
- query GetUserProfile {
- me {
- id
- nickname
- email
- profileImage
- followersCount
- followingCount
- connectedAccounts {
- google
- kakao
- }
- topRecords {
- java
- js
- python
- sql
- go
- }
- totalScore
- wallet {
- balance
- currency
- }
- }
- }
-`;
-
-export const GET_USER_BY_ID = gql`
- query GetUserById($id: ID!) {
- user(id: $id) {
- id
- name
- email
- avatar
- bio
- followingCount
- followerCount
- wallet {
- balance
- currency
- }
- }
- }
-`;
-
-export const GET_MONTHLY_RANKINGS = gql`
- query GetMonthlyRankings {
- monthlyRankings {
- java
- js
- python
- sql
- go
- }
- }
-`;
-
-export const GET_PERFORMANCE_DATA = gql`
- query GetPerformanceData($language: String!, $timePeriod: String!) {
- performanceData(language: $language, timePeriod: $timePeriod) {
- month
- userScore
- comparisonScore
- }
- }
-`;
diff --git a/src/features/user/mock-data.ts b/src/features/user/mock-data.ts
deleted file mode 100644
index 6143193..0000000
--- a/src/features/user/mock-data.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-// Mock data for followers and following pages
-export const mockFollowers = [
- {
- id: "1",
- name: "Esthera Jackson",
- email: "esthera@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: false,
- followedAt: "2025-01-15T10:30:00Z",
- },
- {
- id: "2",
- name: "Alexa Liras",
- email: "alexa@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-14T08:20:00Z",
- },
- {
- id: "3",
- name: "Laurent Michael",
- email: "laurent@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: false,
- followedAt: "2025-01-13T15:45:00Z",
- },
- {
- id: "4",
- name: "Freduardo Hill",
- email: "freduardo@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-12T12:10:00Z",
- },
- {
- id: "5",
- name: "Daniel Thomas",
- email: "daniel@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: false,
- followedAt: "2025-01-11T09:25:00Z",
- },
- {
- id: "6",
- name: "Mark Wilson",
- email: "mark@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-10T14:50:00Z",
- },
-];
-
-export const mockFollowing = [
- {
- id: "7",
- name: "Esthera Jackson",
- email: "esthera@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-09T11:15:00Z",
- },
- {
- id: "8",
- name: "Alexa Liras",
- email: "alexa@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-08T16:40:00Z",
- },
- {
- id: "9",
- name: "Laurent Michael",
- email: "laurent@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-07T13:20:00Z",
- },
- {
- id: "10",
- name: "Freduardo Hill",
- email: "freduardo@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-06T10:05:00Z",
- },
- {
- id: "11",
- name: "Daniel Thomas",
- email: "daniel@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-05T08:30:00Z",
- },
- {
- id: "12",
- name: "Mark Wilson",
- email: "mark@simmmple.com",
- avatar: "/default-avatar.png",
- isFollowing: true,
- followedAt: "2025-01-04T14:55:00Z",
- },
-];
diff --git a/src/features/user/types.ts b/src/features/user/types.ts
deleted file mode 100644
index ab21a6d..0000000
--- a/src/features/user/types.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-export interface UserProfile {
- id: string;
- nickname: string;
- email: string;
- profileImage?: string;
- followersCount: number;
- followingCount: number;
- connectedAccounts: {
- google: boolean;
- kakao: boolean;
- };
- topRecords: {
- java: number | null;
- js: number | null;
- python: number | null;
- sql: number | null;
- go: number | null;
- };
- monthlyRankings: {
- java: number | null;
- js: number | null;
- python: number | null;
- sql: number | null;
- go: number | null;
- };
- totalScore: number;
- wallet: {
- balance: number;
- currency: string;
- };
-}
-
-export interface PerformanceData {
- month: string;
- userScore: number;
- comparisonScore?: number;
-}
-
-export interface GraphFilter {
- language: string;
- timePeriod: "daily" | "weekly" | "annually";
- comparisonUser?: string;
-}
-
-export interface LanguageOption {
- value: string;
- label: string;
-}
-
-export interface TimePeriodOption {
- value: "daily" | "weekly" | "annually";
- label: string;
-}
-
-export interface ConnectedBadgeProps {
- type: "google" | "kakao";
- label: string;
-}
-
-export interface PerformanceChartProps {
- data: PerformanceData[];
- language: string;
- timePeriod: string;
-}
diff --git a/src/features/user/types/follow-types.ts b/src/features/user/types/follow-types.ts
deleted file mode 100644
index 03dc18a..0000000
--- a/src/features/user/types/follow-types.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-// User Types for Follow/Following Features
-export interface FollowerUser {
- id: string;
- name: string;
- email: string;
- avatar: string;
- followedAt: string;
- isFollowing: boolean;
-}
-
-export interface FollowingUser {
- id: string;
- name: string;
- email: string;
- avatar: string;
- followedAt: string;
- isFollowing: boolean; // Always true for following page
-}
-
-export interface PageInfo {
- page: number;
- limit: number;
- total: number;
- totalPages: number;
- hasNextPage: boolean;
- hasPreviousPage: boolean;
-}
-
-export interface FollowersResponse {
- items: FollowerUser[];
- pageInfo: PageInfo;
-}
-
-export interface FollowingResponse {
- items: FollowingUser[];
- pageInfo: PageInfo;
-}
-
-export interface SearchUsersResponse {
- items: User[];
- pageInfo: PageInfo;
-}
-
-export interface User {
- id: string;
- name: string;
- email: string;
- avatar: string;
- isFollowing: boolean;
-}
-
-export interface FollowActionResponse {
- success: boolean;
- message: string;
- user: {
- id: string;
- isFollowing: boolean;
- followerCount?: number;
- followingCount?: number;
- };
-}
-
-// GraphQL query result types
-export interface GetFollowersData {
- followers: FollowersResponse;
-}
-
-export interface GetFollowingData {
- following: FollowingResponse;
-}
-
-export interface GetUserProfileData {
- me?: {
- id: string;
- nickname: string;
- email: string;
- profileImage?: string;
- followersCount: number;
- followingCount: number;
- wallet?: {
- balance: number;
- currency: string;
- };
- };
-}
diff --git a/src/features/wallet/graphql/queries.ts b/src/features/wallet/graphql/queries.ts
deleted file mode 100644
index 5bcfc93..0000000
--- a/src/features/wallet/graphql/queries.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const GET_WALLET_TRANSACTIONS = gql`
- query GetWalletTransactions(
- $page: Int
- $limit: Int
- $type: String
- $from: String
- $to: String
- ) {
- walletTransactions(
- page: $page
- limit: $limit
- type: $type
- from: $from
- to: $to
- ) {
- items {
- id
- type
- amount
- balanceAfter
- createdAt
- description
- }
- pageInfo {
- page
- limit
- total
- totalPages
- hasNextPage
- }
- }
- }
-`;
diff --git a/src/hooks/README.md b/src/hooks/README.md
deleted file mode 100644
index d205a95..0000000
--- a/src/hooks/README.md
+++ /dev/null
@@ -1 +0,0 @@
-### 커스텀 훅
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 5550a3e..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,159 +0,0 @@
-@font-face {
- font-family: "NeoDGM"; /* 원하는 글꼴 이름 */
- src: url("./assets/fonts/neodgm.ttf") format("truetype");
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: "D2Coding"; /* 원하는 글꼴 이름 */
- src: url("./assets/fonts/D2Coding.ttf") format("truetype");
- font-weight: normal;
- font-style: normal;
-}
-
-* {
- font-family: "NeoDGM", sans-serif; /* 전역적으로 'NeoDGM' 폰트를 사용 */
- margin: 0;
- padding: 0;
-}
-
-html,
-body {
- height: 100%;
- margin: 0;
- padding: 0;
- overflow: hidden; /* 요게 핵심 */
- /* cursor: url('/cursors/cl.png') 16 16, auto; */
-}
-
-/* img[role="button"]
-{
- cursor: url('/cursors/click.png') 16 16, pointer;
-}
-
-button:hover,
-a:hover,
-img[role="button"]:hover,
-input
-{
- cursor: url('/cursors/click.png') 16 16, pointer;
-} */
-
-/* custom-scrollbar 클래스용 스크롤바 스타일 */
-/* custom-scrollbar 개선된 버전 */
-.custom-scrollbar::-webkit-scrollbar {
- width: 6px; /* ✨ 더 얇게 */
- height: 4px;
-}
-
-.custom-scrollbar::-webkit-scrollbar-track {
- background: #1b103e; /* ✨ 모달 배경색이랑 완전 맞춤 */
-}
-
-.custom-scrollbar::-webkit-scrollbar-thumb {
- background-color: #51e2f5;
- border-radius: 10px;
- /* border 제거 (불필요) */
-}
-
-.custom-scrollbar::-webkit-scrollbar-thumb:hover {
- background-color: #7deafd;
-}
-
-pre {
- font-size: 17px; /* 조금만 키우기 */
- line-height: 1.6; /* 줄간격도 약간 */
-}
-
-@keyframes glow {
- 0% {
- text-shadow:
- 0 0 4px #fff,
- 0 0 10px #ffd700;
- }
- 50% {
- text-shadow:
- 0 0 6px #fff,
- 0 0 20px #ffd700;
- }
- 100% {
- text-shadow:
- 0 0 4px #fff,
- 0 0 10px #ffd700;
- }
-}
-
-.glow-effect {
- animation: glow 1.5s ease-in-out infinite;
-}
-
-@keyframes explode {
- 0% {
- transform: scale(1) translate(0, 0);
- opacity: 1;
- }
- 100% {
- transform: scale(1.5)
- translate(calc(50px - 100px * var(--x)), calc(50px - 100px * var(--y)));
- opacity: 0;
- }
-}
-
-@keyframes pop {
- 0% {
- transform: scale(0);
- opacity: 1;
- }
- 100% {
- transform: scale(1.5);
- opacity: 0;
- }
-}
-.firework {
- animation: pop 0.6s ease-out;
-}
-
-@keyframes sparkle {
- 0% {
- transform: scale(0.8);
- opacity: 0.9;
- box-shadow: 0 0 4px white;
- }
- 50% {
- transform: scale(1.3);
- opacity: 1;
- box-shadow: 0 0 12px white;
- }
- 100% {
- transform: scale(1);
- opacity: 0.7;
- box-shadow: 0 0 6px white;
- }
-}
-
-.sparkle {
- animation: sparkle 1.2s ease-in-out infinite;
-}
-
-@keyframes shake {
- 0% {
- transform: translateX(-5px);
- }
- 25% {
- transform: translateX(5px);
- }
- 50% {
- transform: translateX(-4px);
- }
- 75% {
- transform: translateX(4px);
- }
- 100% {
- transform: translateX(0);
- }
-}
-
-.shake {
- animation: shake 0.3s ease-in-out;
-}
diff --git a/src/lib/apollo-client.ts b/src/lib/apollo-client.ts
deleted file mode 100644
index 2a94828..0000000
--- a/src/lib/apollo-client.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
-import { setContext } from "@apollo/client/link/context";
-import { getRecaptchaToken } from "@/lib/recaptcha";
-
-const httpLink = createHttpLink({
- uri:
- (import.meta.env.VITE_BFF_GRAPHQL_URL as string | undefined) || "/graphql",
-});
-
-const authLink = setContext(async (_, { headers }) => {
- const token =
- typeof window !== "undefined"
- ? localStorage.getItem("access_token")
- : undefined;
- const recaptchaToken = await getRecaptchaToken("apollo_request");
- return {
- headers: {
- ...headers,
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- ...(recaptchaToken ? { "X-Recaptcha-Token": recaptchaToken } : {}),
- },
- } as { headers: Record };
-});
-
-export const apolloClient = new ApolloClient({
- link: authLink.concat(httpLink),
- cache: new InMemoryCache(),
-});
diff --git a/src/lib/portone.ts b/src/lib/portone.ts
deleted file mode 100644
index 4a5e8a6..0000000
--- a/src/lib/portone.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-declare global {
- interface Window {
- IMP?: any;
- }
-}
-
-// Ensure a minimal PortOne stub exists as early as module load for tests/e2e
-if (typeof window !== "undefined" && !window.IMP) {
- window.IMP = {
- init: () => true,
- request_pay: (_params: any, _cb: any) => {},
- };
-}
-
-export function ensurePortOneLoaded(): void {
- if (typeof window === "undefined") return;
- if (!window.IMP) {
- // Provide a lightweight stub immediately for tests/dev to detect SDK presence
- window.IMP = {
- init: () => true,
- request_pay: (_params: any, _cb: any) => {},
- };
- const script = document.createElement("script");
- script.src = "https://cdn.iamport.kr/v1/iamport.js";
- script.async = true;
- document.head.appendChild(script);
- }
-}
-
-export function requestPay(params: Record): Promise {
- return new Promise((resolve, reject) => {
- if (!window.IMP) {
- reject(new Error("PortOne SDK not loaded"));
- return;
- }
- // Expose params for E2E diagnostics and assertions
- try {
- (window as any).capturedParams = params;
- (window as any).__lastPaymentParams = params;
- } catch {}
- window.IMP.init(import.meta.env.VITE_PORTONE_IMP_CODE);
- window.IMP.request_pay(params, (rsp: any) => {
- if (rsp.success) resolve(rsp);
- else reject({ code: rsp.error_code, message: rsp.error_msg });
- });
- });
-}
diff --git a/src/lib/recaptcha.ts b/src/lib/recaptcha.ts
deleted file mode 100644
index 95c852e..0000000
--- a/src/lib/recaptcha.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-let recaptchaLoadedPromise: Promise | null = null;
-
-function injectRecaptchaScript(siteKey: string): Promise {
- if (recaptchaLoadedPromise) return recaptchaLoadedPromise;
-
- recaptchaLoadedPromise = new Promise((resolve, reject) => {
- if (typeof window === "undefined") {
- resolve();
- return;
- }
-
- const existing = document.querySelector(
- 'script[src^="https://www.google.com/recaptcha/api.js?"]'
- ) as HTMLScriptElement | null;
- if (existing) {
- // If script exists, wait until grecaptcha is ready
- const maybeReady = (window as any).grecaptcha;
- if (maybeReady && typeof maybeReady.ready === "function") {
- maybeReady.ready(() => resolve());
- } else {
- // Fallback: resolve on next tick
- setTimeout(() => resolve(), 0);
- }
- return;
- }
-
- const script = document.createElement("script");
- script.src = `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(
- siteKey
- )}`;
- script.async = true;
- script.defer = true;
- script.onload = () => {
- const grecaptcha = (window as any).grecaptcha;
- if (grecaptcha && typeof grecaptcha.ready === "function") {
- grecaptcha.ready(() => resolve());
- } else {
- resolve();
- }
- };
- script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
- document.head.appendChild(script);
- });
-
- return recaptchaLoadedPromise;
-}
-
-export async function getRecaptchaToken(
- action: string = "global"
-): Promise {
- const siteKey = import.meta.env.VITE_RECAPTCHA_SITE_KEY as string | undefined;
- if (!siteKey) return undefined; // gracefully skip if not configured
-
- await injectRecaptchaScript(siteKey);
-
- const grecaptcha =
- (typeof window !== "undefined" && (window as any).grecaptcha) || undefined;
- if (!grecaptcha || typeof grecaptcha.execute !== "function") return undefined;
-
- try {
- const token: string = await grecaptcha.execute(siteKey, { action });
- return token;
- } catch {
- return undefined;
- }
-}
diff --git a/src/main.tsx b/src/main.tsx
deleted file mode 100644
index a1e6b61..0000000
--- a/src/main.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import App from "@/App";
-import "@/index.css";
-import ErrorBoundary from "@/components/ErrorBoundary";
-import { ApolloProvider } from "@apollo/client/react";
-import { apolloClient } from "@/lib/apollo-client";
-
-const rootElement = document.getElementById("root");
-if (!rootElement) throw new Error("Failed to find the root element");
-
-ReactDOM.createRoot(rootElement).render(
-
-
-
-
-
-
-
-);
diff --git a/src/pages/README.md b/src/pages/README.md
deleted file mode 100644
index 23f21c3..0000000
--- a/src/pages/README.md
+++ /dev/null
@@ -1 +0,0 @@
-### 페이지 단위 컴포넌트
\ No newline at end of file
diff --git a/src/pages/account/FindAccountConfirmPage.tsx b/src/pages/account/FindAccountConfirmPage.tsx
deleted file mode 100644
index 32d91d4..0000000
--- a/src/pages/account/FindAccountConfirmPage.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import { useMemo } from "react";
-import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
-import { css } from "../../../styled-system/css";
-import { Box } from "../../../styled-system/jsx";
-
-type LocationState = {
- nickname?: string;
-};
-
-const FindAccountConfirmPage = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const [params] = useSearchParams();
-
- const nickname = useMemo(() => {
- const state = (location.state as LocationState) || {};
- return state.nickname || params.get("nickname") || "Unknown";
- }, [location.state, params]);
-
- return (
-
-
-
-
-
-
- Find Account
-
-
-
-
-
- {/* Success icon */}
-
-
-
-
-
-
- You’re Nickname is
-
-
-
- {nickname}
-
-
- navigate("/auth/login")}
- style={{
- width: "100%",
- height: "2.75rem",
- borderRadius: "9999px",
- backgroundColor: "#22c55e",
- color: "white",
- border: "none",
- cursor: "pointer",
- transition: "filter 150ms, transform 150ms",
- }}
- onMouseEnter={(e) =>
- (e.currentTarget.style.filter = "brightness(105%)")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.filter = "brightness(100%)")
- }
- onMouseDown={(e) =>
- (e.currentTarget.style.transform = "scale(0.98)")
- }
- onMouseUp={(e) => (e.currentTarget.style.transform = "scale(1)")}
- >
- Back to the Login Page
-
-
-
-
-
- );
-};
-
-export default FindAccountConfirmPage;
-
-// Duplicate legacy implementation removed to resolve compile/export conflicts.
diff --git a/src/pages/account/FindAccountCreatePage.tsx b/src/pages/account/FindAccountCreatePage.tsx
deleted file mode 100644
index 604b314..0000000
--- a/src/pages/account/FindAccountCreatePage.tsx
+++ /dev/null
@@ -1,476 +0,0 @@
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import useAuthStore from "@/store/authStore";
-import { apolloClient } from "@/lib/apollo-client";
-import { css } from "../../../styled-system/css";
-import { Box } from "../../../styled-system/jsx";
-import backIcon from "@/assets/images/less-than_white.png";
-
-// Placeholder GraphQL op; replace when backend is ready
-import { gql } from "@apollo/client";
-const FIND_ACCOUNT = gql`
- mutation FindAccount($input: FindAccountInput!) {
- findAccount(input: $input) {
- maskedEmail
- }
- }
-`;
-
-const SEND_FIND_CODE = gql`
- mutation SendFindCode($input: SendFindCodeInput!) {
- sendFindCode(input: $input) {
- success
- expiresInSec
- }
- }
-`;
-
-const VERIFY_FIND_CODE = gql`
- mutation VerifyFindCode($input: VerifyFindCodeInput!) {
- verifyFindCode(input: $input) {
- success
- maskedEmail
- }
- }
-`;
-
-const FindAccountCreatePage = () => {
- const navigate = useNavigate();
- const token = useAuthStore((state: any) => state.token);
- const [nickname, setNickname] = useState("");
- const [password, setPassword] = useState("");
- const [loading, setLoading] = useState(false);
- const [errorText, setErrorText] = useState("");
- const [step, setStep] = useState<"credentials" | "otp">("credentials");
- const [otp, setOtp] = useState("");
- const [expiresAt, setExpiresAt] = useState(null);
- const [secondsLeft, setSecondsLeft] = useState(0);
-
- useEffect(() => {
- if (token) navigate("/main");
- }, [token, navigate]);
-
- const isValidNickname = (value: string): boolean => {
- return value.trim().length > 0;
- };
-
- const canSubmit =
- isValidNickname(nickname) && password.trim().length > 0 && !loading;
-
- // Step 1: submit credentials; request OTP
- const handleSubmit = async (): Promise => {
- if (!canSubmit) return;
- setErrorText("");
- try {
- setLoading(true);
- // Validate credentials first (without navigating)
- const { data: findData } = await apolloClient.mutate({
- mutation: FIND_ACCOUNT,
- variables: { input: { nickname: nickname.trim(), password } },
- });
- const maskedEmail: string | undefined = (findData as any)?.findAccount
- ?.maskedEmail;
- if (!maskedEmail) {
- setErrorText("일치하는 계정이 없거나 정보가 올바르지 않습니다.");
- return;
- }
- const { data: sendData } = await apolloClient.mutate({
- mutation: SEND_FIND_CODE,
- variables: { input: { nickname: nickname.trim() } },
- });
- const sendPayload = (sendData as any)?.sendFindCode;
- const ttlSec: number = sendPayload?.expiresInSec ?? 180;
- const expire = Date.now() + ttlSec * 1000;
- setExpiresAt(expire);
- setSecondsLeft(ttlSec);
- setStep("otp");
- } catch (e) {
- setErrorText("조회 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
- } finally {
- setLoading(false);
- }
- };
-
- // countdown effect
- useEffect(() => {
- if (step !== "otp" || !expiresAt) return;
- const id = setInterval(() => {
- const left = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000));
- setSecondsLeft(left);
- if (left <= 0) clearInterval(id);
- }, 1000);
- return () => clearInterval(id);
- }, [step, expiresAt]);
-
- const otpValid = otp.trim().length >= 4 && secondsLeft > 0 && !loading;
-
- const handleVerify = async (): Promise => {
- if (!otpValid) return;
- setErrorText("");
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: VERIFY_FIND_CODE,
- variables: { input: { nickname: nickname.trim(), code: otp.trim() } },
- });
- const payload = (data as any)?.verifyFindCode;
- if (!payload?.success) {
- setErrorText("인증코드가 올바르지 않습니다.");
- return;
- }
- navigate("/auth/find/account/confirm", {
- state: { maskedEmail: payload?.maskedEmail },
- });
- } catch (e) {
- setErrorText("인증 처리 중 오류가 발생했습니다.");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- {/* Top-left Back Button */}
- navigate("/auth/login")}
- style={{
- position: "fixed",
- top: "1rem",
- left: "1rem",
- zIndex: 10,
- background: "none",
- border: "none",
- padding: 0,
- cursor: "pointer",
- }}
- >
-
-
-
-
-
-
-
-
-
-
- 계정 찾기
-
-
-
- {step === "credentials" && (
- <>
-
-
- 닉네임
-
- setNickname(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="닉네임을 입력하세요"
- />
-
-
-
- 비밀번호
-
- setPassword(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="비밀번호를 입력하세요"
- />
-
-
-
- {loading ? "조회 중..." : "조회하기"}
-
-
- >
- )}
-
- {step === "otp" && (
- <>
-
-
- 이메일로 전송된 인증번호를 입력하세요
-
-
-
- setOtp(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleVerify()}
- style={{
- width: "10rem",
- padding: "0.75rem 1rem",
- fontSize: "1rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- textAlign: "center",
- letterSpacing: "0.2em",
- }}
- placeholder="000000"
- />
- 0 ? "gray.700" : "red.600",
- minW: "14",
- })}
- data-testid="otp-timer"
- >
- {Math.floor(secondsLeft / 60)
- .toString()
- .padStart(2, "0")}
- :{(secondsLeft % 60).toString().padStart(2, "0")}
-
-
- {secondsLeft === 0 && (
-
- 인증 시간이 만료되었습니다. 다시 요청해주세요.
-
- )}
-
-
- 확인
-
- 0}
- onClick={async () => {
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: SEND_FIND_CODE,
- variables: { input: { nickname: nickname.trim() } },
- });
- const ttl =
- (data as any)?.sendFindCode?.expiresInSec ?? 180;
- setExpiresAt(Date.now() + ttl * 1000);
- setSecondsLeft(ttl);
- } finally {
- setLoading(false);
- }
- }}
- className={css({
- px: "6",
- borderRadius: "full",
- backgroundColor:
- secondsLeft > 0 ? "gray.200" : "gray.700",
- color: secondsLeft > 0 ? "gray.500" : "white",
- fontWeight: "medium",
- transition: "all 150ms",
- })}
- style={{ minHeight: "2.5rem" }}
- >
- 재전송
-
-
- >
- )}
-
- {errorText && (
-
- {errorText}
-
- )}
-
-
-
-
- );
-};
-
-export default FindAccountCreatePage;
diff --git a/src/pages/account/FindPasswordConfirmPage.tsx b/src/pages/account/FindPasswordConfirmPage.tsx
deleted file mode 100644
index d894446..0000000
--- a/src/pages/account/FindPasswordConfirmPage.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import { useMemo } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
-import { css } from "../../../styled-system/css";
-import { Box } from "../../../styled-system/jsx";
-
-type LocationState = { email?: string };
-
-const FindPasswordConfirmPage = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const email = useMemo(() => {
- const state = (location.state as LocationState) || {};
- return state.email || "";
- }, [location.state]);
-
- return (
-
-
-
-
-
-
- Find Password
-
-
-
-
-
-
-
-
-
-
-
- 이메일 발송 완료
-
-
- {email}
-
-
- navigate("/auth/login")}
- style={{
- width: "100%",
- height: "2.75rem",
- borderRadius: "9999px",
- backgroundColor: "#22c55e",
- color: "white",
- border: "none",
- cursor: "pointer",
- transition: "filter 150ms, transform 150ms",
- }}
- onMouseEnter={(e) =>
- (e.currentTarget.style.filter = "brightness(105%)")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.filter = "brightness(100%)")
- }
- onMouseDown={(e) =>
- (e.currentTarget.style.transform = "scale(0.98)")
- }
- onMouseUp={(e) => (e.currentTarget.style.transform = "scale(1)")}
- >
- Back to the Login Page
-
-
-
-
-
- );
-};
-
-export default FindPasswordConfirmPage;
diff --git a/src/pages/account/FindPasswordCreatePage.tsx b/src/pages/account/FindPasswordCreatePage.tsx
deleted file mode 100644
index 22a3c80..0000000
--- a/src/pages/account/FindPasswordCreatePage.tsx
+++ /dev/null
@@ -1,412 +0,0 @@
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import useAuthStore from "@/store/authStore";
-import { apolloClient } from "@/lib/apollo-client";
-import { css } from "../../../styled-system/css";
-import { Box } from "../../../styled-system/jsx";
-import { gql } from "@apollo/client";
-import backIcon from "@/assets/images/less-than_white.png";
-
-const SEND_PW_CODE = gql`
- mutation SendPwCode($input: SendPwCodeInput!) {
- sendPwCode(input: $input) {
- success
- expiresInSec
- }
- }
-`;
-
-const VERIFY_PW_CODE = gql`
- mutation VerifyPwCode($input: VerifyPwCodeInput!) {
- verifyPwCode(input: $input) {
- success
- email
- }
- }
-`;
-
-const FindPasswordCreatePage = () => {
- const navigate = useNavigate();
- const token = useAuthStore((state: any) => state.token);
- const [email, setEmail] = useState("");
- const [loading, setLoading] = useState(false);
- const [errorText, setErrorText] = useState("");
- const [step, setStep] = useState<"email" | "otp">("email");
- const [otp, setOtp] = useState("");
- const [expiresAt, setExpiresAt] = useState(null);
- const [secondsLeft, setSecondsLeft] = useState(0);
-
- useEffect(() => {
- if (token) navigate("/main");
- }, [token, navigate]);
-
- const isValidEmail = (value: string): boolean => /.+@.+\..+/.test(value);
- const canSubmit = isValidEmail(email) && !loading;
-
- const handleSend = async (): Promise => {
- if (!canSubmit) return;
- setErrorText("");
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: SEND_PW_CODE,
- variables: { input: { email: email.trim() } },
- });
- const payload = (data as any)?.sendPwCode;
- const ttlSec: number = payload?.expiresInSec ?? 180;
- const expire = Date.now() + ttlSec * 1000;
- setExpiresAt(expire);
- setSecondsLeft(ttlSec);
- setStep("otp");
- } catch {
- setErrorText("이메일 전송 중 오류가 발생했습니다.");
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- if (step !== "otp" || !expiresAt) return;
- const id = setInterval(() => {
- const left = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000));
- setSecondsLeft(left);
- if (left <= 0) clearInterval(id);
- }, 1000);
- return () => clearInterval(id);
- }, [step, expiresAt]);
-
- const otpValid = otp.trim().length >= 4 && secondsLeft > 0 && !loading;
-
- const handleVerify = async (): Promise => {
- if (!otpValid) return;
- setErrorText("");
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: VERIFY_PW_CODE,
- variables: { input: { email: email.trim(), code: otp.trim() } },
- });
- const payload = (data as any)?.verifyPwCode;
- if (!payload?.success) {
- setErrorText("인증코드가 올바르지 않습니다.");
- return;
- }
- navigate("/auth/find/password/confirm", {
- state: { email: payload.email },
- });
- } catch {
- setErrorText("인증 처리 중 오류가 발생했습니다.");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- {/* Top-left Back Button */}
- navigate("/auth/login")}
- style={{
- position: "fixed",
- top: "1rem",
- left: "1rem",
- zIndex: 10,
- background: "none",
- border: "none",
- padding: 0,
- cursor: "pointer",
- }}
- >
-
-
-
-
-
-
-
-
-
-
- 비밀번호 찾기
-
-
-
- {step === "email" && (
- <>
-
-
- 이메일
-
- setEmail(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleSend()}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="이메일을 입력하세요"
- />
-
-
-
- {loading ? "전송 중..." : "이메일 발송"}
-
-
- >
- )}
-
- {step === "otp" && (
- <>
-
-
- 이메일로 전송된 인증번호를 입력하세요
-
-
-
- setOtp(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleVerify()}
- style={{
- width: "10rem",
- padding: "0.75rem 1rem",
- fontSize: "1rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- textAlign: "center",
- letterSpacing: "0.2em",
- }}
- placeholder="000000"
- />
- 0 ? "gray.700" : "red.600",
- minW: "14",
- })}
- data-testid="otp-timer"
- >
- {Math.floor(secondsLeft / 60)
- .toString()
- .padStart(2, "0")}
- :{(secondsLeft % 60).toString().padStart(2, "0")}
-
-
- {secondsLeft === 0 && (
-
- 인증 시간이 만료되었습니다. 다시 요청해주세요.
-
- )}
-
-
- 확인
-
- 0}
- onClick={async () => {
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: SEND_PW_CODE,
- variables: { input: { email: email.trim() } },
- });
- const ttl =
- (data as any)?.sendPwCode?.expiresInSec ?? 180;
- setExpiresAt(Date.now() + ttl * 1000);
- setSecondsLeft(ttl);
- } finally {
- setLoading(false);
- }
- }}
- className={css({
- px: "6",
- borderRadius: "full",
- backgroundColor:
- secondsLeft > 0 ? "gray.200" : "gray.700",
- color: secondsLeft > 0 ? "gray.500" : "white",
- fontWeight: "medium",
- transition: "all 150ms",
- })}
- style={{ minHeight: "2.5rem" }}
- >
- 재전송
-
-
- >
- )}
-
- {errorText && (
-
- {errorText}
-
- )}
-
-
-
-
- );
-};
-
-export default FindPasswordCreatePage;
diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx
deleted file mode 100644
index 78b7d50..0000000
--- a/src/pages/auth/LoginPage.tsx
+++ /dev/null
@@ -1,466 +0,0 @@
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import loginButtonImage from "@/assets/images/login_button.png";
-import signupButtonImage from "@/assets/images/signup_button.png";
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import useAuthStore from "@/store/authStore";
-import { useSessionStore } from "@/store/useSessionStore";
-import { apolloClient } from "@/lib/apollo-client";
-import { LOGIN } from "@/features/auth/graphql/mutations";
-import { css } from "../../../styled-system/css";
-import { Box, Flex, VStack, HStack } from "../../../styled-system/jsx";
-
-const LoginPage = () => {
- const navigate = useNavigate();
- const [id, setId] = useState("");
- const [password, setPassword] = useState("");
- const token = useAuthStore((state: any) => state.token);
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (token) {
- navigate("/main");
- }
- }, [token, navigate]);
-
- const handleLogin = async () => {
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: LOGIN,
- variables: { input: { email: id, password } },
- });
-
- const payload = (data as any)?.login;
- if (!payload?.tokens?.accessToken) {
- alert("아이디 및 비밀번호가 틀렸습니다.");
- return;
- }
-
- const accessToken: string = payload.tokens.accessToken;
- const nickname: string = payload.user?.name ?? "";
-
- document.cookie = `accessToken=${accessToken}; path=/; max-age=604800;`;
-
- type AuthStoreState = {
- login: (args: { nickname: string; token: string }) => void;
- };
- const authState = useAuthStore.getState() as unknown as AuthStoreState;
- authState.login({ nickname, token: accessToken });
-
- useSessionStore.getState().setSession();
- navigate("/main");
- } catch (err) {
- alert("로그인 실패!");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- {/* Stacked content (logo + title + card) */}
-
- {/* Logo */}
-
-
- {/* Log in Title - Separated */}
-
-
- 우당탕탕 클론노바
-
-
-
- {/* Login Card */}
-
- {/* Form Stack */}
-
- {/* Nickname Input */}
-
-
- 닉네임
-
- setId(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && handleLogin()}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="닉네임을 입력하세요"
- />
-
-
- {/* Password Input */}
-
-
- 비밀번호
-
- e.key === "Enter" && handleLogin()}
- onChange={(e) => setPassword(e.target.value)}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="비밀번호를 입력하세요"
- />
-
-
- {/* Find Account / Find Password Links */}
-
- navigate("/auth/find/account")}
- >
- 계정이 기억나지 않으신가요?
-
-
- navigate("/auth/find/password")}
- >
- 비밀번호가 기억나지 않으신가요?
-
-
-
- {/* Buttons */}
-
- {/* Login and Sign Up buttons */}
-
- {/* Login Button */}
- {
- e.currentTarget.style.opacity = "0.9";
- e.currentTarget.style.transform = "translateY(1px)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.opacity = "1";
- e.currentTarget.style.transform = "translateY(0)";
- }}
- onMouseDown={(e) => {
- e.currentTarget.style.transform = "scale(0.95)";
- }}
- onMouseUp={(e) => {
- e.currentTarget.style.transform = "translateY(1px)";
- }}
- >
-
-
-
- {/* Sign Up Button */}
- navigate("/auth/signup")}
- style={{
- flex: "1",
- background: "none",
- border: "none",
- padding: "0",
- cursor: "pointer",
- transition: "opacity 150ms, transform 150ms",
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.opacity = "0.9";
- e.currentTarget.style.transform = "translateY(1px)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.opacity = "1";
- e.currentTarget.style.transform = "translateY(0)";
- }}
- onMouseDown={(e) => {
- e.currentTarget.style.transform = "scale(0.95)";
- }}
- onMouseUp={(e) => {
- e.currentTarget.style.transform = "translateY(1px)";
- }}
- >
-
-
-
-
- {/* OAuth buttons */}
-
- {/* Google OAuth Button */}
-
- (e.currentTarget.style.backgroundColor = "#f9fafb")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.backgroundColor = "white")
- }
- onClick={() => console.log("Google OAuth")}
- >
-
-
-
-
-
-
-
-
- {/* Kakao OAuth Button */}
-
- (e.currentTarget.style.filter = "brightness(105%)")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.filter = "brightness(100%)")
- }
- onClick={() => console.log("Kakao OAuth")}
- >
-
-
-
-
-
-
-
- {loading && (
-
- 로그인 중...
-
- )}
-
-
-
-
- );
-};
-
-export default LoginPage;
diff --git a/src/pages/auth/SignUpPage.tsx b/src/pages/auth/SignUpPage.tsx
deleted file mode 100644
index ad9faed..0000000
--- a/src/pages/auth/SignUpPage.tsx
+++ /dev/null
@@ -1,521 +0,0 @@
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import signupButtonImage from "@/assets/images/signup_button.png";
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import useAuthStore from "@/store/authStore";
-import { useSessionStore } from "@/store/useSessionStore";
-import { apolloClient } from "@/lib/apollo-client";
-import { REGISTER } from "@/features/auth/graphql/mutations";
-import { css } from "../../../styled-system/css";
-import { Box } from "../../../styled-system/jsx";
-import backIcon from "@/assets/images/less-than_white.png";
-
-const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-
-const SignUpPage = () => {
- const navigate = useNavigate();
- const token = useAuthStore((state: any) => state.token);
-
- const [nickname, setNickname] = useState("");
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [agreeTerms, setAgreeTerms] = useState(false);
- const [agreeMarketing, setAgreeMarketing] = useState(false);
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (token) {
- navigate("/main");
- }
- }, [token, navigate]);
-
- const validate = (): string | null => {
- if (!nickname.trim()) return "닉네임을 입력하세요.";
- if (!EMAIL_REGEX.test(email)) return "올바른 이메일을 입력하세요.";
- if (password.length < 8) return "비밀번호는 8자 이상이어야 합니다.";
- if (!agreeTerms) return "약관에 동의해 주세요.";
- return null;
- };
-
- const handleRegister = async () => {
- const validationError = validate();
- if (validationError) {
- alert(validationError);
- return;
- }
- try {
- setLoading(true);
- const { data } = await apolloClient.mutate({
- mutation: REGISTER,
- variables: { input: { name: nickname, email, password } },
- });
-
- const payload = (data as any)?.register;
- if (payload?.tokens?.accessToken) {
- const accessToken: string = payload.tokens.accessToken;
- const userName: string = payload.user?.name ?? nickname;
-
- document.cookie = `accessToken=${accessToken}; path=/; max-age=604800;`;
-
- type AuthStoreState = {
- login: (args: { nickname: string; token: string }) => void;
- };
- const authState = useAuthStore.getState() as unknown as AuthStoreState;
- authState.login({ nickname: userName, token: accessToken });
- useSessionStore.getState().setSession();
- navigate("/main");
- return;
- }
-
- alert("회원가입이 완료되었습니다. 로그인 해주세요.");
- navigate("/auth/login");
- } catch (err) {
- alert("회원가입 실패!");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- navigate("/auth/login")}
- style={{
- position: "fixed",
- top: "1rem",
- left: "1rem",
- zIndex: 10,
- background: "none",
- border: "none",
- padding: 0,
- cursor: "pointer",
- }}
- >
-
-
-
- {/* Left column: Logo + tagline */}
-
-
-
- 코드와 게임이 만나는 곳, CodeNova에 오신 것을 환영합니다. 지금 바로
- 회원가입하고 새로운 경험을 시작하세요.
-
-
-
- {/* Right column: Sign up form card */}
-
-
-
- 회원가입
-
-
-
-
-
- 닉네임
-
- setNickname(e.target.value)}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="닉네임을 입력하세요"
- />
-
-
-
-
- 이메일
-
- setEmail(e.target.value)}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="이메일을 입력하세요"
- />
-
-
-
-
- 비밀번호
-
- e.key === "Enter" && handleRegister()}
- onChange={(e) => setPassword(e.target.value)}
- style={{
- width: "100%",
- padding: "0.75rem 1rem",
- fontSize: "0.875rem",
- borderRadius: "0.5rem",
- border: "1px solid #d1d5db",
- backgroundColor: "white",
- color: "#111827",
- outline: "none",
- transition: "box-shadow 150ms, border-color 150ms",
- boxSizing: "border-box",
- }}
- placeholder="비밀번호 (8자 이상)"
- />
-
- 8자 이상, 영문/숫자/기호 조합 권장
-
-
-
-
-
- setAgreeTerms(e.target.checked)}
- aria-label="agree terms"
- />
-
- 계정을 생성하면 서비스 약관 및 개인정보 처리방침에 동의하게
- 됩니다.
-
-
-
-
-
- {
- e.currentTarget.style.opacity = "0.9";
- e.currentTarget.style.transform = "translateY(1px)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.opacity = "1";
- e.currentTarget.style.transform = "translateY(0)";
- }}
- onMouseDown={(e) => {
- e.currentTarget.style.transform = "scale(0.95)";
- }}
- onMouseUp={(e) => {
- e.currentTarget.style.transform = "translateY(1px)";
- }}
- >
-
-
-
-
- 이미 계정이 있으신가요?{" "}
- navigate("/auth/login")}
- style={{
- background: "none",
- border: "none",
- padding: 0,
- color: "#111827",
- textDecoration: "underline",
- cursor: "pointer",
- }}
- >
- 로그인
-
-
-
- {/* OAuth buttons */}
-
- {/* Google OAuth Button */}
-
- (e.currentTarget.style.backgroundColor = "#f9fafb")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.backgroundColor = "white")
- }
- onClick={() => console.log("Google OAuth")}
- >
-
-
-
-
-
-
-
-
- {/* Kakao OAuth Button */}
-
- (e.currentTarget.style.filter = "brightness(105%)")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.filter = "brightness(100%)")
- }
- onClick={() => console.log("Kakao OAuth")}
- >
-
-
-
-
-
-
-
-
- {loading && (
-
- 회원가입 중...
-
- )}
-
-
-
- );
-};
-
-export default SignUpPage;
diff --git a/src/pages/follower/FollowerPage.test.tsx b/src/pages/follower/FollowerPage.test.tsx
deleted file mode 100644
index 95ccf6b..0000000
--- a/src/pages/follower/FollowerPage.test.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import React from "react";
-import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { MockedProvider } from "@apollo/client/testing/react";
-import { BrowserRouter } from "react-router-dom";
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import FollowerPage from "./FollowerPage";
-import { GET_FOLLOWERS } from "../../features/user/graphql/follow-operations";
-
-// Mock the Header component
-vi.mock("@/components/common/Header", () => ({
- default: function MockHeader() {
- return Header
;
- },
-}));
-
-// Mock the styled-system
-vi.mock("../../../styled-system/jsx", () => ({
- Box: ({ children, ...props }: any) => {children}
,
-}));
-
-// Mock the background image
-vi.mock("@/assets/images/multi_background.png", () => ({
- default: "mock-background.png",
-}));
-
-// Mock react-router-dom
-const mockNavigate = vi.fn();
-vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual("react-router-dom");
- return {
- ...actual,
- useNavigate: () => mockNavigate,
- };
-});
-
-const mockFollowersData = {
- followers: {
- items: [
- {
- id: "1",
- name: "Esthera Jackson",
- email: "esthera@simmmple.com",
- avatar: "https://via.placeholder.com/40x40/4f46e5/ffffff?text=E",
- followedAt: "2024-01-01T00:00:00Z",
- isFollowing: false,
- },
- {
- id: "2",
- name: "Alexa Liras",
- email: "alexa@simmmple.com",
- avatar: "https://via.placeholder.com/40x40/4f46e5/ffffff?text=A",
- followedAt: "2024-01-02T00:00:00Z",
- isFollowing: true,
- },
- ],
- pageInfo: {
- page: 1,
- limit: 20,
- total: 2,
- totalPages: 1,
- hasNextPage: false,
- hasPreviousPage: false,
- },
- },
-};
-
-const mocks = [
- {
- request: {
- query: GET_FOLLOWERS,
- variables: {
- page: 1,
- limit: 20,
- search: undefined,
- },
- },
- result: {
- data: mockFollowersData,
- },
- },
- // Search query: "Esthera"
- {
- request: {
- query: GET_FOLLOWERS,
- variables: {
- page: 1,
- limit: 20,
- search: "Esthera",
- },
- },
- result: {
- data: {
- followers: {
- items: [mockFollowersData.followers.items[0]],
- pageInfo: {
- page: 1,
- limit: 20,
- total: 1,
- totalPages: 1,
- hasNextPage: false,
- hasPreviousPage: false,
- },
- },
- },
- },
- },
-];
-
-describe("FollowerPage", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it("renders follower page with modal", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Check if header is rendered
- expect(screen.getByTestId("header")).toBeInTheDocument();
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Followers")).toBeInTheDocument();
- });
-
- // Check if search bar is present
- expect(screen.getByPlaceholderText("Search")).toBeInTheDocument();
-
- // Check if follower names are displayed
- await screen.findByText("Esthera Jackson");
- await screen.findByText("Alexa Liras");
- });
-
- it("handles search functionality", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Followers")).toBeInTheDocument();
- });
-
- const searchInput = screen.getByPlaceholderText("Search");
-
- // Type in search input
- fireEvent.change(searchInput, { target: { value: "Esthera" } });
-
- expect(searchInput).toHaveValue("Esthera");
- });
-
- it("handles close button click", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Followers")).toBeInTheDocument();
- });
-
- // Click close button
- const closeButton = screen.getByLabelText("Close");
- fireEvent.click(closeButton);
-
- // Modal should start closing animation
- expect(closeButton).toBeInTheDocument();
- });
-
- it("navigates back to mypage when close button is clicked", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Followers")).toBeInTheDocument();
- });
-
- // Click close button
- const closeButton = screen.getByLabelText("Close");
- fireEvent.click(closeButton);
-
- // Wait for navigation to be called
- await waitFor(() => {
- expect(mockNavigate).toHaveBeenCalledWith("/mypage");
- });
- });
-});
diff --git a/src/pages/follower/FollowerPage.tsx b/src/pages/follower/FollowerPage.tsx
deleted file mode 100644
index facef1d..0000000
--- a/src/pages/follower/FollowerPage.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { useState, useCallback } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box } from "../../../styled-system/jsx";
-import Header from "@/components/common/Header";
-import FollowerModal from "./components/FollowerModal";
-import multibg from "@/assets/images/multi_background.png";
-import { useFollowers } from "./hooks/useFollowers";
-
-const FollowerPage: React.FC = () => {
- const navigate = useNavigate();
- const [searchQuery, setSearchQuery] = useState("");
-
- const { data, loading, error, hasNextPage, loadMore, refetch } = useFollowers(
- {
- page: 1,
- search: searchQuery,
- limit: 20,
- }
- );
-
- const handleSearch = useCallback((query: string) => {
- setSearchQuery(query);
- }, []);
-
- const handleClose = useCallback(() => {
- navigate("/mypage");
- }, [navigate]);
-
- return (
-
- {/* Header */}
-
- {}}
- onShowSetting={() => {}}
- onShowRanking={() => {}}
- />
-
-
- {/* Main Content Area */}
-
- {/* Board Container */}
-
- {/* Modal Container */}
-
-
-
-
- );
-};
-
-export default FollowerPage;
-
-/**
- *
- * className vs inline style
-1. className (CSS 클래스)
-
-Panda CSS의 css() 함수가 빌드 시점에 CSS 클래스를 생성
-생성된 클래스명이 HTML에 적용됨 (예:
)
-외부 스타일시트에 정의된 스타일 규칙 사용
-장점:
-재사용 가능
-미디어 쿼리, pseudo-classes (:hover) 사용 가능
-성능 최적화 (같은 스타일 공유)
-단점:
-CSS 우선순위(specificity) 문제 가능
-다른 스타일과 충돌 가능
-빌드 과정 필요
-2. inline style (인라인 스타일)
-
-JavaScript 객체를 직접 HTML 요소의 style 속성에 적용
-브라우저가
로 렌더링
-장점:
-가장 높은 우선순위 (!important 제외)
-확실하게 적용됨
-빌드 과정 불필요
-단점:
-재사용 어려움
-:hover, 미디어 쿼리 사용 불가
-성능 측면에서 비효율적 (중복 스타일)
-왜 inline style로 변경했나?
-여러분의 경우, className={css({...})}가 제대로 적용되지 않았던 이유는:
-CSS 우선순위 충돌: 다른 스타일이 더 높은 우선순위로 덮어씌울 수 있음
-Panda CSS 빌드 문제: CSS 클래스가 제대로 생성/적용되지 않을 수 있음
-Box-sizing 이슈: border가 width를 초과하게 만듦
-inline style은 가장 높은 우선순위를 가지므로 확실하게 width: "100%"가 적용됩니다.
- * className = 스타일시트의 클래스 참조 (유연하지만 충돌 가능)
- * style = 직접 적용 (확실하지만 제한적)
- */
diff --git a/src/pages/follower/components/FollowerModal.tsx b/src/pages/follower/components/FollowerModal.tsx
deleted file mode 100644
index cf12a43..0000000
--- a/src/pages/follower/components/FollowerModal.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import React, { useState, useRef, useEffect } from "react";
-import { css } from "../../../../styled-system/css";
-import { FollowerUser } from "@/features/user/types/follow-types";
-import SearchBar from "./SearchBar";
-import UserList from "./UserList";
-
-interface FollowerModalProps {
- followers: FollowerUser[];
- loading: boolean;
- error: Error | null;
- searchQuery: string;
- onSearch: (query: string) => void;
- onLoadMore: () => void;
- hasNextPage: boolean;
- onClose: () => void;
- onRefetch: () => void;
-}
-
-const FollowerModal: React.FC
= ({
- followers,
- loading,
- error,
- searchQuery,
- onSearch,
- onLoadMore,
- hasNextPage,
- onClose,
- onRefetch,
-}) => {
- const [isVisible, setIsVisible] = useState(false);
- const modalRef = useRef(null);
-
- useEffect(() => {
- setIsVisible(true);
- }, []);
-
- const handleClose = () => {
- setIsVisible(false);
- setTimeout(onClose, 300);
- };
-
- return (
-
- {/* Close Button - positioned at top right of entire modal */}
-
- ✕
-
-
- {/* Modal Header - Follow title only */}
-
- {/* Center: Title */}
-
- Followers
-
-
-
- {/* Content Area */}
-
- {/* Search Bar - positioned at right side */}
-
-
-
-
- {/* User List Container */}
-
- {/* NICKNAME Header */}
-
-
- {/* Scrollable User List */}
-
-
-
-
-
-
- {/* Bottom pixel decoration bar */}
-
- {[...Array(16)].map((_, i) => (
-
- ))}
-
-
- );
-};
-
-export default FollowerModal;
diff --git a/src/pages/follower/components/SearchBar.tsx b/src/pages/follower/components/SearchBar.tsx
deleted file mode 100644
index af4263b..0000000
--- a/src/pages/follower/components/SearchBar.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { useState, useCallback, useRef } from "react";
-
-interface SearchBarProps {
- value: string;
- onChange: (value: string) => void;
- placeholder?: string;
-}
-
-const SearchBar: React.FC = ({
- value,
- onChange,
- placeholder = "Search",
-}) => {
- const inputRef = useRef(null);
- const [localValue, setLocalValue] = useState(value);
- const [showClearButton, setShowClearButton] = useState(value.length > 0);
-
- const handleChange = useCallback(
- (e: React.ChangeEvent) => {
- const newValue = e.target.value;
- setLocalValue(newValue);
- setShowClearButton(newValue.length > 0);
- onChange(newValue);
- },
- [onChange]
- );
-
- const handleFocus = useCallback((e: React.FocusEvent) => {
- e.target.style.borderColor = "#ec4899";
- e.target.style.boxShadow = "0 0 0 3px rgba(236, 72, 153, 0.1)";
- }, []);
-
- const handleBlur = useCallback((e: React.FocusEvent) => {
- e.target.style.borderColor = "#d1d5db";
- e.target.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
- }, []);
-
- const handleClear = useCallback(() => {
- setLocalValue("");
- setShowClearButton(false);
- onChange("");
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, [onChange]);
-
- return (
-
- {/* Search Icon */}
-
- 🔍
-
-
- {/* Input Field */}
-
-
- {/* Clear Button */}
- {showClearButton && (
-
{
- e.currentTarget.style.backgroundColor = "#e5e7eb";
- e.currentTarget.style.color = "#374151";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "#f3f4f6";
- e.currentTarget.style.color = "#6b7280";
- }}
- >
- ×
-
- )}
-
- );
-};
-
-export default SearchBar;
diff --git a/src/pages/follower/components/UserItem.tsx b/src/pages/follower/components/UserItem.tsx
deleted file mode 100644
index 46c7074..0000000
--- a/src/pages/follower/components/UserItem.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import React, { useState, useCallback } from "react";
-import { css } from "../../../../styled-system/css";
-import { FollowerUser } from "@/features/user/types/follow-types";
-import { useMutation } from "@apollo/client/react";
-import {
- FOLLOW_USER,
- UNFOLLOW_USER,
-} from "@/features/user/graphql/follow-operations";
-
-interface UserItemProps {
- user: FollowerUser;
-}
-
-const UserItem: React.FC = ({ user }) => {
- const [isFollowing, setIsFollowing] = useState(user.isFollowing);
- const [isLoading, setIsLoading] = useState(false);
- const [followUser] = useMutation(FOLLOW_USER);
- const [unfollowUser] = useMutation(UNFOLLOW_USER);
-
- const handleFollowToggle = useCallback(async () => {
- if (isLoading) return;
- setIsLoading(true);
- try {
- if (isFollowing) {
- await unfollowUser({ variables: { userId: user.id } });
- setIsFollowing(false);
- } else {
- await followUser({ variables: { userId: user.id } });
- setIsFollowing(true);
- }
- } catch {
- // keep previous state on error
- } finally {
- setIsLoading(false);
- }
- }, [isFollowing, isLoading, followUser, unfollowUser, user.id]);
-
- return (
-
- {/* Left side: Profile Image + User Info */}
-
- {/* Profile Image */}
-
-
- {/* User Info */}
-
-
- {user.name}
-
-
- {user.email}
-
-
-
-
- {/* Follow Button */}
-
- {isLoading ? (
-
- ) : (
- "👤"
- )}
-
-
- );
-};
-
-export default UserItem;
diff --git a/src/pages/follower/components/UserList.tsx b/src/pages/follower/components/UserList.tsx
deleted file mode 100644
index 5078fc4..0000000
--- a/src/pages/follower/components/UserList.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React, { useCallback, useRef, useEffect } from "react";
-import { css } from "../../../../styled-system/css";
-import { FollowerUser } from "@/features/user/types/follow-types";
-import UserItem from "./UserItem";
-
-interface UserListProps {
- followers: FollowerUser[];
- loading: boolean;
- error: Error | null;
- onLoadMore: () => void;
- hasNextPage: boolean;
- onRefetch: () => void;
-}
-
-const UserList: React.FC = ({
- followers,
- loading,
- error,
- onLoadMore,
- hasNextPage,
- onRefetch,
-}) => {
- const listRef = useRef(null);
- const loadingRef = useRef(null);
-
- // Intersection Observer for infinite scroll
- useEffect(() => {
- const observer = new IntersectionObserver(
- (entries) => {
- if (entries[0].isIntersecting && hasNextPage && !loading) {
- onLoadMore();
- }
- },
- { threshold: 0.1 }
- );
-
- if (loadingRef.current) {
- observer.observe(loadingRef.current);
- }
-
- return () => observer.disconnect();
- }, [hasNextPage, loading, onLoadMore]);
-
- const handleRetry = useCallback(() => {
- onRefetch();
- }, [onRefetch]);
-
- if (error) {
- return (
-
-
- Failed to load followers
-
-
- Try Again
-
-
- );
- }
-
- if (!loading && followers.length === 0) {
- return (
-
- No followers yet
-
- );
- }
-
- return (
-
- {/* User Items */}
-
- {followers.map((follower) => (
-
- ))}
-
-
- {/* Loading Indicator */}
- {loading && (
-
- )}
-
- {/* End of List */}
- {!hasNextPage && followers.length > 0 && (
-
- End of list
-
- )}
-
- );
-};
-
-export default UserList;
diff --git a/src/pages/follower/hooks/useFollowers.test.tsx b/src/pages/follower/hooks/useFollowers.test.tsx
deleted file mode 100644
index c37311f..0000000
--- a/src/pages/follower/hooks/useFollowers.test.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { describe, it, expect, vi } from "vitest";
-import { renderHook, waitFor } from "@testing-library/react";
-import { MockedProvider } from "@apollo/client/testing/react";
-import { useFollowers } from "./useFollowers";
-import { GET_FOLLOWERS } from "@/features/user/graphql/follow-operations";
-
-const mockFollowersData = {
- followers: {
- items: [
- {
- id: "1",
- name: "Esthera Jackson",
- email: "esthera@simmmple.com",
- avatar: "https://via.placeholder.com/40x40/4f46e5/ffffff?text=E",
- followedAt: "2024-01-01T00:00:00Z",
- isFollowing: false,
- },
- ],
- pageInfo: {
- page: 1,
- limit: 20,
- total: 1,
- totalPages: 1,
- hasNextPage: false,
- hasPreviousPage: false,
- },
- },
-};
-
-const mocks = [
- {
- request: {
- query: GET_FOLLOWERS,
- variables: {
- page: 1,
- limit: 20,
- search: undefined,
- },
- },
- result: {
- data: mockFollowersData,
- },
- },
-];
-
-describe("useFollowers", () => {
- it("should fetch followers data", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useFollowers({ page: 1, search: "" }), {
- wrapper,
- });
-
- // Initially loading should be true
- expect(result.current.loading).toBe(true);
-
- // Wait for data to load
- await waitFor(() => {
- expect(result.current.loading).toBe(false);
- });
-
- // Check if data is loaded correctly
- expect(result.current.data).toEqual({
- ...mockFollowersData.followers,
- items: mockFollowersData.followers.items,
- });
- expect(result.current.hasNextPage).toBe(false);
- expect(result.current.error).toBeNull();
- });
-
- it("should handle search functionality", async () => {
- const searchMocks = [
- {
- request: {
- query: GET_FOLLOWERS,
- variables: {
- page: 1,
- limit: 20,
- search: "Esthera",
- },
- },
- result: {
- data: mockFollowersData,
- },
- },
- ];
-
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(
- () => useFollowers({ page: 1, search: "Esthera" }),
- { wrapper }
- );
-
- await waitFor(() => {
- expect(result.current.loading).toBe(false);
- });
-
- expect(result.current.data).toEqual({
- ...mockFollowersData.followers,
- items: mockFollowersData.followers.items,
- });
- });
-
- it("should handle error state", async () => {
- const errorMocks = [
- {
- request: {
- query: GET_FOLLOWERS,
- variables: {
- page: 1,
- limit: 20,
- search: undefined,
- },
- },
- error: new Error("Failed to fetch followers"),
- },
- ];
-
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useFollowers({ page: 1, search: "" }), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.loading).toBe(false);
- });
-
- expect(result.current.error).toBeDefined();
- expect(result.current.data).toBeNull();
- });
-
- it("should handle refetch functionality", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useFollowers({ page: 1, search: "" }), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.loading).toBe(false);
- });
-
- // Test refetch
- expect(typeof result.current.refetch).toBe("function");
- });
-
- it("should handle loadMore functionality", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useFollowers({ page: 1, search: "" }), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.loading).toBe(false);
- });
-
- // Test loadMore
- expect(typeof result.current.loadMore).toBe("function");
- });
-});
diff --git a/src/pages/follower/hooks/useFollowers.test.tsx.backup b/src/pages/follower/hooks/useFollowers.test.tsx.backup
deleted file mode 100644
index 97893f0..0000000
--- a/src/pages/follower/hooks/useFollowers.test.tsx.backup
+++ /dev/null
@@ -1,13 +0,0 @@
-import { describe, it, expect, vi } from "vitest";
-import { renderHook, waitFor } from "@testing-library/react";
-import "@testing-library/jest-dom";
-
-// Skip these tests for now - need proper GraphQL setup
-describe.skip("useFollowing", () => {
-
-describe("useFollowing", () => {
- it("should handle following functionality", () => {
- // Test will be implemented when GraphQL setup is complete
- expect(true).toBe(true);
- });
-});
diff --git a/src/pages/follower/hooks/useFollowers.ts b/src/pages/follower/hooks/useFollowers.ts
deleted file mode 100644
index fdea63e..0000000
--- a/src/pages/follower/hooks/useFollowers.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { useQuery } from "@apollo/client/react";
-import { GET_FOLLOWERS } from "@/features/user/graphql/follow-operations";
-import {
- FollowerUser,
- FollowersResponse,
- GetFollowersData,
-} from "@/features/user/types/follow-types";
-
-interface UseFollowersOptions {
- page: number;
- search: string;
- limit?: number;
-}
-
-interface UseFollowersReturn {
- data: FollowersResponse | null;
- loading: boolean;
- error: Error | null;
- refetch: () => void;
- loadMore: () => void;
- hasNextPage: boolean;
-}
-
-export const useFollowers = ({
- page,
- search,
- limit = 20,
-}: UseFollowersOptions): UseFollowersReturn => {
- const [allFollowers, setAllFollowers] = useState([]);
- const [currentPage, setCurrentPage] = useState(1);
-
- const { data, loading, error, refetch } = useQuery(
- GET_FOLLOWERS,
- {
- variables: {
- page: currentPage,
- limit,
- search: search || undefined,
- },
- notifyOnNetworkStatusChange: true,
- }
- );
-
- // Reset followers when search changes
- useEffect(() => {
- if (search) {
- setAllFollowers([]);
- setCurrentPage(1);
- }
- }, [search]);
-
- // Update followers when new data arrives
- useEffect(() => {
- if (data?.followers) {
- if (currentPage === 1) {
- setAllFollowers(data.followers.items);
- } else {
- setAllFollowers((prev) => [...prev, ...data.followers.items]);
- }
- }
- }, [data, currentPage]);
-
- const loadMore = useCallback(() => {
- if (data?.followers?.pageInfo.hasNextPage && !loading) {
- setCurrentPage((prev) => prev + 1);
- }
- }, [data, loading]);
-
- const handleRefetch = useCallback(() => {
- setAllFollowers([]);
- setCurrentPage(1);
- refetch();
- }, [refetch]);
-
- return {
- data: data?.followers
- ? {
- items: currentPage === 1 ? data.followers.items : allFollowers,
- pageInfo: data.followers.pageInfo,
- }
- : null,
- loading,
- error: error || null,
- refetch: handleRefetch,
- loadMore,
- hasNextPage: data?.followers?.pageInfo.hasNextPage || false,
- };
-};
diff --git a/src/pages/following/FollowingPage.test.tsx b/src/pages/following/FollowingPage.test.tsx
deleted file mode 100644
index 4012de7..0000000
--- a/src/pages/following/FollowingPage.test.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-import React from "react";
-import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { MockedProvider } from "@apollo/client/testing/react";
-import { BrowserRouter } from "react-router-dom";
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import FollowingPage from "./FollowingPage";
-import { GET_FOLLOWING } from "../../features/user/graphql/follow-operations";
-
-// Mock the Header component
-vi.mock("@/components/common/Header", () => ({
- default: function MockHeader() {
- return Header
;
- },
-}));
-
-// Mock the styled-system
-vi.mock("../../../styled-system/jsx", () => ({
- Box: ({ children, ...props }: any) => {children}
,
-}));
-
-// Mock the background image
-vi.mock("@/assets/images/multi_background.png", () => ({
- default: "mock-background.png",
-}));
-
-// Mock react-router-dom
-const mockNavigate = vi.fn();
-vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual("react-router-dom");
- return {
- ...actual,
- useNavigate: () => mockNavigate,
- };
-});
-
-const mockFollowingData = {
- following: {
- items: [
- {
- id: "1",
- name: "Esthera Jackson",
- email: "esthera@simmmple.com",
- avatar: "https://via.placeholder.com/40x40/4f46e5/ffffff?text=E",
- followedAt: "2024-01-01T00:00:00Z",
- isFollowing: true,
- },
- {
- id: "2",
- name: "Alexa Liras",
- email: "alexa@simmmple.com",
- avatar: "https://via.placeholder.com/40x40/4f46e5/ffffff?text=A",
- followedAt: "2024-01-02T00:00:00Z",
- isFollowing: true,
- },
- ],
- pageInfo: {
- page: 1,
- limit: 20,
- total: 2,
- totalPages: 1,
- hasNextPage: false,
- hasPreviousPage: false,
- },
- },
-};
-
-const mocks = [
- {
- request: {
- query: GET_FOLLOWING,
- variables: {
- page: 1,
- limit: 20,
- search: undefined,
- },
- },
- result: {
- data: mockFollowingData,
- },
- },
- // Search query: "Esthera"
- {
- request: {
- query: GET_FOLLOWING,
- variables: {
- page: 1,
- limit: 20,
- search: "Esthera",
- },
- },
- result: {
- data: {
- following: {
- items: [mockFollowingData.following.items[0]],
- pageInfo: {
- page: 1,
- limit: 20,
- total: 1,
- totalPages: 1,
- hasNextPage: false,
- hasPreviousPage: false,
- },
- },
- },
- },
- },
- // Search query: "__no_match__" returns empty list
- {
- request: {
- query: GET_FOLLOWING,
- variables: {
- page: 1,
- limit: 20,
- search: "__no_match__",
- },
- },
- result: {
- data: {
- following: {
- items: [],
- pageInfo: {
- page: 1,
- limit: 20,
- total: 0,
- totalPages: 1,
- hasNextPage: false,
- hasPreviousPage: false,
- },
- },
- },
- },
- },
-];
-
-describe("FollowingPage", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it("renders following page with modal", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Check if header is rendered
- expect(screen.getByTestId("header")).toBeInTheDocument();
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Following")).toBeInTheDocument();
- });
-
- // Check if search bar is present
- expect(screen.getByPlaceholderText("Search")).toBeInTheDocument();
-
- // Check if following names are displayed
- await screen.findByText("Esthera Jackson");
- await screen.findByText("Alexa Liras");
- });
-
- it("handles search functionality", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Following")).toBeInTheDocument();
- });
-
- const searchInput = screen.getByPlaceholderText("Search");
-
- // Type in search input
- fireEvent.change(searchInput, { target: { value: "Esthera" } });
-
- expect(searchInput).toHaveValue("Esthera");
- });
-
- it("handles close button click", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Following")).toBeInTheDocument();
- });
-
- // Click close button
- const closeButton = screen.getByLabelText("Close");
- fireEvent.click(closeButton);
-
- // Modal should start closing animation
- expect(closeButton).toBeInTheDocument();
- });
-
- it("navigates back to mypage when close button is clicked", async () => {
- render(
- // @ts-expect-error addTypename is supported at runtime in our Apollo version
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Following")).toBeInTheDocument();
- });
-
- // Click close button
- const closeButton = screen.getByLabelText("Close");
- fireEvent.click(closeButton);
-
- // Wait for navigation to be called
- await waitFor(() => {
- expect(mockNavigate).toHaveBeenCalledWith("/mypage");
- });
- });
-
- it("shows empty state when search yields no results", async () => {
- render(
-
-
-
-
-
- );
-
- // Wait for the modal to appear
- await waitFor(() => {
- expect(screen.getByText("Following")).toBeInTheDocument();
- });
-
- // Enter a search that yields no matches
- const searchInput = screen.getByPlaceholderText("Search");
- fireEvent.change(searchInput, { target: { value: "__no_match__" } });
-
- // Expect empty state
- await waitFor(() => {
- expect(screen.getByText("Not following anyone yet")).toBeInTheDocument();
- });
- });
-});
diff --git a/src/pages/following/FollowingPage.tsx b/src/pages/following/FollowingPage.tsx
deleted file mode 100644
index badb980..0000000
--- a/src/pages/following/FollowingPage.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { useState, useCallback } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box } from "../../../styled-system/jsx";
-import Header from "@/components/common/Header";
-import FollowingModal from "./components/FollowingModal";
-import multibg from "@/assets/images/multi_background.png";
-import { useFollowing } from "./hooks/useFollowing";
-
-const FollowingPage: React.FC = () => {
- const navigate = useNavigate();
- const [searchQuery, setSearchQuery] = useState("");
- const { data, loading, error, hasNextPage, loadMore, refetch } = useFollowing(
- {
- page: 1,
- search: searchQuery,
- limit: 20,
- }
- );
-
- const handleSearch = useCallback((query: string) => {
- setSearchQuery(query);
- }, []);
-
- const handleClose = useCallback(() => {
- navigate("/mypage");
- }, [navigate]);
-
- return (
-
- {/* Header */}
-
- {}}
- onShowSetting={() => {}}
- onShowRanking={() => {}}
- />
-
-
- {/* Main Content Area */}
-
- {/* Board Container */}
-
- {/* Modal Container */}
-
-
-
-
- );
-};
-
-export default FollowingPage;
diff --git a/src/pages/following/components/FollowingModal.tsx b/src/pages/following/components/FollowingModal.tsx
deleted file mode 100644
index eb6453d..0000000
--- a/src/pages/following/components/FollowingModal.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-import React, { useState, useRef, useEffect } from "react";
-import { css } from "../../../../styled-system/css";
-import { FollowingUser } from "@/features/user/types/follow-types";
-import SearchBar from "./SearchBar";
-import UserList from "./UserList";
-
-interface FollowingModalProps {
- following: FollowingUser[];
- loading: boolean;
- error: Error | null;
- searchQuery: string;
- onSearch: (query: string) => void;
- onLoadMore: () => void;
- hasNextPage: boolean;
- onClose: () => void;
- onRefetch: () => void;
-}
-
-const FollowingModal: React.FC = ({
- following,
- loading,
- error,
- searchQuery,
- onSearch,
- onLoadMore,
- hasNextPage,
- onClose,
- onRefetch,
-}) => {
- const [isVisible, setIsVisible] = useState(false);
- const modalRef = useRef(null);
-
- useEffect(() => {
- setIsVisible(true);
- }, []);
-
- const handleClose = () => {
- setIsVisible(false);
- setTimeout(onClose, 300);
- };
-
- return (
-
- {/* Close Button - positioned at top right of entire modal */}
-
- ✕
-
-
- {/* Modal Header - Following title only */}
-
- {/* Center: Title */}
-
- Following
-
-
-
- {/* Content Area */}
-
- {/* Search Bar - positioned at right side */}
-
-
-
-
- {/* User List Container */}
-
- {/* NICKNAME Header */}
-
-
- {/* Scrollable User List */}
-
-
-
-
-
-
- {/* Bottom pixel decoration bar */}
-
- {[...Array(16)].map((_, i) => (
-
- ))}
-
-
- );
-};
-
-export default FollowingModal;
diff --git a/src/pages/following/components/SearchBar.tsx b/src/pages/following/components/SearchBar.tsx
deleted file mode 100644
index af4263b..0000000
--- a/src/pages/following/components/SearchBar.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { useState, useCallback, useRef } from "react";
-
-interface SearchBarProps {
- value: string;
- onChange: (value: string) => void;
- placeholder?: string;
-}
-
-const SearchBar: React.FC = ({
- value,
- onChange,
- placeholder = "Search",
-}) => {
- const inputRef = useRef(null);
- const [localValue, setLocalValue] = useState(value);
- const [showClearButton, setShowClearButton] = useState(value.length > 0);
-
- const handleChange = useCallback(
- (e: React.ChangeEvent) => {
- const newValue = e.target.value;
- setLocalValue(newValue);
- setShowClearButton(newValue.length > 0);
- onChange(newValue);
- },
- [onChange]
- );
-
- const handleFocus = useCallback((e: React.FocusEvent) => {
- e.target.style.borderColor = "#ec4899";
- e.target.style.boxShadow = "0 0 0 3px rgba(236, 72, 153, 0.1)";
- }, []);
-
- const handleBlur = useCallback((e: React.FocusEvent) => {
- e.target.style.borderColor = "#d1d5db";
- e.target.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
- }, []);
-
- const handleClear = useCallback(() => {
- setLocalValue("");
- setShowClearButton(false);
- onChange("");
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, [onChange]);
-
- return (
-
- {/* Search Icon */}
-
- 🔍
-
-
- {/* Input Field */}
-
-
- {/* Clear Button */}
- {showClearButton && (
-
{
- e.currentTarget.style.backgroundColor = "#e5e7eb";
- e.currentTarget.style.color = "#374151";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "#f3f4f6";
- e.currentTarget.style.color = "#6b7280";
- }}
- >
- ×
-
- )}
-
- );
-};
-
-export default SearchBar;
diff --git a/src/pages/following/components/UserItem.tsx b/src/pages/following/components/UserItem.tsx
deleted file mode 100644
index bfd965c..0000000
--- a/src/pages/following/components/UserItem.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import React, { useState, useCallback } from "react";
-import { css } from "../../../../styled-system/css";
-import { FollowingUser } from "@/features/user/types/follow-types";
-import { useMutation } from "@apollo/client/react";
-import { UNFOLLOW_USER } from "@/features/user/graphql/follow-operations";
-
-interface UserItemProps {
- user: FollowingUser;
-}
-
-const UserItem: React.FC = ({ user }) => {
- const [isLoading, setIsLoading] = useState(false);
- const [removed, setRemoved] = useState(false);
- const [unfollowUser] = useMutation(UNFOLLOW_USER);
-
- const handleUnfollow = useCallback(async () => {
- if (isLoading || removed) return;
- setIsLoading(true);
- try {
- await unfollowUser({ variables: { userId: user.id } });
- setRemoved(true);
- } catch {
- // no-op UI; keep item visible
- } finally {
- setIsLoading(false);
- }
- }, [isLoading, removed, unfollowUser, user.id]);
-
- if (removed) return null;
-
- return (
-
- {/* Left side: Profile Image + User Info */}
-
- {/* Profile Image */}
-
-
- {/* User Info */}
-
-
- {user.name}
-
-
- {user.email}
-
-
-
-
- {/* Unfollow Button */}
-
- {isLoading ? (
-
- ) : (
- "👤"
- )}
-
-
- );
-};
-
-export default UserItem;
diff --git a/src/pages/following/components/UserList.tsx b/src/pages/following/components/UserList.tsx
deleted file mode 100644
index 9f74787..0000000
--- a/src/pages/following/components/UserList.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React, { useCallback, useRef, useEffect } from "react";
-import { css } from "../../../../styled-system/css";
-import { FollowingUser } from "@/features/user/types/follow-types";
-import UserItem from "./UserItem";
-
-interface UserListProps {
- following: FollowingUser[];
- loading: boolean;
- error: Error | null;
- onLoadMore: () => void;
- hasNextPage: boolean;
- onRefetch: () => void;
-}
-
-const UserList: React.FC = ({
- following,
- loading,
- error,
- onLoadMore,
- hasNextPage,
- onRefetch,
-}) => {
- const listRef = useRef(null);
- const loadingRef = useRef(null);
-
- // Intersection Observer for infinite scroll
- useEffect(() => {
- const observer = new IntersectionObserver(
- (entries) => {
- if (entries[0].isIntersecting && hasNextPage && !loading) {
- onLoadMore();
- }
- },
- { threshold: 0.1 }
- );
-
- if (loadingRef.current) {
- observer.observe(loadingRef.current);
- }
-
- return () => observer.disconnect();
- }, [hasNextPage, loading, onLoadMore]);
-
- const handleRetry = useCallback(() => {
- onRefetch();
- }, [onRefetch]);
-
- if (error) {
- return (
-
-
- Failed to load following
-
-
- Try Again
-
-
- );
- }
-
- if (!loading && following.length === 0) {
- return (
-
- Not following anyone yet
-
- );
- }
-
- return (
-
- {/* User Items */}
-
- {following.map((user) => (
-
- ))}
-
-
- {/* Loading Indicator */}
- {loading && (
-
- )}
-
- {/* End of List */}
- {!hasNextPage && following.length > 0 && (
-
- End of list
-
- )}
-
- );
-};
-
-export default UserList;
diff --git a/src/pages/following/hooks/useFollowing.test.tsx b/src/pages/following/hooks/useFollowing.test.tsx
deleted file mode 100644
index 49b6ede..0000000
--- a/src/pages/following/hooks/useFollowing.test.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { describe, it, expect, vi } from "vitest";
-import { renderHook, waitFor } from "@testing-library/react";
-import "@testing-library/jest-dom";
-
-// Skip these tests for now - need proper GraphQL setup
-describe.skip("useFollowing", () => {
- describe("useFollowing", () => {
- it("should handle following functionality", () => {
- // Test will be implemented when GraphQL setup is complete
- expect(true).toBe(true);
- });
- });
-});
diff --git a/src/pages/following/hooks/useFollowing.ts b/src/pages/following/hooks/useFollowing.ts
deleted file mode 100644
index ace37ff..0000000
--- a/src/pages/following/hooks/useFollowing.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { useQuery } from "@apollo/client/react";
-import { GET_FOLLOWING } from "@/features/user/graphql/follow-operations";
-import {
- FollowingUser,
- FollowingResponse,
- GetFollowingData,
-} from "@/features/user/types/follow-types";
-
-interface UseFollowingOptions {
- page: number;
- search: string;
- limit?: number;
-}
-
-interface UseFollowingReturn {
- data: FollowingResponse | null;
- loading: boolean;
- error: Error | null;
- refetch: () => void;
- loadMore: () => void;
- hasNextPage: boolean;
-}
-
-export const useFollowing = ({
- page,
- search,
- limit = 20,
-}: UseFollowingOptions): UseFollowingReturn => {
- const [allFollowing, setAllFollowing] = useState([]);
- const [currentPage, setCurrentPage] = useState(1);
-
- const { data, loading, error, refetch } = useQuery(
- GET_FOLLOWING,
- {
- variables: {
- page: currentPage,
- limit,
- search: search || undefined,
- },
- notifyOnNetworkStatusChange: true,
- }
- );
-
- // Reset following when search changes
- useEffect(() => {
- if (search) {
- setAllFollowing([]);
- setCurrentPage(1);
- }
- }, [search]);
-
- // Update following when new data arrives
- useEffect(() => {
- if (data?.following) {
- if (currentPage === 1) {
- setAllFollowing(data.following.items);
- } else {
- setAllFollowing((prev) => [...prev, ...data.following.items]);
- }
- }
- }, [data, currentPage]);
-
- const loadMore = useCallback(() => {
- if (data?.following?.pageInfo.hasNextPage && !loading) {
- setCurrentPage((prev) => prev + 1);
- }
- }, [data, loading]);
-
- const handleRefetch = useCallback(() => {
- setAllFollowing([]);
- setCurrentPage(1);
- refetch();
- }, [refetch]);
-
- return {
- data: data?.following
- ? {
- items: allFollowing,
- pageInfo: data.following.pageInfo,
- }
- : null,
- loading,
- error: error || null,
- refetch: handleRefetch,
- loadMore,
- hasNextPage: data?.following?.pageInfo.hasNextPage || false,
- };
-};
diff --git a/src/pages/game/GameResultPage.tsx b/src/pages/game/GameResultPage.tsx
deleted file mode 100644
index 2c70d02..0000000
--- a/src/pages/game/GameResultPage.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { css } from "styled-system/css";
-import { useLocation, useNavigate } from "react-router-dom";
-
-interface GameResultState {
- score: number;
- rank?: number;
- isNewRecord?: boolean;
-}
-
-export default function GameResultPage() {
- const navigate = useNavigate();
- const location = useLocation();
- const state = (location.state as GameResultState) || { score: 0 };
-
- return (
-
-
-
- 게임 결과
-
-
- {Math.floor(state.score)}
-
- {state.rank != null && (
-
- 현재 순위: {state.rank} 위
-
- )}
- {state.isNewRecord && (
-
- 새로운 기록 달성!
-
- )}
-
- navigate(-1)}
- >
- 다시 하기
-
- navigate("/ranking")}
- >
- 랭킹 보러가기
-
-
-
-
- );
-}
diff --git a/src/pages/language/LanguageStorePage.test.tsx b/src/pages/language/LanguageStorePage.test.tsx
deleted file mode 100644
index 27d4f3e..0000000
--- a/src/pages/language/LanguageStorePage.test.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import React from "react";
-import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import { BrowserRouter } from "react-router-dom";
-import { vi } from "vitest";
-import LanguageStorePage from "./LanguageStorePage";
-
-// Mock react-router-dom
-const mockNavigate = vi.fn();
-vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual("react-router-dom");
- return {
- ...actual,
- useNavigate: () => mockNavigate,
- };
-});
-
-// Mock styled-system
-vi.mock("../../../styled-system/jsx", () => ({
- Box: ({ children, ...props }: any) => {children}
,
-}));
-
-// Mock styled-system/css
-vi.mock("../../../styled-system/css", () => ({
- css: (styles: any) => styles,
-}));
-
-const renderWithRouter = (component: React.ReactElement) => {
- return render({component} );
-};
-
-describe("LanguageStorePage", () => {
- beforeEach(() => {
- mockNavigate.mockClear();
- });
-
- it("renders language list correctly", () => {
- renderWithRouter( );
-
- expect(screen.getByText("Programming Languages")).toBeInTheDocument();
- const javaImages = screen.getAllByAltText("JAVA");
- expect(javaImages.length).toBeGreaterThan(0);
- expect(screen.getByAltText("JavaScript")).toBeInTheDocument();
- expect(screen.getByAltText("Python")).toBeInTheDocument();
- expect(screen.getByAltText("SQL")).toBeInTheDocument();
- expect(screen.getByAltText("C#")).toBeInTheDocument();
- });
-
- it("displays language details when selected", () => {
- renderWithRouter( );
-
- // JAVA should be selected by default (first language)
- expect(screen.getByText(/자바.*Java/)).toBeInTheDocument();
- expect(screen.getByText("5,000")).toBeInTheDocument();
- });
-
- it("changes language details when different language is selected", () => {
- renderWithRouter( );
-
- // Click on JavaScript
- const jsButton = screen.getByAltText("JavaScript");
- fireEvent.click(jsButton);
-
- expect(screen.getByText(/자바스크립트.*JavaScript/)).toBeInTheDocument();
- expect(screen.getByText("3,000")).toBeInTheDocument();
- });
-
- it("shows purchase button for unowned languages", () => {
- renderWithRouter( );
-
- // JAVA is not owned by default - just check that the component renders
- expect(screen.getByText("Store")).toBeInTheDocument();
- });
-
- it("shows 'Already bought' for owned languages", () => {
- renderWithRouter( );
-
- // Click on JavaScript (which is owned)
- const jsButton = screen.getByAltText("JavaScript");
- fireEvent.click(jsButton);
-
- expect(screen.getByText("Already bought")).toBeInTheDocument();
- });
-
- it("shows 'Insufficient balance' when user doesn't have enough stars", () => {
- renderWithRouter( );
-
- // Click on Python (price: 4000, balance: 3817)
- const pythonButton = screen.getByAltText("Python");
- fireEvent.click(pythonButton);
-
- expect(screen.getByText("Insufficient balance")).toBeInTheDocument();
- });
-
- it("handles purchase flow correctly", async () => {
- renderWithRouter( );
-
- // Click on SQL (price: 2500, balance: 3817 - should be purchasable)
- const sqlButton = screen.getByAltText("SQL");
- fireEvent.click(sqlButton);
-
- const purchaseButton = screen.getByText("Purchase");
- fireEvent.click(purchaseButton);
-
- // Wait for purchase to complete (with timeout)
- await waitFor(
- () => {
- expect(screen.getByText("Already bought")).toBeInTheDocument();
- },
- { timeout: 2000 }
- );
- });
-
- it("closes modal when close button is clicked", () => {
- renderWithRouter( );
-
- const closeButton = screen.getByAltText("Close");
- fireEvent.click(closeButton);
-
- expect(mockNavigate).toHaveBeenCalledWith("/main");
- });
-
- it("displays error message on purchase failure", async () => {
- renderWithRouter( );
-
- // Click on SQL
- const sqlButton = screen.getByAltText("SQL");
- fireEvent.click(sqlButton);
-
- const purchaseButton = screen.getByText("Purchase");
- fireEvent.click(purchaseButton);
-
- // Just verify the component doesn't crash
- expect(screen.getByText("Store")).toBeInTheDocument();
- });
-
- it("shows placeholder text when no language is selected", () => {
- renderWithRouter( );
-
- // The component should always have a selected language (first one by default)
- // This test verifies that the component renders without crashing
- expect(screen.getByText("Store")).toBeInTheDocument();
- });
-
- it("renders store title correctly", () => {
- renderWithRouter( );
-
- expect(screen.getByText("Store")).toBeInTheDocument();
- });
-
- it("renders character placeholder correctly", () => {
- renderWithRouter( );
-
- // Check if character image is rendered (now using actual images instead of text)
- const javaImages = screen.getAllByAltText("JAVA");
- expect(javaImages.length).toBeGreaterThan(0);
- });
-});
diff --git a/src/pages/language/LanguageStorePage.tsx b/src/pages/language/LanguageStorePage.tsx
deleted file mode 100644
index 6f5541c..0000000
--- a/src/pages/language/LanguageStorePage.tsx
+++ /dev/null
@@ -1,794 +0,0 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box } from "../../../styled-system/jsx";
-import multibg from "../../assets/images/multi_background.png";
-import Header from "../../components/common/Header.tsx";
-import TutoModal from "../../components/common/TutoModal.tsx";
-import SettingModal from "../../components/modal/SettingModal.tsx";
-import RankingModal from "../../components/modal/RankingModal.tsx";
-import javaImage from "../../assets/images/Java.png";
-import jsImage from "../../assets/images/js.png";
-import pythonImage from "../../assets/images/python.png";
-import sqlImage from "../../assets/images/SQL.png";
-import csharpImage from "../../assets/images/C.png";
-import javaButton from "../../assets/images/java_button.png";
-import jsButton from "../../assets/images/js_button.png";
-import pythonButton from "../../assets/images/python_button.png";
-import sqlButton from "../../assets/images/SQL_button.png";
-import csharpButton from "../../assets/images/C_button.png";
-import purchaseButton from "../../assets/images/large_btn.png";
-import closeButton from "../../assets/images/close_btn.png";
-import { css } from "../../../styled-system/css";
-
-// Language data configuration
-interface Language {
- id: string;
- name: string;
- displayName: string;
- price: number; // In stars
- description: string;
- characterImage: string;
- isOwned: boolean;
- category: "backend" | "frontend" | "database" | "mobile";
-}
-
-const languages: Language[] = [
- {
- id: "java",
- name: "JAVA",
- displayName: "JAVA",
- price: 5000,
- description:
- "자바(Java)는 썬 마이크로시스템즈에서 1995년에 개발한 객체 지향 프로그래밍 언어이다. 소스 코드를 하드웨어의 기계어로 직접 컴파일하여 링크하는 C/C++의 컴파일러와 달리 바이트코드인 클래스 파일(.class)을 생성하고, 자바 가상 머신(JVM)이 바이트 코드를 인터프리팅한다. 그래서 자바는 컴파일러 언어와 인터프리터 언어의 특징을 모두 가진 하이브리드 언어다.",
- characterImage: javaImage,
- isOwned: false,
- category: "backend",
- },
- {
- id: "javascript",
- name: "JS",
- displayName: "JavaScript",
- price: 3000,
- description:
- "자바스크립트(JavaScript)는 웹 브라우저에서 동적으로 작동하는 프로그래밍 언어이다. HTML과 CSS와 함께 웹의 핵심 기술 중 하나로, 웹 페이지의 동적 상호작용을 구현하는 데 사용된다.",
- characterImage: jsImage,
- isOwned: true,
- category: "frontend",
- },
- {
- id: "python",
- name: "PYTHON",
- displayName: "Python",
- price: 4000,
- description:
- "파이썬(Python)은 1991년 귀도 반 로섬이 개발한 고급 프로그래밍 언어이다. 간결하고 읽기 쉬운 문법으로 유명하며, 데이터 과학, 웹 개발, 인공지능 등 다양한 분야에서 널리 사용된다.",
- characterImage: pythonImage,
- isOwned: false,
- category: "backend",
- },
- {
- id: "sql",
- name: "SQL",
- displayName: "SQL",
- price: 2500,
- description:
- "SQL(Structured Query Language)은 관계형 데이터베이스 관리 시스템(RDBMS)에서 데이터를 조작하고 관리하기 위한 표준 언어이다. 데이터베이스의 구조를 정의하고, 데이터를 조회, 삽입, 수정, 삭제하는 데 사용된다.",
- characterImage: sqlImage,
- isOwned: false,
- category: "database",
- },
- {
- id: "csharp",
- name: "C#",
- displayName: "C#",
- price: 4500,
- description:
- "C#은 마이크로소프트에서 개발한 객체 지향 프로그래밍 언어이다. .NET 프레임워크와 함께 사용되며, 윈도우 애플리케이션, 웹 애플리케이션, 게임 개발 등에 널리 사용된다.",
- characterImage: csharpImage,
- isOwned: false,
- category: "backend",
- },
-];
-
-// Background Layer Component
-const BackgroundLayer: React.FC = () => (
-
- {/* Cityscape Buildings */}
-
- {/* Horizon Light */}
-
-
-);
-
-// Cityscape Buildings Component
-const CityscapeBuildings: React.FC = () => (
- <>
- {/* Left Buildings */}
-
- {Array.from({ length: 8 }, (_, i) => (
-
- {/* Building Windows */}
- {Array.from({ length: Math.floor(Math.random() * 6) + 2 }, (_, j) => (
-
- ))}
-
- ))}
-
-
- {/* Right Buildings */}
-
- {Array.from({ length: 8 }, (_, i) => (
-
- {/* Building Windows */}
- {Array.from({ length: Math.floor(Math.random() * 6) + 2 }, (_, j) => (
-
- ))}
-
- ))}
-
- >
-);
-
-// Horizon Light Component
-const HorizonLight: React.FC = () => (
-
-);
-
-// Language Item Component
-interface LanguageItemProps {
- language: Language;
- isSelected: boolean;
- onSelect: (language: Language) => void;
-}
-
-const LanguageItem: React.FC = ({
- language,
- isSelected,
- onSelect,
-}) => {
- // Get button image based on language
- const getButtonImage = () => {
- switch (language.id) {
- case "java":
- return javaButton;
- case "javascript":
- return jsButton;
- case "python":
- return pythonButton;
- case "sql":
- return sqlButton;
- case "csharp":
- return csharpButton;
- default:
- return javaButton;
- }
- };
-
- return (
- onSelect(language)}
- style={{
- position: "relative",
- cursor: "pointer",
- transition: "all 0.15s ease-in-out",
- opacity: isSelected ? 1 : 0.7,
- transform: isSelected ? "scale(1.05)" : "scale(1)",
- }}
- onMouseEnter={(e) => {
- if (!isSelected) {
- e.currentTarget.style.transform = "scale(1.02)";
- e.currentTarget.style.opacity = "0.9";
- }
- }}
- onMouseLeave={(e) => {
- if (!isSelected) {
- e.currentTarget.style.transform = "scale(1)";
- e.currentTarget.style.opacity = "0.7";
- }
- }}
- >
-
-
- );
-};
-
-// Purchase Button Component
-interface PurchaseButtonProps {
- language: Language;
- onPurchase: (language: Language) => void;
- disabled: boolean;
- currentBalance: number;
-}
-
-const PurchaseButton: React.FC = ({
- language,
- onPurchase,
- disabled,
- currentBalance,
-}) => {
- const getButtonText = () => {
- if (language.isOwned) {
- return "Already bought";
- }
- if (currentBalance < language.price) {
- return "Insufficient balance";
- }
- return "Purchase";
- };
-
- const getButtonStyle = () => {
- if (language.isOwned) {
- return {
- filter: "grayscale(100%) brightness(0.7)",
- cursor: "not-allowed",
- };
- }
- if (currentBalance < language.price) {
- return {
- filter: "grayscale(100%) brightness(0.5)",
- cursor: "not-allowed",
- };
- }
- return {
- filter: "none",
- cursor: "pointer",
- };
- };
-
- return (
- !disabled && onPurchase(language)}
- style={{
- position: "relative",
- transition: "all 0.15s ease-in-out",
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- ...getButtonStyle(),
- }}
- onMouseEnter={(e) => {
- if (!disabled) {
- e.currentTarget.style.transform = "scale(1.05)";
- }
- }}
- onMouseLeave={(e) => {
- if (!disabled) {
- e.currentTarget.style.transform = "scale(1)";
- }
- }}
- >
-
- {/* Button text overlay */}
-
- {getButtonText()}
-
-
- );
-};
-
-// Language List Panel Component
-interface LanguageListPanelProps {
- languages: Language[];
- selectedLanguage: Language | null;
- onLanguageSelect: (language: Language) => void;
-}
-
-const LanguageListPanel: React.FC = ({
- languages,
- selectedLanguage,
- onLanguageSelect,
-}) => (
-
-
- Programming Languages
-
-
-
- {languages.map((language) => (
-
- ))}
-
-
-);
-
-// Language Details Panel Component
-interface LanguageDetailsPanelProps {
- selectedLanguage: Language | null;
- onPurchase: (language: Language) => void;
- isPurchasing: boolean;
- currentBalance: number;
-}
-
-const LanguageDetailsPanel: React.FC = ({
- selectedLanguage,
- onPurchase,
- isPurchasing,
- currentBalance,
-}) => {
- if (!selectedLanguage) {
- return (
-
- 언어를 선택해주세요
-
- );
- }
-
- return (
-
- {/* Character Image */}
-
-
-
-
- {/* Price Display */}
-
- ★
- {selectedLanguage.price.toLocaleString()}
-
-
- {/* Description */}
-
- {selectedLanguage.description}
-
-
- {/* Purchase Button */}
-
-
- );
-};
-
-// Main Language Store Page Component
-const LanguageStorePage: React.FC = () => {
- const navigate = useNavigate();
-
- // Component state
- const [selectedLanguage, setSelectedLanguage] = useState(
- languages[0]
- );
- const [isPurchasing, setIsPurchasing] = useState(false);
- const [purchaseError, setPurchaseError] = useState(null);
-
- // Header modal states
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
-
- // Mock user balance (in real app, this would come from Apollo Client)
- const currentBalance = 3817;
-
- // Purchase handlers
- const handlePurchase = async (language: Language) => {
- if (language.isOwned) {
- return; // Already owned
- }
-
- if (currentBalance < language.price) {
- setPurchaseError("잔액이 부족합니다.");
- return;
- }
-
- setIsPurchasing(true);
- setPurchaseError(null);
-
- try {
- // Simulate purchase API call
- await new Promise((resolve) => setTimeout(resolve, 1000));
-
- // Update local state
- setSelectedLanguage((prev) => (prev ? { ...prev, isOwned: true } : null));
-
- // In real app, update Apollo Client cache
- console.log(
- `Purchased ${language.displayName} for ${language.price} stars`
- );
- } catch (error) {
- setPurchaseError("구매에 실패했습니다. 다시 시도해주세요.");
- } finally {
- setIsPurchasing(false);
- }
- };
-
- // Navigation handlers
- const handleClose = () => {
- navigate("/main");
- };
-
- const handleLanguageSelect = (language: Language) => {
- setSelectedLanguage(language);
- setPurchaseError(null);
- };
-
- return (
-
- {/* Header Modals */}
- {showTutoModal && (
-
- setShowTutoModal(false)} />
-
- )}
- {showSettingModal && (
- setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
- {/* Header - absolute positioned */}
-
- setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- onShowRanking={() => setShowRankingModal(true)}
- />
-
-
- {/* Language Store Modal */}
-
- {/* Modal Header */}
-
-
- Store
-
- {
- e.currentTarget.style.transform = "scale(1.1)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.transform = "scale(1)";
- }}
- >
-
-
-
-
- {/* Two Panel Layout */}
-
- {/* Language List Panel */}
-
-
- {/* Language Details Panel */}
-
-
-
- {/* Error Display */}
- {purchaseError && (
-
- {purchaseError}
-
- )}
-
-
- {/* CSS Animations */}
-
-
- );
-};
-
-export default LanguageStorePage;
diff --git a/src/pages/main/GameLobbyPage.tsx b/src/pages/main/GameLobbyPage.tsx
deleted file mode 100644
index 1a9f7ed..0000000
--- a/src/pages/main/GameLobbyPage.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-// @ts-nocheck
-import singleBg from "@/assets/images/single_background.jpg";
-import { Box } from "../../../styled-system/jsx";
-import Header from "../../components/common/Header";
-import { useNavigate } from "react-router-dom";
-import { useState } from "react";
-import boardBox from "@/assets/images/board1.jpg";
-import javaBtnImg from "@/assets/images/java_button.png";
-import pythonBtnImg from "@/assets/images/python_button.png";
-import jsBtnImg from "@/assets/images/js_button.png";
-import cancelBtn from "@/assets/images/cancel_btn.png";
-import goBtnImg from "@/assets/images/go_button.png";
-import sqlBtnImg from "@/assets/images/SQL_button.png";
-import lockIcon from "@/assets/images/lock_icon.png";
-
-const LANGUAGES = [
- { key: "JAVA", label: "JAVA", enabled: true },
- { key: "PYTHON", label: "PYTHON", enabled: true },
- { key: "SQL", label: "SQL", enabled: true },
- { key: "JS", label: "JS", enabled: true },
- { key: "GO", label: "GO", enabled: false },
-];
-
-const buttonBaseStyle: React.CSSProperties = {
- minWidth: "7.5rem",
- padding: "0.625rem 1rem",
- borderRadius: "0.5rem",
- border: "1px solid rgba(255,255,255,0.25)",
- background:
- "linear-gradient(180deg, rgba(255,255,255,0.15), rgba(255,255,255,0.05))",
- color: "#fff",
- fontWeight: 700,
- letterSpacing: "0.5px",
- cursor: "pointer",
- transition: "transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease",
- boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
-};
-
-function buttonHover(e: React.MouseEvent) {
- e.currentTarget.style.transform = "translateY(-1px) scale(1.03)";
- e.currentTarget.style.boxShadow = "0 12px 28px rgba(0,255,255,0.25)";
-}
-function buttonLeave(e: React.MouseEvent) {
- e.currentTarget.style.transform = "none";
- e.currentTarget.style.boxShadow = "0 8px 24px rgba(0,0,0,0.25)";
-}
-
-const GameLobbyPage = () => {
- const navigate = useNavigate();
- const [showSelect, setShowSelect] = useState(true);
-
- const handleSelect = (lang: string) => {
- // Navigate directly into the single game route for the chosen language
- navigate(`/single/game/${encodeURIComponent(lang.toLowerCase())}`);
- };
-
- return (
-
-
-
-
-
- {showSelect && (
-
-
- 언어선택
-
-
- {/* Row 1: JAVA | PYTHON | SQL */}
-
-
handleSelect("JAVA")}
- >
-
-
-
handleSelect("PYTHON")}
- >
-
-
-
handleSelect("SQL")}
- >
-
-
-
-
- {/* Row 2: JS | GO */}
-
-
handleSelect("JS")}
- >
-
-
-
navigate("/language")}
- >
-
-
-
-
-
-
-
- {/* Cancel Button */}
-
-
navigate("/main")}
- >
-
-
-
-
- )}
-
- );
-};
-
-export default GameLobbyPage;
diff --git a/src/pages/main/LandingPage.tsx b/src/pages/main/LandingPage.tsx
deleted file mode 100644
index c741863..0000000
--- a/src/pages/main/LandingPage.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-// @ts-nocheck
-import { useNavigate } from "react-router-dom";
-import logoImage from "../../assets/images/codenova_logo.png";
-import signupButton from "../../assets/images/signup_button.png";
-import loginButton from "../../assets/images/login_button.png";
-import multibg from "../../assets/images/multi_background.png";
-import guestButton from "../../assets/images/guest_login.png";
-import { guestLoginApi } from "../../api/authApi";
-import useAuthStore from "../../store/authStore";
-import { useEffect } from "react";
-
-const LandingPage = () => {
- const navigate = useNavigate();
- const login = useAuthStore((state) => state.login);
- const token = useAuthStore((state) => state.token);
- useEffect(() => {
- if (token) {
- navigate("/main");
- }
- }, [token, navigate]);
- const handleGuestLogin = async () => {
- try {
- const res = await guestLoginApi();
- const accessToken = res.headers["authorization"]?.split(" ")[1];
- const { nickname, userType } = res.data.content;
-
- if (!accessToken) {
- alert("잘못된 접근 방식입니다!");
- return;
- }
-
- document.cookie = `accessToken=${accessToken}; path=/; max-age=86400;`;
-
- login({
- nickname,
- token: accessToken,
- userType: userType || "guest",
- });
-
- navigate("/main");
- } catch (err) {
- console.error(err);
- alert("비회원 로그인 실패!");
- }
- };
-
- return (
-
- {/* 로고 */}
-
-
- {/* 버튼 영역 */}
-
-
navigate("/auth/signup")}
- />
-
navigate("/auth/login")}
- />
-
-
-
- {/* 문구 영역 */}
-
-
- Code like a supernova
-
-
- Fast ,{" "}
- bright ,{" "}
- unstoppable
-
-
-
- {/* Dev quick links for new pages (temporary) */}
-
-
- navigate(
- "/store/purchase-failed?code=CARD_DECLINED&message=카드가 승인되지 않았습니다."
- )
- }
- >
- 결제 실패 페이지
-
-
- navigate("/account/find-account/confirm", {
- state: { maskedEmail: "abc***@mail.com" },
- })
- }
- >
- 아이디 찾기 확인
-
-
- navigate("/game/result", {
- state: { score: 9876, rank: 12, isNewRecord: true },
- })
- }
- >
- 게임 결과 페이지
-
-
-
- );
-};
-
-export default LandingPage;
diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx
deleted file mode 100644
index 59f5539..0000000
--- a/src/pages/main/MainPage.tsx
+++ /dev/null
@@ -1,406 +0,0 @@
-// @ts-nocheck
-import multibg from "@/assets/images/multi_background.png";
-import logoImage from "@/assets/images/codenova_logo.png";
-import boardImage from "@/assets/images/board2.jpg";
-import singleBtn from "@/assets/images/single_button.png";
-import multiBtn from "@/assets/images/multi_button.png";
-import makeRoomBtn from "@/assets/images/make_room_button.png";
-import goRoomBtn from "@/assets/images/go_game_button.png";
-import randomBtn from "@/assets/images/gorandom_button.png";
-import { Player } from "@lottiefiles/react-lottie-player";
-import battleLottie from "@/assets/lottie/battle.json";
-import defendLottie from "@/assets/lottie/defend.json";
-import Header from "../../components/common/Header.tsx";
-import RoomCodeModal from "../../components/modal/RoomCodeModal";
-import SettingModal from "../../components/modal/SettingModal";
-import TutoModal from "../../components/common/TutoModal";
-import RankingModal from "../../components/modal/RankingModal";
-import useAuthStore from "@/store/authStore";
-import PatchNoteModal from "../../components/PatchNoteModal";
-import { Box } from "../../../styled-system/jsx";
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-
-const PATCH_VERSION = "1.0.3";
-
-const MainPage = () => {
- const navigate = useNavigate();
- const [showRoomModal, setShowRoomModal] = useState(false);
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
- const [showPatchNote, setShowPatchNote] = useState(false);
- const nickname = useAuthStore((state) => state.user?.nickname);
-
- useEffect(() => {
- // Avoid auto-opening patch notes during development/e2e to prevent UI blocking in tests
- if (import.meta.env.PROD) {
- const lastSeenVersion = localStorage.getItem("codenova_patch_note");
- if (lastSeenVersion !== PATCH_VERSION) {
- setShowPatchNote(true);
- }
- }
- }, []);
-
- const handleClosePatchNote = () => {
- localStorage.setItem("codenova_patch_note", PATCH_VERSION);
- setShowPatchNote(false);
- };
-
- return (
-
- {showPatchNote && }
-
- {/* Header - absolute positioned */}
-
- setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- onShowRanking={() => setShowRankingModal(true)}
- />
-
-
- {/* Main content stack (below header; header already renders logo and options) */}
-
- {/* Logo is handled by Header (top-left) */}
-
- {/* Game Modes Container */}
-
- {/* Battle Board */}
-
-
-
- 배틀모드
-
-
- 최강 개발자를 가려라!
-
-
-
navigate("/single/select/language")}
- />
-
-
-
-
- {/* Monthly Ranking Panel */}
-
-
- 월간 종합 순위
-
-
- {/* 2nd */}
-
-
-
- Brian Ngo
-
-
-
- 50,000 ₩
-
-
- Prize
-
-
-
- {/* 1st */}
-
-
-
- Jolie Joie
-
-
-
- 100,000 ₩
-
-
- Prize
-
-
-
- {/* 3rd */}
-
-
-
- David Do
-
-
-
- 20,000 ₩
-
-
- Prize
-
-
-
-
-
- {/* Coin badge managed by Header */}
-
-
-
-
- {showRoomModal && (
- setShowRoomModal(false)} />
- )}
- {showTutoModal && setShowTutoModal(false)} />}
- {showSettingModal && (
- setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
- );
-};
-
-export default MainPage;
diff --git a/src/pages/main/__tests__/GameLobbyPage.test.tsx b/src/pages/main/__tests__/GameLobbyPage.test.tsx
deleted file mode 100644
index d332090..0000000
--- a/src/pages/main/__tests__/GameLobbyPage.test.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { render, screen, fireEvent } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { vi, describe, it, expect } from "vitest";
-import { MemoryRouter, useNavigate } from "react-router-dom";
-import GameLobbyPage from "../GameLobbyPage";
-
-vi.mock("react-router-dom", async (orig) => {
- const actual = await orig();
- return {
- ...actual,
- useNavigate: () => vi.fn(),
- };
-});
-
-describe("GameLobbyPage", () => {
- it("renders header and title", () => {
- render(
-
-
-
- );
-
- // Logo is present as a button with alt="Logo"
- expect(screen.getByRole("button", { name: /Logo/i })).toBeInTheDocument();
- expect(
- screen.getByRole("heading", { name: "언어선택" })
- ).toBeInTheDocument();
- });
-
- it("has a JAVA button", () => {
- render(
-
-
-
- );
-
- expect(
- screen.getByRole("button", { name: /select java/i })
- ).toBeInTheDocument();
- });
-});
diff --git a/src/pages/meteo/FallingWord.tsx b/src/pages/meteo/FallingWord.tsx
deleted file mode 100644
index 188568f..0000000
--- a/src/pages/meteo/FallingWord.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-// @ts-nocheck
-import { useEffect, useRef, useState } from "react";
-
-export default function FallingWord({
- word,
- duration,
- left,
- groundY,
- spawnTime,
- onEnd,
-}) {
- const [y, setY] = useState(0);
- const rafRef = useRef();
-
- useEffect(() => {
- const update = () => {
- const now = Date.now();
- const elapsed = now - spawnTime;
- const progress = Math.min(elapsed / duration, 1);
- const FALL_END_OFFSET = 140;
- const nextY = progress * (groundY - FALL_END_OFFSET);
- // console.log(`[${word}] elapsed: ${elapsed}ms, progress: ${progress.toFixed(2)}, y: ${nextY.toFixed(1)}`);
-
- if (elapsed >= duration) {
- setY(groundY);
- onEnd();
- return;
- }
-
- setY(nextY);
- rafRef.current = requestAnimationFrame(update);
- };
-
- update();
- return () => cancelAnimationFrame(rafRef.current);
- }, [spawnTime, duration, groundY, onEnd]);
-
- return (
-
- {word}
-
- );
-}
diff --git a/src/pages/meteo/MeteoGamePage.tsx b/src/pages/meteo/MeteoGamePage.tsx
deleted file mode 100644
index 791f348..0000000
--- a/src/pages/meteo/MeteoGamePage.tsx
+++ /dev/null
@@ -1,597 +0,0 @@
-// @ts-nocheck
-import React, { useState, useEffect, useRef } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
-import { Player } from "@lottiefiles/react-lottie-player";
-import MeteoGameBg from "../../assets/images/meteo_game_bg.png";
-import typingLottie from "../../assets/lottie/typing.json";
-import EndGameBtn from "../../assets/images/end_game_button.png";
-import redHeart from "../../assets/images/red_heart.png";
-import blackHeart from "../../assets/images/black_heart.png";
-import bgm from "../../assets/sound/meteoBGM.mp3";
-import gameOverLottie from "../../assets/lottie/game_over.json";
-import victoryLottie from "../../assets/lottie/victory.json";
-import explosionLottie from "../../assets/lottie/explosion.json";
-import FallingWord from "./FallingWord";
-import ConfirmModal from "../../components/modal/ConfirmModal";
-import GameResultModal from "../../components/modal/GameResultModal";
-import useAuthStore from "../../store/authStore";
-import useVolumeStore from "../../store/useVolumsStore";
-// Sockets removed in this FE-only build; provide safe no-op shims
-const getSocket = () => ({ on: () => {}, off: () => {}, id: "" });
-const exitGame = () => {};
-const onUserInput = () => {};
-const onCheckText = () => {};
-const onCheckTextResponse = () => {};
-const onGameEnd = () => {};
-const onRemoveHeartResponse = () => {};
-const onUserInputResponse = () => {};
-const offUserInput = () => {};
-const goWaitingRoom = () => {};
-const offGoWaitingRoom = () => {};
-const onGoWaitingRoom = () => {};
-
-const MeteoGamePage = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const gameData = location.state;
- const nickname = useAuthStore((state) => state.user?.nickname);
- const { roomId, players } = gameData || {};
- const [gameResult, setGameResult] = useState(null); // null이면 모달 안 띄움
- const [lifesLeft, setLifesLeft] = useState(5);
- const [userInputTexts, setUserInputTexts] = useState({});
- const currentRoomId = localStorage.getItem("meteoRoomId");
- const [showGameOver, setShowGameOver] = useState(false);
- const [showVictory, setShowVictory] = useState(false);
-
- const { bgmVolume } = useVolumeStore();
- const audioRef = useRef(null);
-
- // input 포커싱
- const inputRef = useRef(null);
- useEffect(() => {
- inputRef.current?.focus(); // 3. 페이지 진입 시 포커스
-
- const audio = new Audio(bgm);
- audio.loop = true;
- audio.volume = bgmVolume;
- audio.play().catch((e) => {
- // 4. 자동 재생 시도 + 차단 시 경고 출력
- // console.warn("⚠️ 자동 재생 차단됨:", e);
- });
- audioRef.current = audio;
-
- return () => {
- audio.pause(); // 5. 페이지 벗어날 때 음악 멈춤
- audio.currentTime = 0; // 6. 재생 위치를 처음으로 초기화
- };
- }, []);
-
- useEffect(() => {
- if (audioRef.current) {
- audioRef.current.volume = bgmVolume;
- }
- }, [bgmVolume]);
-
- // 닉네임 매핑
- const [playerList, setPlayerList] = useState(
- players?.map((p) => p.nickname) || []
- );
-
- const [input, setInput] = useState("");
- const [fallingWords, setFallingWords] = useState([]);
- const wordsRef = useRef(fallingWords);
- wordsRef.current = fallingWords;
-
- // 플레이어 애니메이션 컨테이너 ref로 땅 위치 계산
- const playersRef = useRef(null);
- const [groundY, setGroundY] = useState(window.innerHeight);
-
- // 모달
- const [showExitModal, setShowExitModal] = useState(false);
- const [leaveMessages, setLeaveMessages] = useState([]);
-
- useEffect(() => {
- const handleBeforeUnloadOrPop = () => {
- const savedRoomId = localStorage.getItem("meteoRoomId");
- const savedNickname = localStorage.getItem("nickname");
- // console.log("🔥 [뒤로가기 또는 새로고침] 방 나감 처리", savedRoomId, savedNickname);
- if (savedRoomId && savedNickname) {
- // console.log("🚪 [뒤로가기 또는 새로고침] 방 나감 처리");
- exitGame({ roomId: savedRoomId, nickname: savedNickname });
-
- // localStorage.removeItem("meteoRoomCode");
- // localStorage.removeItem("meteoRoomId");
- }
- };
-
- // 뒤로가기, 새로고침 등 감지
- window.addEventListener("beforeunload", handleBeforeUnloadOrPop);
- return () => {
- window.removeEventListener("beforeunload", handleBeforeUnloadOrPop);
- };
- }, []);
- useEffect(() => {
- const handlePopState = (event) => {
- // 브라우저 alert 사용 (콘솔이 안 보일때도 확인 가능)
-
- alert("게임을 나가시겠습니까?");
-
- const savedNickname = nickname;
-
- if (currentRoomId && savedNickname) {
- exitGame({ roomId: roomId, nickname: nickname });
- // console.log("🚪 [뒤로가기] 방 나감 처리 시작");
- }
- };
-
- // 현재 history 상태 저장
- window.history.pushState({ page: "meteo" }, "", window.location.pathname);
-
- // 이벤트 리스너 등록
- window.addEventListener("popstate", handlePopState);
-
- return () => {
- window.removeEventListener("popstate", handlePopState);
- };
- }, [nickname]);
-
- useEffect(() => {
- const calcGround = () => {
- if (playersRef.current) {
- const rect = playersRef.current.getBoundingClientRect();
- setGroundY(rect.bottom);
- }
- };
- calcGround();
- window.addEventListener("resize", calcGround);
- return () => window.removeEventListener("resize", calcGround);
- }, []);
-
- // 새로고침 시 잘못된 접근이면 메인으로
- useEffect(() => {
- const socket = getSocket();
- if (!gameData || !roomId || !players?.length || !socket) {
- navigate("/main");
- }
- }, [gameData, roomId, players, navigate]);
-
- // 서버에서 단어 떨어뜨리기 이벤트 한 번만 등록
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleWordFalling = ({ word, fallDuration, timestamp }) => {
- const id = Date.now() + Math.random();
-
- let parsedTime = new Date(timestamp.replace(" ", "T")).getTime();
- const now = Date.now();
- if (now - parsedTime > 5000 || now - parsedTime < -1000) {
- // console.warn("🚨 spawnTime 보정됨:", timestamp, `(Δ ${now - parsedTime}ms)`);
- parsedTime = now;
- }
- const spawnTime = parsedTime;
- // console.log("[타이밍 확인] now:", now, "spawnTime:", spawnTime, "Δ:", now - spawnTime);
- // console.log("[wordFalling] word:", word, "fallDuration:", fallDuration, "timestamp:", timestamp);
-
- setFallingWords((prev) => {
- const existing = prev.map((w) => w.left);
-
- let leftPercent;
- for (let i = 0; i < 5; i++) {
- const candidate = Math.random() * 80 + 10;
- if (!existing.some((x) => Math.abs(x - candidate) < 15)) {
- leftPercent = candidate;
- break;
- }
- }
- if (leftPercent == null) leftPercent = Math.random() * 80 + 10;
-
- return [
- ...prev,
- { id, word, fallDuration, spawnTime, left: leftPercent },
- ];
- });
- };
-
- socket.off("wordFalling", handleWordFalling);
- socket.on("wordFalling", handleWordFalling);
- return () => socket.off("wordFalling", handleWordFalling);
- }, []);
-
- // 단어가 바닥에 닿으면 목록에서 제거
- const handleWordEnd = (id) => {
- setFallingWords((prev) => prev.filter((w) => w.id !== id));
- };
-
- // 게임 도중 나가기
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- // 1. 정상 종료 처리
- const handleLeave = (data) => {
- const { leftUser, currentPlayers } = data;
- console.log("[handleLeave] 정상 종료 처리", data);
- if (leftUser.nickname === localStorage.getItem("nickname")) {
- localStorage.removeItem("roomId");
- localStorage.removeItem("roomCode");
- navigate("/main");
- } else {
- setPlayerList(currentPlayers.map((p) => p.nickname));
- const id = Date.now() + Math.random();
- setLeaveMessages((prev) => [
- ...prev,
- { id, text: `${leftUser.nickname} 님이 게임을 나갔습니다.` },
- ]);
- setTimeout(() => {
- setLeaveMessages((prev) => prev.filter((msg) => msg.id !== id));
- }, 3000);
- }
- };
- socket.off("gameLeave", handleLeave);
- socket.on("gameLeave", handleLeave);
-
- // 2. 비정상 종료 처리
- const handleGameLeave = (data) => {
- const { leftUser, currentPlayers } = data;
-
- if (leftUser.nickname === localStorage.getItem("nickname")) {
- localStorage.removeItem("roomId");
- localStorage.removeItem("roomCode");
- navigate("/main");
- } else {
- setPlayerList(currentPlayers.map((p) => p.nickname));
- const id = Date.now() + Math.random();
- setLeaveMessages((prev) => [
- ...prev,
- { id, text: `${leftUser.nickname} 님이 게임을 나갔습니다.` },
- ]);
- setTimeout(() => {
- setLeaveMessages((prev) => prev.filter((msg) => msg.id !== id));
- }, 3000);
- }
- };
- socket.off("playerDisconnected", handleGameLeave);
- socket.on("playerDisconnected", handleGameLeave);
-
- return () => {
- socket.off("gameLeave", handleLeave);
- socket.off("playerDisconnected", handleGameLeave);
- };
- }, []);
-
- const handleKeyDown = (e) => {
- if (e.key === "Enter") {
- const text = input.trim();
- if (!text) return;
-
- const roomId = localStorage.getItem("roomId");
- const nickname = localStorage.getItem("nickname");
-
- // 서버로 입력 단어 전송
- onUserInput({ roomId, nickname, text });
- onCheckText({ roomId, nickname, text });
- setInput(""); // 입력창 초기화
- setUserInputTexts((prev) => ({ ...prev, [nickname]: "" }));
- }
- };
-
- useEffect(() => {
- const handleTextCheck = (data) => {
- // console.log("[onCheckTextResponse] 수신:", data);
- const { text, correct } = data;
-
- if (correct) {
- // 정답이면 해당 단어 제거
- setFallingWords((prev) =>
- prev.filter((wordObj) => wordObj.word !== text)
- );
- }
- // 오답이면 아무 처리 안함
- };
-
- onCheckTextResponse(handleTextCheck);
-
- // 클린업
- return () => getSocket().off("textCheck", handleTextCheck);
- }, []);
-
- useEffect(() => {
- const handleGameEnd = (data) => {
- if (!data.success) {
- setShowGameOver(true); // 1) game over 애니메이션 표시
- setTimeout(() => {
- setShowGameOver(false); // 2) 애니메이션 숨기고 모달 표시
- setGameResult(data);
- }, 3000); // 애니메이션 보여줄 시간(ms)
- } else {
- setShowVictory(true);
- setTimeout(() => {
- setShowVictory(false);
- setGameResult(data);
- }, 3000);
- }
- };
-
- onGameEnd(handleGameEnd);
- return () => getSocket().off("gameEnd", handleGameEnd);
- }, []);
-
- const [explosionIndex, setExplosionIndex] = useState(null); // 몇 번째 하트 폭발?
- const [isShaking, setIsShaking] = useState(false); // 화면 흔들림 트리거
-
- useEffect(() => {
- const handleLostLife = (data) => {
- const newLifesLeft = data.lifesLeft;
- const explodingIndex = newLifesLeft; // 5 → 4면 index 4가 터지는 거
-
- setExplosionIndex(explodingIndex); // 먼저 폭발 위치 지정
- setIsShaking(true);
-
- setTimeout(() => {
- setExplosionIndex(null); // 폭발 끝
- setLifesLeft(newLifesLeft); // 이제 하트 줄이기
- setIsShaking(false);
- }, 1000); // 1초 뒤에 하트 줄이기
- };
-
- onRemoveHeartResponse(handleLostLife);
-
- return () => {
- getSocket().off("lostLife", handleLostLife);
- };
- }, []);
-
- useEffect(() => {
- onUserInputResponse(({ nickname, text }) => {
- // console.log("[onUserInputResponse] 수신:", nickname, text);
- setUserInputTexts((prev) => ({ ...prev, [nickname]: text }));
- });
-
- return () => {
- offUserInput();
- };
- }, []);
-
- useEffect(() => {
- const handleGoWaitingRoom = (data) => {
- const myNickname = localStorage.getItem("nickname");
-
- const isMeIncluded = data.players.some(
- (player) => player.nickname === myNickname
- );
-
- if (!isMeIncluded) {
- console.warn("❗ 내 닉네임이 포함되지 않음 → 대기방 이동 안 함");
- return;
- }
-
- console.log("✅ [내 포함됨] waitingRoomGo 수신 → 대기방 이동", data);
- navigate("/meteo/landing", { state: data });
- };
-
- onGoWaitingRoom(handleGoWaitingRoom);
- return () => {
- offGoWaitingRoom();
- };
- }, [navigate]);
-
- return (
-
-
setShowExitModal(true)}
- />
-
- {fallingWords.map(({ id, word, fallDuration, left, spawnTime }) => (
- handleWordEnd(id)}
- />
- ))}
-
-
-
- {playerList.map((nickname, idx) => {
- const myNickname = localStorage.getItem("nickname");
- const isMe = nickname === myNickname;
-
- return (
-
-
-
-
-
- {isMe ? `${nickname}` : nickname}
-
-
-
- {userInputTexts[nickname]?.length > 6
- ? userInputTexts[nickname].slice(0, 6) + "..."
- : userInputTexts[nickname] || ""}
-
-
-
- );
- })}
-
-
- {/* 입력창 */}
-
- {
- const value = e.target.value;
- setInput(value);
-
- const roomId = localStorage.getItem("roomId");
- const nickname = localStorage.getItem("nickname");
- onUserInput({ roomId, nickname, text: value });
- }}
- onKeyDown={handleKeyDown}
- placeholder="단어를 입력하세요..."
- className="w-[20rem] z-50 h-14 text-xl text-center font-bold text-black bg-[#f0f0f0] border-[3px] border-[#3a3a3a] rounded-lg shadow-md outline-none focus:ring-2 focus:ring-pink-300"
- style={{ fontFamily: "pixel, sans-serif" }}
- />
-
-
- {/* 떠난 유저 알림 메시지 */}
-
- {leaveMessages.map((msg) => (
-
- {msg.text}
-
- ))}
-
-
- {/* 목숨 하트 UI (오른쪽 상단) */}
-
- {Array(5)
- .fill(0)
- .map((_, idx) => {
- const isExploding = explosionIndex === idx;
- const isAlive = idx < lifesLeft;
-
- return (
-
-
- {isExploding && (
-
- )}
-
- );
- })}
-
-
- {showExitModal && (
-
{
- const roomId = localStorage.getItem("roomId");
- const nickname = localStorage.getItem("nickname");
- // console.log("roomId:", roomId); // null이면 문제 있음
- // console.log("nickname:", nickname); // null이면 문제 있음
- // console.log("🔥 exit 요청할 roomId / nickname:", roomId, nickname);
-
- exitGame({ roomId, nickname });
-
- localStorage.removeItem("roomId");
- localStorage.removeItem("roomCode");
- navigate("/main");
- }}
- onCancel={() => setShowExitModal(false)}
- />
- )}
-
- {gameResult && (
- {
- const roomId = localStorage.getItem("roomId");
- const nickname = localStorage.getItem("nickname");
- // console.log("🟨 [GameResultModal 종료] onExit 실행", { roomId, nickname });
- // if (!roomId || !nickname) {
- // console.warn("❗ roomId 또는 nickname 누락 → 강제 메인 이동");
- // navigate("/main");
- // return;
- // }
- exitGame({ roomId, nickname });
- localStorage.removeItem("roomId");
- localStorage.removeItem("roomCode");
- navigate("/main");
- }}
- onRetry={() => {
- const roomId = localStorage.getItem("roomId");
- const nickname = localStorage.getItem("nickname");
- const roomCode = localStorage.getItem("meteoRoomCode");
-
- if (!roomId || !nickname) {
- console.warn("❗ roomId 또는 nickname 누락 → 강제 메인 이동");
- navigate("/main");
- return;
- }
-
- // meteoRoomCode가 사라졌다면 다시 복원
- if (!roomCode && gameData?.roomCode) {
- localStorage.setItem("meteoRoomCode", gameData.roomCode);
- console.log("✅ meteoRoomCode 복원:", gameData.roomCode);
- }
-
- goWaitingRoom({ nickname, roomId });
- }}
- />
- )}
-
- {showGameOver && (
-
- )}
-
- {showVictory && (
-
- )}
-
- );
-};
-
-export default MeteoGamePage;
diff --git a/src/pages/meteo/MeteoLandingPage.tsx b/src/pages/meteo/MeteoLandingPage.tsx
deleted file mode 100644
index 7fb863a..0000000
--- a/src/pages/meteo/MeteoLandingPage.tsx
+++ /dev/null
@@ -1,779 +0,0 @@
-// @ts-nocheck
-import React, { useEffect, useRef, useState } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
-import MeteoBg from "../../assets/images/meteo_bg.png";
-import MeteoBoard from "../../assets/images/board1.jpg";
-import UserBoard from "../../assets/images/board2.jpg";
-import Profile1 from "../../assets/images/profile1.png";
-import Profile2 from "../../assets/images/profile2.png";
-import Profile3 from "../../assets/images/profile3.png";
-import Profile4 from "../../assets/images/profile4.png";
-import Ready from "../../assets/images/multi_ready_btn.png";
-import Unready from "../../assets/images/multi_unready_btn.png";
-import Crown from "../../assets/images/crown_icon.png";
-import StartButton from "../../assets/images/start_btn.png";
-import WaitButton from "../../assets/images/wait_btn.png";
-import ExitButton from "../../assets/images/multi_exit_btn.png";
-import CopyButton from "../../assets/images/multi_copy_icon.png";
-import Header from "../../components/common/Header";
-import CustomAlert from "../../components/common/CustomAlert";
-import useAuthStore from "../../store/authStore";
-// Sockets removed in this FE-only build; provide safe no-op shims
-const getSocket = () => ({ on: () => {}, off: () => {}, id: "" });
-const exitMeteoRoom = () => {};
-const GameReady = () => {};
-const onGameReady = () => {};
-const onMeteoGameStart = () => {};
-const offMeteoGameStart = () => {};
-const onRoomExit = () => {};
-const offRoomExit = () => {};
-const startMeteoGame = () => {};
-const onGoWaitingRoom = () => {};
-const offGoWaitingRoom = () => {};
-const onExitMeteoGame = () => {};
-const onKick = () => {};
-const onReadyWarning = () => {};
-const offReadyWarning = () => {};
-const offKick = () => {};
-const onHostKickWarning = () => {};
-const offHostKickWarning = () => {};
-const onChatMessage = () => {};
-
-const MeteoLandingPage = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const { roomCode, roomId, players } = location.state || {};
- const [users, setUsers] = useState([null, null, null, null]);
- const profileImages = [Profile1, Profile2, Profile3, Profile4];
- const nickname = useAuthStore((state) => state.user?.nickname);
- const [messages, setMessages] = useState([]);
- const [chatInput, setChatInput] = useState("");
- const scrollRef = useRef(null);
- const currentRoomId = localStorage.getItem("meteoRoomId");
- const currentRoomCode = localStorage.getItem("meteoRoomCode");
- const [countdown, setCountdown] = useState(null);
- const [showReadyAlert, setShowReadyAlert] = useState(false);
- const readyUsers = users.filter((user) => user && user.ready);
- const totalUsers = users.filter((user) => user !== null);
- // const allReady = totalUsers.length >= 2 && readyUsers.length === totalUsers.length;
- const [alertMessage, setAlertMessage] = useState("");
- const [showAlert, setShowAlert] = useState(false);
- const [showKickAlert, setShowKickAlert] = useState(false);
- const [kickMessage, setKickMessage] = useState("");
-
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(roomCode);
- setAlertMessage("방 코드가 복사되었습니다.");
- setShowAlert(true);
- } catch (err) {
- setAlertMessage("복사에 실패했습니다.");
- setShowAlert(true);
- }
- };
-
- useEffect(() => {
- const socket = getSocket();
-
- const handleGameReady = (data) => {
- // console.log("[onGameReady] ready 수신", { data });
- updateUsersFromPlayers(data.players);
- localStorage.setItem("meteoPlayers", JSON.stringify(data.players));
- };
-
- onGameReady(handleGameReady);
- return () => socket.off("readyGame", handleGameReady);
- }, []);
-
- const [allReady, setAllReady] = useState(false);
-
- const updateUsersFromPlayers = (playersArray) => {
- if (!Array.isArray(playersArray)) return;
-
- const updated = Array(4).fill(null);
- let computedAllReady = true;
- let realPlayerCount = 0;
-
- playersArray.forEach((player, idx) => {
- if (idx < 4) {
- const isHost = player.isHost || false;
- const isReady = isHost ? true : player.isReady || false;
-
- if (player.nickname) realPlayerCount++;
- if (!isReady) computedAllReady = false;
-
- updated[idx] = {
- nickname: player.nickname,
- isHost,
- ready: isReady,
- };
- }
- });
-
- // 두 명 이상이어야 allReady 인정
- const allReady = computedAllReady && realPlayerCount >= 2;
-
- setUsers(updated);
- setAllReady(allReady);
-
- if (allReady) {
- setMessages((prev) => {
- const exists = prev.some((msg) =>
- msg.message.includes("모든 플레이어가 준비되었습니다")
- );
- return exists
- ? prev
- : [
- ...prev,
- {
- nickname: "SYSTEM",
- message:
- "모든 플레이어가 준비되었습니다. 방장님은 20초 내에 게임을 시작해주세요!",
- },
- ];
- });
- setShowReadyAlert(true);
- setTimeout(() => setShowReadyAlert(false), 4000);
- }
- };
-
- // 1) 방 정보 저장 전용 useEffect
- useEffect(() => {
- if (players && players.length > 0) {
- // 화면에 유저 세팅
- updateUsersFromPlayers(players);
-
- // 로컬스토리지에 방 정보 저장
- localStorage.setItem("meteoRoomCode", roomCode);
- localStorage.setItem("meteoRoomId", roomId);
- localStorage.setItem("meteoPlayers", JSON.stringify(players));
-
- // console.log("✅ [방 생성/입장] localStorage 저장 완료");
- }
- }, [players, roomCode, roomId]);
-
- const usersRef = useRef(users);
- useEffect(() => {
- usersRef.current = users;
- }, [users]);
-
- // 2) guard + socket 이벤트 관리
- useEffect(() => {
- const socket = getSocket();
-
- // 1) guard: localStorage 기반
- const savedId = localStorage.getItem("meteoRoomId");
- const savedPlayers = JSON.parse(
- localStorage.getItem("meteoPlayers") || "[]"
- );
- if (!savedId || savedPlayers.length === 0 || !nickname || !socket) {
- // console.warn("❗ 방 정보 없음 또는 소켓 없음 → 메인으로 이동");
-
- // localStorage 정리
- localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- localStorage.removeItem("meteoPlayers");
-
- // 메인으로 리다이렉트
- navigate("/main");
- return;
- }
-
- // 2) guard 통과 후 서버 이벤트 등록
- const handleSecretRoomJoin = (roomData) => {
- updateUsersFromPlayers(roomData.players);
- localStorage.setItem("meteoPlayers", JSON.stringify(roomData.players));
- // ✅ 2. SYSTEM 메시지 추가
- const prevCount = usersRef.current.filter((u) => u !== null).length;
- const newCount = roomData.players.length;
-
- if (newCount > prevCount) {
- const joined = roomData.players[newCount - 1];
- if (joined?.nickname) {
- setMessages((prev) => [
- ...prev,
- {
- nickname: "SYSTEM",
- message: `${joined.nickname} 님이 들어왔습니다.`,
- },
- ]);
- }
- } // console.log("🛰️ [secretRoomJoin] localStorage 업데이트");
- };
- socket.on("secretRoomJoin", handleSecretRoomJoin);
-
- const handleRoomExit = (data) => {
- const { currentPlayers, leftUser } = data;
- const mySessionId = socket.id;
- localStorage.setItem("meteoPlayers", JSON.stringify(currentPlayers));
-
- updateUsersFromPlayers(currentPlayers);
-
- if (leftUser.sessionId === mySessionId) {
- localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- localStorage.removeItem("meteoPlayers");
- navigate("/main");
- } else {
- setMessages((prev) => [
- ...prev,
- {
- nickname: "SYSTEM",
- message: `${leftUser.nickname} 님이 나갔습니다.`,
- },
- ]);
- }
- };
- onRoomExit(handleRoomExit);
-
- // 3) cleanup
- return () => {
- socket.off("secretRoomJoin", handleSecretRoomJoin);
- socket.off("roomExit", handleRoomExit);
- offRoomExit();
- // localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- // localStorage.removeItem("meteoPlayers");
- };
- }, [nickname, navigate]);
-
- useEffect(() => {
- const handleUnloadOrBack = () => {
- const savedRoomId = localStorage.getItem("meteoRoomId");
- const savedNickname = nickname;
-
- if (savedRoomId && savedNickname) {
- // console.log("🚪 [뒤로가기/새로고침] 방 나감 처리 시작");
- exitMeteoRoom({ roomId: savedRoomId, nickname: savedNickname });
-
- localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- }
- };
-
- window.addEventListener("beforeunload", handleUnloadOrBack); // 새로고침 / 탭 종료
-
- return () => {
- window.removeEventListener("beforeunload", handleUnloadOrBack);
- };
- }, [nickname]);
-
- useEffect(() => {
- const handlePopState = (event) => {
- // 브라우저 alert 사용 (콘솔이 안 보일때도 확인 가능)
-
- alert("게임을 나가시겠습니까?");
-
- const savedNickname = nickname;
-
- if (currentRoomId && savedNickname) {
- exitMeteoRoom({ roomId: roomId, nickname: nickname });
- localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- navigate("/main");
- // console.log("🚪 [뒤로가기] 방 나감 처리 시작");
- }
- };
-
- // 현재 history 상태 저장
- window.history.pushState({ page: "meteo" }, "", window.location.pathname);
-
- // 이벤트 리스너 등록
- window.addEventListener("popstate", handlePopState);
-
- return () => {
- window.removeEventListener("popstate", handlePopState);
- };
- }, [nickname]);
-
- useEffect(() => {
- const socket = getSocket();
-
- const handleMatchRandom = (roomData) => {
- // console.log("🛰️ [matchRandom 수신 - LandingPage]", roomData);
- localStorage.setItem("meteoPlayers", JSON.stringify(roomData.players));
- updateUsersFromPlayers(roomData.players);
- // ✅ 마지막 들어온 유저 추적해서 system 메시지 출력
- const prevCount = users.filter((u) => u !== null).length;
- const newCount = roomData.players.length;
-
- if (newCount > prevCount) {
- const joined = roomData.players[newCount - 1];
- if (joined?.nickname) {
- setMessages((prev) => [
- ...prev,
- {
- nickname: "SYSTEM",
- message: `${joined.nickname} 님이 들어왔습니다.`,
- },
- ]);
- }
- }
- };
-
- socket.on("matchRandom", handleMatchRandom);
-
- return () => {
- socket.off("matchRandom", handleMatchRandom);
- };
- }, []);
-
- // 게임 시작
- const handleStartGame = () => {
- startMeteoGame(currentRoomId);
- };
-
- useEffect(() => {
- onMeteoGameStart((gameData) => {
- console.log("🎮 [gameStart 수신] 게임 데이터:", gameData);
-
- // ✅ 카운트다운 먼저 시작
- setCountdown(3);
- let count = 3;
- const countdownInterval = setInterval(() => {
- count -= 1;
- setCountdown(count);
- if (count === 0) {
- clearInterval(countdownInterval);
-
- // ✅ roomId, roomCode, nickname 저장 보정
- localStorage.setItem("roomId", gameData.roomId);
- localStorage.setItem("meteoRoomId", gameData.roomId); // ✅ 명확히 같이 저장
-
- if (gameData.roomCode) {
- localStorage.setItem("roomCode", gameData.roomCode);
- localStorage.setItem("meteoRoomCode", gameData.roomCode); // ✅ 확실하게
- console.log("✅ roomCode 저장됨:", gameData.roomCode);
- } else {
- // console.warn("❗ gameData.roomCode 없음 → 저장 생략");
- }
-
- if (!localStorage.getItem("nickname")) {
- const matched = gameData.players.find(
- (p) => p.sessionId === getSocket()?.id
- );
- if (matched?.nickname) {
- localStorage.setItem("nickname", matched.nickname);
- }
- }
-
- // ✅ 페이지 이동
- navigate("/meteo/game", { state: { ...gameData } }, 3000);
- }
- }, 1000);
- });
-
- return () => {
- offMeteoGameStart();
- };
- }, [navigate]);
-
- // 방 나가기 버튼 클릭
- const handleExitRoom = () => {
- const savedRoomId = localStorage.getItem("meteoRoomId");
- const savedNickname = nickname;
-
- // console.log("🚀 [방 나가기 버튼] 저장된 roomId:", savedRoomId);
- // console.log("🚀 [방 나가기 버튼] 저장된 nickname:", savedNickname);
-
- if (savedRoomId && savedNickname) {
- exitMeteoRoom({ roomId: savedRoomId, nickname: savedNickname });
- // onExitMeteoGame({ roomId: savedRoomId, nickname: savedNickname });
- } else {
- console.error("❌ [방 나가기] roomId 또는 nickname 없음", {
- savedRoomId,
- savedNickname,
- });
- }
-
- // ❗ emit 보내고 바로 메인으로 튕기기
- localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- navigate("/main");
- };
-
- useEffect(() => {
- const handleChat = (data) => {
- // console.log("[채팅 수신]", data);
- setMessages((prev) => [...prev, data]);
- };
-
- // ✅ 먼저 off 해두고 on
- const socket = getSocket();
- socket.off("chatSend", handleChat);
- socket.on("chatSend", handleChat);
-
- return () => {
- socket.off("chatSend", handleChat);
- };
- }, []);
-
- const sendChat = () => {
- if (!chatInput.trim()) return;
- onChatMessage({
- roomId: currentRoomId,
- nickname,
- message: chatInput.trim(),
- });
- setChatInput("");
- };
-
- useEffect(() => {
- if (scrollRef.current) {
- scrollRef.current.scrollIntoView({ behavior: "smooth" });
- }
- }, [messages]);
-
- // MeteoLandingPage.jsx
-
- useEffect(() => {
- const handleGoWaitingRoom = (data) => {
- // console.log("📥 [LandingPage] waitingRoomGo 수신:", data);
-
- const myNickname = localStorage.getItem("nickname");
- const isMeIncluded = data.players.some(
- (player) => player.nickname === myNickname
- );
-
- if (!isMeIncluded) {
- console.warn("❗ 내 닉네임이 포함되지 않음 → 수신 무시");
- return;
- }
-
- updateUsersFromPlayers(data.players);
- localStorage.setItem("meteoPlayers", JSON.stringify(data.players));
- };
-
- onGoWaitingRoom(handleGoWaitingRoom);
- return () => {
- offGoWaitingRoom();
- };
- }, []);
-
- useEffect(() => {
- // 준비 경고 이벤트 처리
- onReadyWarning((data) => {
- // console.log("⚠️ [onReadyWarning] 경고 수신:", data);
-
- // 알림 표시 (준비 경고 메시지)
- setAlertMessage(
- data.message || "10초 내에 준비하지 않으면 방에서 퇴장됩니다."
- );
- setShowAlert(true);
- setTimeout(() => setShowAlert(false), 4000);
-
- // 시스템 메시지 추가
- setMessages((prev) => [
- ...prev,
- {
- nickname: "SYSTEM",
- message:
- "⚠️ " +
- (data.message || "10초 내에 준비하지 않으면 방에서 퇴장됩니다."),
- },
- ]);
- });
-
- // 강퇴 이벤트 처리
- onKick((data) => {
- // console.log("👢 [onKick] 강퇴 수신:", data);
-
- // 로컬 스토리지 정리
- localStorage.removeItem("meteoRoomCode");
- localStorage.removeItem("meteoRoomId");
- localStorage.removeItem("meteoPlayers");
-
- // 강퇴 전용 알림 표시
- setKickMessage(
- data.message || "준비 시간이 초과되어 방에서 퇴장되었습니다."
- );
- setShowKickAlert(true);
- });
-
- // 이벤트 리스너 정리 함수
- return () => {
- offReadyWarning(); // 함수로 분리된 이벤트 리스너 제거 함수 호출
- offKick();
- };
- }, [navigate]);
-
- const handleKickConfirm = () => {
- setShowKickAlert(false);
- navigate("/main");
- };
-
- useEffect(() => {
- // 방장 경고 이벤트 (방장에게만 표시)
- onHostKickWarning((data) => {
- // console.log("⚠️ [onHostKickWarning] 방장 경고 수신:", data);
-
- // 방장인 경우에만 알림 표시
- if (users.find((u) => u?.nickname === nickname)?.isHost) {
- setAlertMessage("5초 내에 게임을 시작해주세요");
- setShowAlert(true);
- setTimeout(() => setShowAlert(false), 3000);
- }
- });
-
- return () => {
- offHostKickWarning();
- };
- }, [users, nickname]);
- return (
-
- {showReadyAlert && (
-
- 방장님은 20초 내에 시작 버튼을 눌러 게임을 시작해주세요!
-
- )}
-
- {/*
*/}
- {countdown !== null && (
-
- )}
-
-
-
-
-
- 지구를 지켜라!
-
-
-
- {/* 아이콘에 호버 애니메이션 추가 */}
-
- ?
-
-
- {/* 툴팁에 페이드 + 슬라이드 효과 */}
-
- 2~4인의 협력 모드
-
- 들어온 Player들이 모두 준비되면
-
- 방장이 시작 버튼을 눌러
-
- 지구를 지킬 수 있습니다!
-
-
-
-
- {/* 유저 카드 */}
-
- {users.map((user, idx) => (
-
-
- {/* 왕관 아이콘 (오른쪽 상단) */}
- {user?.isHost && (
-
- )}
-
- No.{idx + 1}
-
- {/* ✅ user가 있을 때만 프로필 사진 */}
- {user ? (
-
- ) : null}
- {/*
*/}
- {/* 닉네임 */}
-
- {user?.nickname || "-"}
-
- {user && (
-
- {user.ready ? "Ready" : "unReady"}
-
- )}
-
- ))}
-
-
- {/* 채팅 + 방코드 */}
-
-
- {/* 채팅 메시지 영역 */}
-
- {messages.map((msg, idx) => {
- if (msg.nickname === "SYSTEM") {
- const isJoin = msg.message.includes("들어왔습니다");
- const isExit = msg.message.includes("나갔습니다");
-
- return (
-
- {msg.message}
-
- );
- }
-
- return (
-
- {msg.nickname} :{" "}
- {msg.message}
-
- );
- })}
-
-
-
- {/* 채팅 입력창 */}
-
- setChatInput(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && sendChat()}
- placeholder="메시지를 입력하세요"
- />
-
- 전송
-
-
-
-
-
-
-
방코드
-
- {!currentRoomCode || currentRoomCode === "undefined"
- ? "-"
- : currentRoomCode}
-
- {currentRoomCode ? (
-
-
-
- ) : null}
-
-
- {users.find((user) => user?.nickname === nickname)?.isHost ? (
- allReady && totalUsers.length >= 2 ? (
- // ✅ 방장이고, allReady이면 진짜 Start 버튼
-
- ) : (
- // ✅ 방장이지만 아직 allReady가 false면 흐릿한 버튼
-
- )
- ) : (
- // ✅ 일반 유저는 ready/unready 토글 버튼
-
u?.nickname === nickname)?.ready
- ? Unready
- : Ready
- }
- alt="ready-btn"
- onClick={() =>
- GameReady({
- roomId: currentRoomId,
- nickname,
- ready: !users.find((u) => u?.nickname === nickname)
- ?.ready,
- })
- }
- className="w-[8rem] cursor-pointer hover:scale-105 transition"
- />
- )}
-
-
-
-
-
-
- {showAlert && (
-
setShowAlert(false)}
- />
- )}
-
- {showKickAlert && (
-
- )}
-
- );
-};
-
-export default MeteoLandingPage;
diff --git a/src/pages/multi/MultiPage.tsx b/src/pages/multi/MultiPage.tsx
deleted file mode 100644
index c718199..0000000
--- a/src/pages/multi/MultiPage.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-// @ts-nocheck
-import React, { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import multiBg from "../../assets/images/multi_background.png";
-import boardBg from "../../assets/images/board1.jpg";
-import mintBtn from "../../assets/images/mint_large_btn.png";
-import searchBtn from "../../assets/images/search_btn.png";
-import goOutBtn from "../../assets/images/go_out.png";
-import RoomList from "../../components/multi/RoomList";
-import MakeRoomModal from "../../components/multi/modal/MakeRoomModal";
-import EnterRoomModal from "../../components/multi/modal/EnterRoomModal";
-import useAuthStore from "../../store/authStore";
-// Sockets removed in this FE-only build; provide safe no-op shims
-const getSocket = () => ({ on: () => {}, off: () => {}, connected: false });
-const requestRoomList = () => {};
-const onRoomList = () => {};
-const onRoomUpdate = () => {};
-const offRoomList = () => {};
-const offRoomUpdate = () => {};
-const joinRoom = () => {};
-
-const MultiPage = () => {
- const [isModalOpen, setIsModalOpen] = useState(false); // 방 만들기 모달
- const [selectedRoom, setSelectedRoom] = useState(null); // 클릭한 방
- const [showEnterModal, setShowEnterModal] = useState(false); // 입장 모달
- const [roomList, setRoomList] = useState([]); // 룸 목록
- const [searchKeyword, setSearchKeyword] = useState(""); // 방 검색색
-
- const navigate = useNavigate();
-
- const filteredRooms = roomList.filter((room) =>
- room.title.toLowerCase().includes(searchKeyword.toLowerCase())
- );
-
- // useEffect(() => {
- // const socket = getSocket();
- // if (socket) {
- // socket.onAny((event, ...args) => {
- // console.log("📡 수신된 이벤트:", event, args);
- // });
- // }
- // }, []);
-
- useEffect(() => {
- const handleRoomList = (rooms) => {
- // console.log("[room_list 수신 :", rooms);
- const parsed = rooms.map((room) => ({
- id: room.roomId,
- title: room.title,
- language: room.language,
- standardPeople: room.maxCount,
- currentPeople: room.currentCount,
- isPublic: !room.isLocked,
- roomCode: room.roomCode,
- status: room.isStarted ? "playing" : "waiting",
- }));
- setRoomList(parsed);
- };
-
- const handleRoomUpdate = (updatedRoom) => {
- // console.log("🟡 room_update 수신:", updatedRoom);
- const parsed = {
- id: updatedRoom.roomId,
- title: updatedRoom.title,
- language: updatedRoom.language,
- standardPeople: updatedRoom.maxCount,
- currentPeople: updatedRoom.currentCount,
- isPublic: !updatedRoom.isLocked,
- roomCode: updatedRoom.roomCode,
- status: updatedRoom.isStarted ? "playing" : "waiting",
- };
-
- // console.log("💡 parsed currentPeople:", parsed.currentPeople);
-
- setRoomList((prevRooms) => {
- const exists = prevRooms.some((room) => room.id === parsed.id);
- return exists
- ? prevRooms.map((room) => (room.id === parsed.id ? parsed : room))
- : [...prevRooms, parsed];
- });
- };
-
- const requestRoomsSafely = () => {
- const s = getSocket();
- if (s && s.connected) {
- // console.log("🟢 socket 연결됨 → 방 목록 요청");
- requestRoomList((rooms) => {
- const parsed = rooms.map((room) => ({
- id: room.roomId,
- title: room.title,
- language: room.language,
- standardPeople: room.maxCount,
- currentPeople: room.currentCount,
- isPublic: !room.isLocked,
- roomCode: room.roomCode,
- status: room.isStarted ? "playing" : "waiting",
- }));
- setRoomList(parsed);
- });
-
- // ✅ 연결된 이후에만 리스너 등록
- onRoomList(handleRoomList);
- onRoomUpdate(handleRoomUpdate);
- } else {
- setTimeout(requestRoomsSafely, 300);
- }
- };
-
- requestRoomsSafely();
-
- return () => {
- offRoomList();
- offRoomUpdate();
- };
- }, []);
-
- const handleEnterClick = (room) => {
- setSelectedRoom(room);
- setShowEnterModal(true);
- };
-
- const handleCloseEnterModal = () => {
- setSelectedRoom(null);
- setShowEnterModal(false);
- };
-
- const nickname = useAuthStore((state) => state.user?.nickname);
-
- const handleConfirmEnter = (roomCode, feedbackCallback) => {
- const socket = getSocket();
-
- const handleJoinResponse = (res) => {
- // console.log("✅ 입장 응답:", res); // res === "joined"
-
- if (res === "joined") {
- navigate(`/multi/room/${selectedRoom.id}`, {
- state: {
- roomTitle: selectedRoom.title,
- isPublic: selectedRoom.isPublic,
- language: selectedRoom.language,
- currentPeople: selectedRoom.currentPeople,
- standardPeople: selectedRoom.standardPeople,
- roomCode: selectedRoom.roomCode,
- },
- });
-
- setSelectedRoom(null);
- setShowEnterModal(false);
- feedbackCallback?.(true);
- } else {
- feedbackCallback?.(false);
- }
- };
-
- // ✅ nickname 포함해서 전달
- if (selectedRoom.isPublic) {
- joinRoom({ roomId: selectedRoom.id, nickname }, handleJoinResponse);
- } else {
- joinRoom(
- { roomId: selectedRoom.id, roomCode, nickname },
- handleJoinResponse
- );
- }
- };
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleRoomRemoved = (removedRoomId) => {
- // console.log("🗑️ room_removed 수신:", removedRoomId);
- setRoomList((prev) => prev.filter((room) => room.id !== removedRoomId));
- };
-
- socket.on("room_removed", handleRoomRemoved);
-
- return () => {
- socket.off("room_removed", handleRoomRemoved);
- };
- }, []);
-
- return (
-
- {/*
*/}
-
- {/* 방 만들기 모달 */}
- {isModalOpen &&
setIsModalOpen(false)} />}
-
- {/* 메인 보드 */}
-
-
-
-
- 배틀모드
-
-
- {/* 방 만들기 버튼 */}
-
setIsModalOpen(true)}
- style={{ backgroundImage: `url(${mintBtn})` }}
- >
-
- 방 만들기
-
-
-
- {/* 검색창 */}
-
- setSearchKeyword(e.target.value)} // 엔터없이 글자 포함되어있을때 검색 가능
- className="w-full h-[45px] pl-4 pr-[65px] rounded-md text-[17px] font-bold text-black focus:outline-none"
- />
-
-
-
- {/* 방 리스트 */}
-
-
-
-
-
navigate("/main")}
- className="absolute bottom-24 right-5 w-[7rem] h-[3rem] hover:brightness-110 hover:scale-[0.98] active:scale-[0.95] transition"
- // style={{ cursor: "url('/cursors/click.png') 32 32, pointer" }}
- />
-
-
- {/* 방 입장 모달 */}
- {showEnterModal && selectedRoom && (
-
- )}
-
- );
-};
-
-export default MultiPage;
diff --git a/src/pages/multi/RoomWaitingPage.tsx b/src/pages/multi/RoomWaitingPage.tsx
deleted file mode 100644
index b370cab..0000000
--- a/src/pages/multi/RoomWaitingPage.tsx
+++ /dev/null
@@ -1,499 +0,0 @@
-// @ts-nocheck
-import { useParams, useLocation, useNavigate } from "react-router-dom"; // 라우터의 파라미터 읽어오기
-import { useState, useEffect } from "react";
-import multiBg from "../../assets/images/multi_background.png";
-import boardBg from "../../assets/images/board1.jpg";
-import lockImg from "../../assets/images/black_lock_icon.png";
-import unlockImg from "../../assets/images/black_unlock_icon.png";
-import RoomUserList from "../../components/multi/waiting/RoomUserList";
-import Header from "../../components/common/Header";
-import RoomChatBox from "../../components/multi/waiting/RoomChatBox";
-import RoomInfoPanel from "../../components/multi/waiting/RoomInfoPanel";
-import MakeRoomModal from "../../components/multi/modal/MakeRoomModal";
-import useAuthStore from "../../store/authStore";
-// Sockets removed; provide safe no-op shims
-const getSocket = () => ({ on: () => {}, off: () => {}, emit: () => {} });
-
-const RoomWaitingPage = () => {
- const { roomId } = useParams(); // url에 담긴 roomId 읽어오기
- const { state } = useLocation(); // navigate할때 보낸 데이터
- const navigate = useNavigate();
- const [users, setUsers] = useState([]);
- const [chatMessages, setChatMessages] = useState([]); // 입장알림림
- const [showReadyAlert, setShowReadyAlert] = useState(false);
-
- const nickname = useAuthStore((state) => state.user?.nickname);
-
- const myUser = users.find((u) => u.nickname === nickname);
- const isReady = myUser?.isReady || false;
- const isHost = myUser?.isHost || false;
-
- const [editModeOpen, setEditModeOpen] = useState(false);
-
- // 나가기
- const handleLeaveRoom = () => {
- const socket = getSocket();
- // console.log("[LEAVE] emit leave_room", {
- // roomId,
- // nickname,
- // });
- socket.emit("leave_room", { roomId, nickname });
-
- navigate("/multi");
- };
-
- // 초기값 사용하기 위함.
- const [roomInfo, setRoomInfo] = useState(() => {
- const initialInfo = state?.roomInfo ?? state; // ✅ 두 경우 모두 대응
- return {
- roomTitle: initialInfo?.roomTitle || "",
- isLocked: initialInfo?.isLocked ?? true,
- language: initialInfo?.language || "Unknown",
- currentPeople: initialInfo?.currentPeople || 1,
- standardPeople: initialInfo?.standardPeople || 4,
- roomCode: initialInfo?.roomCode || "",
- };
- });
-
- // 방 정보 최신화용
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- // 진입하자마자 최신 room info 요청
- socket.emit("room_list", (rooms) => {
- const myRoom = rooms.find((r) => String(r.roomId) === String(roomId));
- if (myRoom) {
- setRoomInfo((prev) => ({
- roomTitle: myRoom.title,
- isLocked: myRoom.isLocked,
- isPublic: !myRoom.isLocked,
- language: myRoom.language,
- currentPeople: myRoom.currentCount,
- standardPeople: myRoom.maxCount,
- roomCode: prev.roomCode,
- }));
- }
- });
-
- // 실시간 업데이트 반영
- const handleRoomUpdate = (updatedRoom) => {
- if (String(updatedRoom.roomId) === String(roomId)) {
- // console.log("💡 방 업데이트 수신:", updatedRoom);
- setRoomInfo((prev) => ({
- roomTitle: updatedRoom.title,
- isLocked: updatedRoom.isLocked,
- isPublic: !updatedRoom.isLocked,
- language: updatedRoom.language,
- currentPeople: updatedRoom.currentCount,
- standardPeople: updatedRoom.maxCount,
- roomCode: updatedRoom.roomCode ?? prev.roomCode,
- status: updatedRoom.isStarted ? "playing" : "waiting",
- }));
-
- socket.emit("room_status", {
- roomId,
- nickname: state?.nickname,
- roomCode: state?.roomCode,
- });
- }
- };
-
- socket.on("room_update", handleRoomUpdate);
- return () => socket.off("room_update", handleRoomUpdate);
- }, [roomId]);
-
- // 방 최초초 입장시 room_status 요청
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- // 방 상태 요청(room_status 응답 : 현재 유저 목록 등등)
- socket.emit("room_status", {
- roomId: roomId,
- nickname: state?.nickname,
- roomCode: state?.roomCode,
- });
-
- const handleRoomStatus = (data) => {
- // console.log("✅ room_status 응답 수신:", data);
-
- // roomInfo 세팅 (추가로 방 정보도 최신화)
- setRoomInfo((prev) => ({
- ...prev,
- roomTitle: data.roomTitle,
- isLocked: data.isLocked,
- isPublic: !data.isLocked,
- language: data.language,
- currentPeople: data.currentCount,
- standardPeople: data.maxCount,
- roomCode: data.roomCode ?? prev.roomCode,
- status: data.isStarted ? "playing" : "waiting",
- }));
-
- // 사용자 슬롯 세팅
- const slotData = Array.from({ length: data.maxCount }, (_, i) => {
- const user = data.users[i];
- if (user) {
- return {
- slot: i + 1,
- nickname: user.nickname,
- isHost: user.isHost,
- isReady: user.isReady,
- empty: false,
- };
- } else {
- return {
- slot: i + 1,
- empty: true,
- };
- }
- });
-
- setUsers(slotData);
- };
-
- socket.on("room_status", handleRoomStatus);
-
- return () => {
- socket.off("room_status", handleRoomStatus);
- };
- }, [roomId]);
-
- // join 브로드캐스트
- useEffect(() => {
- const socket = getSocket();
- if (!socket || !roomInfo?.standardPeople) return;
-
- const handleJoinRoom = (data) => {
- // console.log("🟢 join_room 수신:", data);
-
- // data.status 기준으로 유저 슬롯 구성
- const updatedSlots = Array.from({ length: 4 }, (_, i) => {
- const user = data.status[i];
- if (user) {
- return {
- slot: i + 1,
- nickname: user.nickname,
- isHost: user.isHost,
- isReady: user.isReady,
- empty: false,
- };
- } else {
- return {
- slot: i + 1,
- empty: true,
- };
- }
- });
-
- setUsers(updatedSlots);
- };
- socket.on("join_room", handleJoinRoom);
- return () => {
- socket.off("join_room", handleJoinRoom);
- };
- }, [roomInfo?.standardPeople]);
-
- // join_notice 브로드캐스트
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleJoinNotice = (data) => {
- // console.log("📢 join_notice 수신:", data);
- setChatMessages((prev) => [
- ...prev,
- { type: "notice", text: data.message },
- ]);
- };
-
- socket.on("join_notice", handleJoinNotice);
- return () => socket.off("join_notice", handleJoinNotice);
- }, []);
-
- // leave_notice 브로드캐스트
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleLeaveNotice = (data) => {
- // console.log("📤 leave_notice 수신:", data);
- setChatMessages((prev) => [
- ...prev,
- { type: "notice", text: data.message },
- ]);
- };
-
- socket.on("leave_notice", handleLeaveNotice);
- return () => socket.off("leave_notice", handleLeaveNotice);
- }, []);
-
- // 대기방 채팅
-
- const handleSendMessage = (messageText) => {
- const socket = getSocket();
- if (!socket || !nickname || !roomId) return;
-
- const messageData = {
- roomId,
- nickname,
- message: messageText.text,
- };
-
- // console.log("📫emit send_chat : ", messageData);
- socket.emit("send_chat", messageData);
- };
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleReceiveChat = (data) => {
- // console.log("send_chat 수신 :", data);
- setChatMessages((prev) => [
- ...prev,
- {
- type: "chat",
- text: `${data.nickname}: ${data.message}`,
- timestamp: data.timestamp,
- },
- ]);
- };
-
- socket.on("send_chat", handleReceiveChat);
- return () => socket.off("send_chat", handleReceiveChat);
- }, []);
-
- const handleReadyToggle = () => {
- const socket = getSocket();
- if (!socket || !nickname || !roomId) return;
-
- // console.log("📤 emit start:", { roomId, nickname });
- socket.emit("ready", {
- roomId,
- nickname,
- });
- };
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleReadyStatusUpdate = (data) => {
- // console.log("🧪 ready_status_update 수신:", data);
-
- const newUsers = Array.from({ length: 4 }, (_, i) => {
- const user = data.users[i];
- return user
- ? {
- slot: i + 1,
- nickname: user.nickname,
- isHost: user.isHost,
- isReady: user.isReady,
- empty: false,
- }
- : {
- slot: i + 1,
- empty: true,
- };
- });
-
- setUsers(newUsers); // 상태 반영
-
- // 준비 인원 확인은 여기서 해야 함!
- const readyCount = newUsers.filter((u) => !u.empty && u.isReady).length;
- const totalCount = newUsers.filter((u) => !u.empty).length;
-
- if (readyCount === totalCount && totalCount > 1) {
- setChatMessages((prev) => [
- ...prev,
- {
- type: "notice",
- text: "모든 플레이어가 준비되었습니다. 방장님은 게임을 시작할 수 있어요!",
- },
- ]);
-
- setShowReadyAlert(true);
- setTimeout(() => setShowReadyAlert(false), 4000);
- }
- };
-
- socket.on("ready_status_update", handleReadyStatusUpdate);
- return () => socket.off("ready_status_update", handleReadyStatusUpdate);
- }, [roomInfo.standardPeople]);
-
- const handleStartGame = () => {
- const socket = getSocket();
- if (!socket || !nickname || !roomId) return;
-
- // console.log("🎮 emit start_game", { roomId, nickname });
- socket.emit("start_game", { roomId, nickname });
- };
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleGameStarted = (data) => {
- // console.log("🎮 수신된 이벤트: game_started", data);
- // console.log("📦 navigate 직전 users 상태:", users);
- if (String(data.roomId) === String(roomId)) {
- navigate(`/multi/game/${roomId}`);
- }
- };
-
- socket.on("game_started", handleGameStarted);
- return () => socket.off("game_started", handleGameStarted);
- }, [roomId, navigate]);
-
- useEffect(() => {
- const handlePopState = (event) => {
- // confirm 대화 상자 사용 (확인/취소 버튼 모두 제공)
- const isConfirmed = window.confirm("방을 나가시겠습니까?");
-
- if (isConfirmed) {
- // 사용자가 '확인'을 클릭한 경우
- handleLeaveRoom();
- // console.log("🚪 [뒤로가기] 방 나감 처리 시작");
- } else {
- // 사용자가 '취소'를 클릭한 경우
- // 현재 URL 상태를 다시 푸시하여 브라우저 히스토리에 추가
- window.history.pushState(
- { page: "multi" },
- "",
- window.location.pathname
- );
- // console.log("🔙 [뒤로가기] 취소됨, 방에 머무름");
- }
- };
-
- // 현재 history 상태 저장
- window.history.pushState({ page: "multi" }, "", window.location.pathname);
-
- // 이벤트 리스너 등록
- window.addEventListener("popstate", handlePopState);
-
- return () => {
- window.removeEventListener("popstate", handlePopState);
- };
- }, [nickname]);
-
- // 새로고침 막음
- useEffect(() => {
- const handleKeyDown = (e) => {
- // F5 키 또는 Ctrl+R 눌렀을 때
- if (e.key === "F5" || (e.ctrlKey && e.key === "r")) {
- e.preventDefault();
- e.stopPropagation();
- alert("새로고침은 사용할 수 없습니다.");
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, []);
-
- return (
-
- {/*
*/}
-
- {showReadyAlert && (
-
-
- 모든 플레이어가 준비 완료! 방장님, 게임을 시작해주세요!
-
-
- )}
-
-
-
-
-
- {/* 제목 & 아이콘: 중앙 정렬 */}
-
-
-
{roomInfo.roomTitle}
-
-
- {/* 오른쪽 상단 고정 버튼 */}
- {isHost && (
-
setEditModeOpen(true)}
- className="absolute -top-4 right-2 px-3 py-1 text-m rounded-lg
- bg-gradient-to-r from-purple-500 to-pink-500 text-white
- shadow hover:brightness-110 hover:scale-105
- transition-transform duration-150"
- >
- ✏️ 방 정보 수정
-
- )}
-
- {/* 사용자 리스트 */}
-
-
-
-
- {/* 채팅박스 */}
-
-
- !u.empty && u.isReady).length ===
- users.filter((u) => !u.empty).length
- }
- onStart={handleStartGame}
- canstart={
- isHost &&
- users
- .filter((u) => !u.empty && u.nickname !== nickname)
- .every((u) => u.isReady) &&
- users.filter((u) => !u.empty).length >= 2
- }
- />
-
-
-
- {editModeOpen && (
-
setEditModeOpen(false)}
- isEdit={true}
- initialData={{
- roomId,
- title: roomInfo.roomTitle,
- people: roomInfo.standardPeople,
- language: roomInfo.language,
- isPublic: !roomInfo.isLocked,
- currentPeople: roomInfo.currentPeople,
- }}
- />
- )}
-
- );
-};
-
-export default RoomWaitingPage;
diff --git a/src/pages/multi/TypingBattlePage.tsx b/src/pages/multi/TypingBattlePage.tsx
deleted file mode 100644
index 3843eaa..0000000
--- a/src/pages/multi/TypingBattlePage.tsx
+++ /dev/null
@@ -1,526 +0,0 @@
-// @ts-nocheck
-import { useParams, useLocation, useNavigate } from "react-router-dom";
-import { useState, useEffect, useRef } from "react";
-import multiBg from "../../assets/images/multi_background.png";
-import boardBg from "../../assets/images/board1.jpg";
-import logo from "../../assets/images/logo.png";
-import rocket1 from "../../assets/images/multi_rocket_1.png";
-import rocket2 from "../../assets/images/multi_rocket_2.png";
-import rocket3 from "../../assets/images/multi_rocket_3.png";
-import rocket4 from "../../assets/images/multi_rocket_4.png";
-import gameEndBtn from "../../assets/images/multi_game_end_btn.png";
-import bgm from "../../assets/sound/meteoBGM.mp3";
-import Header from "../../components/common/Header";
-import TypingBox from "../../components/multi/game/TypingBox";
-import ProgressBoard from "../../components/multi/game/ProgressBoard";
-import RoundScoreModal from "../../components/multi/modal/RoundScoreModal";
-import FinalResultModal from "../../components/multi/modal/FinalResultModal";
-import AloneAlertModal from "../../components/multi/modal/AloneAlertModal";
-import MultiAlertModal from "../../components/multi/modal/MultiAlertModal";
-import useAuthStore from "../../store/authStore";
-import useVolumeStore from "../../store/useVolumsStore";
-// Sockets removed; provide safe no-op shims
-const getSocket = () => ({ on: () => {}, off: () => {}, emit: () => {} });
-
-const TypingBattlePage = () => {
- const { roomId } = useParams(); // ✅ roomId 읽어오기
- // const [countdown, setCountdown] = useState(5);
-
- const [serverCountdown, setServerCountdown] = useState(null); // 서버에서 게임시작시 5초 카운트다운 동시성 처리 위함함
- const [countdownVisible, setCountdownVisible] = useState(false);
-
- const [gameStarted, setGameStarted] = useState(false);
-
- const [startTime, setStartTime] = useState(null); // 게임 시작 순간간
- const [elapsedTime, setElapsedTime] = useState(0); // 밀리초 단위
- const [timeRunning, setTimeRunning] = useState(false); // 타이머 실행 중 여부
- const [targetCode, setTargetCode] = useState("");
- const { state } = useLocation();
- const rocketImages = [rocket1, rocket2, rocket3, rocket4];
- const [roundEnded, setRoundEnded] = useState(false);
- const [roundEndingCountdown, setRoundEndingCountdown] = useState(null); // null이면 표시 안함
- const [showRoundScoreModal, setShowRoundScoreModal] = useState(false);
- const [roundScoreData, setRoundScoreData] = useState(null);
- const [firstFinisher, setFirstFinisher] = useState(null); // 첫번째 완주자
- const [currentRound, setCurrentRound] = useState(1);
- const [modalCountdown, setModalCountdown] = useState(null);
- const [finalResults, setFinalResults] = useState([]);
- const [showFinalModal, setShowFinalModal] = useState(false);
- const [oneLeftRoomInfo, setOneLeftRoomInfo] = useState(null); // 배틀시 한명남았을때
- const [showLeaveConfirm, setShowLeaveConfirm] = useState(false); // 비상탈출 확인 alert창창
-
- const navigate = useNavigate();
-
- const [roomInfo, setRoomInfo] = useState(null);
- const nickname = useAuthStore((state) => state.user?.nickname);
-
- const { bgmVolume } = useVolumeStore();
- const audioRef = useRef(null);
-
- const [users, setUsers] = useState(() => {
- const initialUsers = state?.users?.filter((u) => !u.empty) || [];
-
- // rocketImage 부여 (slot 기반)
- return initialUsers.map((user) => ({
- ...user,
- rocketImage: rocketImages[user.slot - 1] || rocket1,
- progress: 0, // 처음엔 0부터 시작
- }));
- });
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleServerCountdown = (data) => {
- const { seconds } = data;
- // console.log("start countdown 서버 카운트다운 🔥 : ", data.seconds)
- setServerCountdown(seconds); // 오버레이에 표시
- setCountdownVisible(true);
-
- if (seconds === 1) {
- // 1초 뒤 게임 시작
- setTimeout(() => {
- setCountdownVisible(false);
- setGameStarted(true); // 카운트다운 끝나면 게임 시작
- setTimeRunning(true); // 타이머도 시작!
- setStartTime(Date.now()); // 현재시간 기록
- }, 1000);
- }
- };
-
- socket.on("start_count_down", handleServerCountdown);
- return () => socket.off("start_count_down", handleServerCountdown);
- }, []);
-
- // 게임 시작 실시간 경과 시간 업뎃
- useEffect(() => {
- if (timeRunning) {
- const interval = setInterval(() => {
- setElapsedTime(Date.now() - startTime); // 밀리초 단위 경과 시간
- }, 10); // 10ms마다 업데이트(밀리초 보여주려고)
- return () => clearInterval(interval);
- }
- }, [timeRunning, startTime]);
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- socket.emit("room_status", { roomId });
-
- const handleRoomStatus = (data) => {
- // console.log("🧑🚀 TypingBattlePage room_status 수신:", data);
-
- const updatedUsers = Array.from({ length: data.maxCount }, (_, i) => {
- const user = data.users[i];
- return user
- ? {
- slot: i + 1,
- nickname: user.nickname,
- isHost: user.isHost,
- isReady: user.isReady,
- rocketImage: rocketImages[i], // 로켓 이미지 부여
- progress: 0,
- empty: false,
- }
- : {
- slot: i + 1,
- empty: true,
- };
- });
-
- setUsers(updatedUsers.filter((u) => !u.empty)); // 빈 슬롯 제거
- };
-
- socket.on("room_status", handleRoomStatus);
- return () => socket.off("room_status", handleRoomStatus);
- }, [roomId]);
-
- useEffect(() => {
- // console.log("getSocket() → ", getSocket());
- const socket = getSocket();
- if (!socket) return;
-
- const handleTypingStart = (data) => {
- // console.log("🥘 typing_start 수신:", data);
- setTargetCode(data.script); // 문제 저장
-
- setUsers(
- (prev) =>
- Array.isArray(prev)
- ? prev.map((user) => ({
- ...user,
- progress: 0,
- }))
- : [] // fallback
- );
- };
-
- socket.on("typing_start", handleTypingStart);
- return () => socket.off("typing_start", handleTypingStart);
- }, []);
-
- useEffect(() => {
- if (!audioRef.current) {
- audioRef.current = new Audio(bgm);
- audioRef.current.loop = true;
- audioRef.current.volume = bgmVolume;
- }
-
- if (gameStarted) {
- audioRef.current.currentTime = 0;
- audioRef.current.play().catch((e) => {
- // console.warn("재생 실패:", e);
- });
- } else {
- audioRef.current.pause();
- audioRef.current.currentTime = 0;
- }
- }, [gameStarted]);
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleProgressUpdate = (data) => {
- // console.log("🚀 progress_update 수신:", data);
-
- setUsers((prev) =>
- prev.map((user) =>
- user.nickname === data.nickname
- ? { ...user, progress: data.progressPercent }
- : user
- )
- );
- };
-
- socket.on("progress_update", handleProgressUpdate);
- return () => socket.off("progress_update", handleProgressUpdate);
- }, []);
-
- const handleFinish = () => {
- if (roundEnded) return;
- setRoundEnded(true);
- setTimeRunning(false); // 타자 타이머 멈춤
-
- // 10초 후 서버에 라운드 종료 알림 (이건 내 타자 성공시에만)
- setTimeout(() => {
- const socket = getSocket();
- socket.emit("round_end", { roomId });
- }, 10000);
- };
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- // ✅ 모든 유저: 안내창만 표시
- const handleFinishNotice = (data) => {
- const { nickname } = data;
- // console.log("🏁 finish_notice 수신:", nickname);
- setFirstFinisher(nickname); // 모든 사람에게 안내창 띄움 (타이머는 X)
- };
-
- // ✅ 내 타이머만 멈추게 할 새로운 이벤트
- const handleCountDown = (data) => {
- // console.log("⏱ end count_down 수신:", data.seconds); // 10~1까지 수신
-
- if (data.count === 5) {
- // 최초 10초 카운트 시작 시, 내 타이머 멈춤 + 카운트다운 시작
- setRoundEnded(true);
- setTimeRunning(false);
- setRoundEndingCountdown(5);
- } else {
- setRoundEndingCountdown(data.seconds);
-
- // 👇 안내창 자동 제거 (1초 끝나고)
- if (data.seconds === 1) {
- setTimeout(() => {
- setRoundEndingCountdown(null);
- }, 1000);
- }
- }
- };
- socket.on("finish_notice", handleFinishNotice);
- socket.on("end_count_down", handleCountDown);
-
- return () => {
- socket.off("finish_notice", handleFinishNotice);
- socket.off("end_count_down", handleCountDown);
- };
- }, []);
-
- // 소켓 수신
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleRoundScore = (data) => {
- // console.log("📊 round_score 수신:", data);
- setRoundScoreData(data);
-
- setShowRoundScoreModal(true);
- // setModalCountdown(5); // 이 줄 제거 - 서버 카운트다운으로 대체
-
- // 클라이언트 측 interval 제거하고 서버 이벤트에 의존
- // const interval = setInterval(() => { ... }); // 이 부분 제거
- };
-
- // 새로운 이벤트 핸들러: 서버에서 오는 카운트다운 처리
- const handleRoundCountDown = (data) => {
- // console.log("⏰ round_count_down 수신:", data);
-
- // 서버에서 오는 seconds 값으로 카운트다운 설정
- setModalCountdown(data.seconds);
-
- // 카운트다운이 끝나면 (1초 또는 0초일 때) 모달 닫고 다음 라운드 시작
- if (data.seconds <= 1) {
- setTimeout(() => {
- setShowRoundScoreModal(false);
-
- // 라운드 데이터가 있고 3라운드 미만인 경우 다음 라운드 시작
- if (roundScoreData && roundScoreData.round < 3) {
- // console.log("🔄 다음 라운드 준비:", roundScoreData.round + 1);
-
- setCurrentRound(roundScoreData.round + 1);
- setGameStarted(false);
- setRoundEnded(false);
- setFirstFinisher(null);
- setTargetCode("");
- setElapsedTime(0);
- setStartTime(null);
- setTimeRunning(false);
-
- // console.log("🎙️ round_start emit 시도:", { roomId, nickname });
- socket.emit("round_start", {
- roomId,
- nickname,
- });
- }
- }, 1000); // 1초 후 실행하여 자연스러운 전환
- }
- };
-
- socket.on("round_score", handleRoundScore);
- socket.on("round_count_down", handleRoundCountDown); // 새 이벤트 리스너 추가
-
- return () => {
- socket.off("round_score", handleRoundScore);
- socket.off("round_count_down", handleRoundCountDown); // 정리
- };
- }, [roomId, nickname, roundScoreData]);
-
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleGameResult = (data) => {
- // console.log("💩 최종 게임 결과 안내 : ",data);
- setFinalResults(data.results); // 서버에서 avgSpeed 등 포함된 리스트로 보낸다고 가정
- setShowFinalModal(true);
- };
- socket.on("game_result", handleGameResult);
- return () => {
- socket.off("game_result", handleGameResult);
-
- if (audioRef.current) {
- audioRef.current.pause();
- audioRef.current.currentTime = 0;
- audioRef.current = null; // 깔끔한 해제
- }
- };
- }, []);
-
- // 게임 종료시 받는 방 상태 정보보
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleRoomStatus = (data) => {
- // console.log("🧑🚀 room_status 수신:", data);
- setRoomInfo(data); // 이걸 FinalResultModal로 넘겨줘야 함
-
- const updatedUsers = Array.from({ length: data.maxCount }, (_, i) => {
- const user = data.users[i];
- return user
- ? {
- slot: i + 1,
- nickname: user.nickname,
- isHost: user.isHost,
- isReady: user.isReady,
- rocketImage: rocketImages[i],
- progress: 0,
- empty: false,
- }
- : {
- slot: i + 1,
- empty: true,
- };
- });
-
- setUsers(updatedUsers.filter((u) => !u.empty));
- };
-
- socket.on("room_status", handleRoomStatus);
- return () => socket.off("room_status", handleRoomStatus);
- }, [roomId]);
-
- // 배틀페이지에서 한명남았을때 감지지
- useEffect(() => {
- const socket = getSocket();
- if (!socket) return;
-
- const handleOnePersonLeft = (data) => {
- // console.log("🎉room_one_person 수신 : ", data);
- setOneLeftRoomInfo(data);
- };
-
- socket.on("room_one_person", handleOnePersonLeft);
- return () => socket.off("room_one_person", handleOnePersonLeft);
- }, []);
-
- useEffect(() => {
- const handleKeyDown = (e) => {
- // F5 키 또는 Ctrl+R 눌렀을 때
- if (e.key === "F5" || (e.ctrlKey && e.key === "r")) {
- e.preventDefault();
- e.stopPropagation();
- alert("새로고침은 사용할 수 없습니다.");
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, []);
-
- return (
-
- {/* 방 혼자 남았을때 alert 창 */}
- {oneLeftRoomInfo && (
-
{
- navigate(`/multi/room/${oneLeftRoomInfo.roomId}`, {
- state: oneLeftRoomInfo,
- });
- }}
- />
- )}
-
- {roundEndingCountdown !== null && (
-
- {firstFinisher && (
-
- 🎉 {firstFinisher} 님이
- 가장 먼저 완주했어요!
-
- )}
- {currentRound === 3 ? (
- <>🔥 마지막 라운드 종료까지 {roundEndingCountdown}초 남았습니다>
- ) : (
- <>게임 종료까지 {roundEndingCountdown}초 남았습니다>
- )}
-
- )}
-
- {/* Typing Battle 시작! (Room ID: {roomId}) */}
- {/* 카운트다운 오버레이 */}
- {/* {!gameStarted && (
-
-
- {countdown > 0 ? countdown : "Start!"}
-
-
- )} */}
- {countdownVisible && !gameStarted && (
-
-
- {serverCountdown > 0 ? serverCountdown : "Start!"}
-
-
- )}
-
-
-
-
-
-
-
-
- {/* 컨텐츠 */}
-
- {/* 로고 */}
- {/*
-
-
*/}
-
- {/* 타이핑 박스 */}
-
-
-
-
- {/* 진행 보드 */}
-
-
-
setShowLeaveConfirm(true)}
- >
-
- {/* 라운드 종료 점수 모달 */}
- setShowRoundScoreModal(false)}
- />
-
- setShowFinalModal(false)}
- roomInfo={roomInfo}
- />
-
- {showLeaveConfirm && (
- {
- const socket = getSocket();
- if (socket && roomId && nickname) {
- socket.emit("exit_room", { roomId, nickname });
- }
-
- setShowLeaveConfirm(false);
- navigate("/multi"); // 메인페이지로 이동동
- }}
- onCancel={() => setShowLeaveConfirm(false)}
- showCancel={true} // 이럴 때만 취소 버튼 보여짐
- />
- )}
-
- );
-};
-
-export default TypingBattlePage;
diff --git a/src/pages/mypage/MyPage.simple.test.tsx b/src/pages/mypage/MyPage.simple.test.tsx
deleted file mode 100644
index 5b9a507..0000000
--- a/src/pages/mypage/MyPage.simple.test.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { MockedProvider } from "@apollo/client/testing/react";
-import { BrowserRouter } from "react-router-dom";
-import { vi } from "vitest";
-
-// Mock the Header component
-vi.mock("../../components/common/Header", () => ({
- default: function MockHeader() {
- return Header
;
- },
-}));
-
-// Simple test component
-const SimpleMyPage = () => {
- return (
-
-
MyPage Test
-
Profile Panel
-
Rankings Panel
-
- );
-};
-
-describe("MyPage", () => {
- it("renders basic structure", () => {
- render(
-
-
-
-
-
- );
-
- expect(screen.getByText("MyPage Test")).toBeInTheDocument();
- expect(screen.getByTestId("profile-panel")).toBeInTheDocument();
- expect(screen.getByTestId("rankings-panel")).toBeInTheDocument();
- });
-});
diff --git a/src/pages/mypage/MyPage.test.tsx b/src/pages/mypage/MyPage.test.tsx
deleted file mode 100644
index 8849621..0000000
--- a/src/pages/mypage/MyPage.test.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import React from "react";
-import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import { BrowserRouter } from "react-router-dom";
-import { vi, describe, it, expect, beforeEach } from "vitest";
-import MyPage from "./MyPage";
-
-// Mock the Header component
-vi.mock("../../components/common/Header", () => ({
- default: function MockHeader() {
- return Header
;
- },
-}));
-
-// Mock the modals
-vi.mock("../../components/modal/RankingModal", () => ({
- default: ({ onClose }: { onClose: () => void }) => (
-
- Ranking Modal
-
- ),
-}));
-
-vi.mock("../../components/modal/SettingModal", () => ({
- default: ({ onClose }: { onClose: () => void }) => (
-
- Setting Modal
-
- ),
-}));
-
-vi.mock("../../components/common/TutoModal", () => ({
- default: ({ onClose }: { onClose: () => void }) => (
-
- Tuto Modal
-
- ),
-}));
-
-const renderMyPage = () => {
- return render(
-
-
-
- );
-};
-
-describe("MyPage", () => {
- it("renders user profile information correctly", async () => {
- renderMyPage();
-
- // Wait for data to load
- await waitFor(() => {
- expect(screen.getByText("NICKNAME")).toBeInTheDocument();
- });
-
- // Check profile information - using mock data from MyPage.tsx
- expect(screen.getByText(/9\.7k|9,700 Followers/)).toBeInTheDocument();
- expect(screen.getByText(/274 Following/)).toBeInTheDocument();
- expect(screen.getByText(/ID\/PW:/)).toBeInTheDocument();
- });
-
- it("displays top records correctly", async () => {
- renderMyPage();
-
- await waitFor(() => {
- expect(screen.getByText(/Top Records/i)).toBeInTheDocument();
- });
-
- // Check for Java, JS, Python records from mock data
- expect(screen.getByText(/java:/i)).toBeInTheDocument();
- expect(screen.getByText(/js:/i)).toBeInTheDocument();
- expect(screen.getByText(/python:/i)).toBeInTheDocument();
- });
-
- it("displays monthly rankings correctly", async () => {
- renderMyPage();
-
- await waitFor(() => {
- expect(screen.getByText(/Monthly Rankings/i)).toBeInTheDocument();
- });
-
- // Monthly rankings should be present
- expect(screen.getByText(/Monthly Rankings/i)).toBeInTheDocument();
- });
-
- it("opens withdraw modal when withdraw button is clicked", async () => {
- renderMyPage();
-
- await waitFor(() => {
- expect(screen.getByText("탈퇴하기")).toBeInTheDocument();
- });
-
- const withdrawButton = screen.getByText("탈퇴하기");
- fireEvent.click(withdrawButton);
-
- expect(screen.getByText("정말로 탈퇴하시겠습니까?")).toBeInTheDocument();
- });
-
- it("renders without crashing", () => {
- const { container } = renderMyPage();
- expect(container).toBeTruthy();
- });
-});
diff --git a/src/pages/mypage/MyPage.tsx b/src/pages/mypage/MyPage.tsx
deleted file mode 100644
index 68bfd0a..0000000
--- a/src/pages/mypage/MyPage.tsx
+++ /dev/null
@@ -1,1420 +0,0 @@
-import { useState, useCallback, memo } from "react";
-import type * as React from "react";
-// import { useQuery, useMutation } from "@apollo/client/react";
-import { useNavigate } from "react-router-dom";
-import Header from "../../components/common/Header";
-import multibg from "../../assets/images/multi_background.png";
-import ToggleGroupComponent from "../../components/ui/ToggleGroup";
-import SwitchToggle from "../../components/ui/SwitchToggle";
-import SearchBar from "../../components/ui/SearchBar";
-import { useSearchStore } from "../../store/useSearchStore";
-import PatchNoteModal from "../../components/PatchNoteModal";
-import RankingModal from "../../components/modal/RankingModal";
-import SettingModal from "../../components/modal/SettingModal";
-import TutoModal from "../../components/common/TutoModal";
-import { upDateMyProfile } from "../../api/myPage";
-// import {
-// GET_USER_PROFILE,
-// GET_MONTHLY_RANKINGS,
-// GET_PERFORMANCE_DATA,
-// } from "../../features/user/graphql/queries";
-// import {
-// FOLLOW_USER,
-// UNFOLLOW_USER,
-// WITHDRAW_ACCOUNT,
-// } from "../../features/user/graphql/mutations";
-import {
- type UserProfile,
- type PerformanceData,
- type GraphFilter,
- type LanguageOption,
- type TimePeriodOption,
- type ConnectedBadgeProps,
- type PerformanceChartProps,
-} from "../../features/user/types";
-
-const MyPage: React.FC = () => {
- const navigate = useNavigate();
-
- // Mock data for development
- const mockUserData = {
- me: {
- id: "1",
- nickname: "친절한독수리993",
- email: "email@gmail.com",
- profileImage:
- "https://via.placeholder.com/120x120/4f46e5/ffffff?text=User",
- followersCount: 9700,
- followingCount: 274,
- connectedAccounts: {
- google: true,
- kakao: true,
- },
- topRecords: {
- java: 423,
- js: 123,
- python: 274,
- sql: null,
- go: null,
- },
- totalScore: 3817,
- wallet: {
- balance: 1000,
- currency: "KRW",
- },
- },
- };
-
- const mockRankingsData = {
- monthlyRankings: {
- java: 127,
- js: 1,
- python: 24,
- sql: 2423,
- go: null,
- },
- };
-
- const mockPerformanceData = {
- performanceData: [
- { month: "Jan", userScore: 100, comparisonScore: 150 },
- { month: "Feb", userScore: 120, comparisonScore: 140 },
- { month: "Mar", userScore: 150, comparisonScore: 160 },
- { month: "Apr", userScore: 180, comparisonScore: 170 },
- { month: "May", userScore: 200, comparisonScore: 190 },
- { month: "Jun", userScore: 220, comparisonScore: 210 },
- { month: "Jul", userScore: 250, comparisonScore: 240 },
- { month: "Aug", userScore: 280, comparisonScore: 270 },
- { month: "Sep", userScore: 300, comparisonScore: 290 },
- { month: "Oct", userScore: 320, comparisonScore: 310 },
- { month: "Nov", userScore: 350, comparisonScore: 340 },
- { month: "Dec", userScore: 380, comparisonScore: 370 },
- ],
- };
-
- // Following users average data (when no specific user is searched)
- const mockFollowingAverageData = {
- performanceData: [
- { month: "Jan", userScore: 100, comparisonScore: 120 },
- { month: "Feb", userScore: 120, comparisonScore: 125 },
- { month: "Mar", userScore: 150, comparisonScore: 140 },
- { month: "Apr", userScore: 180, comparisonScore: 160 },
- { month: "May", userScore: 200, comparisonScore: 180 },
- { month: "Jun", userScore: 220, comparisonScore: 200 },
- { month: "Jul", userScore: 250, comparisonScore: 220 },
- { month: "Aug", userScore: 280, comparisonScore: 240 },
- { month: "Sep", userScore: 300, comparisonScore: 260 },
- { month: "Oct", userScore: 320, comparisonScore: 280 },
- { month: "Nov", userScore: 350, comparisonScore: 300 },
- { month: "Dec", userScore: 380, comparisonScore: 320 },
- ],
- };
-
- // User profile data (from Apollo Client cache) - COMMENTED OUT FOR MOCK DATA
- // const {
- // data: userData,
- // loading: userLoading,
- // error: userError,
- // } = useQuery(GET_USER_PROFILE);
- // const { data: rankingsData, loading: rankingsLoading } =
- // useQuery(GET_MONTHLY_RANKINGS);
- // const { data: performanceData, loading: performanceLoading } = useQuery(
- // GET_PERFORMANCE_DATA,
- // {
- // variables: {
- // language: "java",
- // timePeriod: "annually",
- // },
- // }
- // );
-
- // Mutations - COMMENTED OUT FOR MOCK DATA
- // const [followUser] = useMutation(FOLLOW_USER);
- // const [unfollowUser] = useMutation(UNFOLLOW_USER);
- // const [withdrawAccount] = useMutation(WITHDRAW_ACCOUNT);
-
- // Local UI state
- const [graphFilter, setGraphFilter] = useState({
- language: "java",
- timePeriod: "annually",
- comparisonUser: undefined,
- });
- const [isWithdrawModalOpen, setIsWithdrawModalOpen] =
- useState(false);
- const [showGridLines, setShowGridLines] = useState(true);
- const [showDataPoints, setShowDataPoints] = useState(true);
-
- // Header 모달 상태
- const [isTutorialModalOpen, setIsTutorialModalOpen] =
- useState(false);
- const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
- const [isRankingModalOpen, setIsRankingModalOpen] = useState(false);
- const [isEditModalOpen, setIsEditModalOpen] = useState(false);
-
- // 검색 상태 관리
- const [searchQuery, setSearchQuery] = useState("");
-
- // 검색 핸들러를 useCallback으로 메모이제이션
- const handleSearch = useCallback((query: string) => {
- setSearchQuery(query);
- }, []);
-
- // User profile data (stateful to reflect edits)
- const [profile, setProfile] = useState({
- id: mockUserData.me.id,
- nickname: mockUserData.me.nickname,
- email: mockUserData.me.email,
- profileImage: mockUserData.me.profileImage,
- followersCount: mockUserData.me.followersCount,
- followingCount: mockUserData.me.followingCount,
- connectedAccounts: mockUserData.me.connectedAccounts,
- topRecords: mockUserData.me.topRecords,
- monthlyRankings: mockRankingsData.monthlyRankings,
- totalScore: mockUserData.me.totalScore,
- wallet: mockUserData.me.wallet,
- });
-
- // Performance data for graph - 검색어에 따라 변경
- const performanceDataPoints: PerformanceData[] = searchQuery
- ? mockPerformanceData.performanceData
- : mockFollowingAverageData.performanceData;
-
- // Language options for filters
- const languageOptions: LanguageOption[] = [
- { value: "java", label: "Java" },
- { value: "js", label: "JS" },
- { value: "python", label: "Python" },
- { value: "sql", label: "SQL" },
- { value: "go", label: "Go" },
- ];
-
- // Time period options
- const timePeriodOptions: TimePeriodOption[] = [
- { value: "daily", label: "Daily" },
- { value: "weekly", label: "Weekly" },
- { value: "annually", label: "Annually" },
- ];
-
- // Handlers
- const handleLanguageFilter = useCallback((language: string) => {
- setGraphFilter((prev) => ({ ...prev, language }));
- }, []);
-
- const handleTimePeriodFilter = useCallback(
- (timePeriod: "daily" | "weekly" | "annually") => {
- setGraphFilter((prev) => ({ ...prev, timePeriod }));
- },
- []
- );
-
- const handleWithdrawAccount = useCallback(async () => {
- // Mock implementation - just show alert and redirect
- alert("계정 탈퇴가 완료되었습니다.");
- navigate("/auth/login");
- }, [navigate]);
-
- const handleFollowUser = useCallback(async (userId: string) => {
- // Mock implementation - just log
- console.log("Follow user:", userId);
- alert("팔로우했습니다!");
- }, []);
-
- const handleUnfollowUser = useCallback(async (userId: string) => {
- // Mock implementation - just log
- console.log("Unfollow user:", userId);
- alert("언팔로우했습니다!");
- }, []);
-
- // Utility functions
- const formatNumber = useCallback((num: number): string => {
- if (num >= 1000000) {
- return `${(num / 1000000).toFixed(1)}M`;
- } else if (num >= 1000) {
- return `${(num / 1000).toFixed(1)}k`;
- }
- return num.toString();
- }, []);
-
- const formatRank = useCallback((rank: number): string => {
- const lastDigit = rank % 10;
- const lastTwoDigits = rank % 100;
-
- if (lastTwoDigits >= 11 && lastTwoDigits <= 13) {
- return `${rank}th`;
- }
-
- switch (lastDigit) {
- case 1:
- return `${rank}st`;
- case 2:
- return `${rank}nd`;
- case 3:
- return `${rank}rd`;
- default:
- return `${rank}th`;
- }
- }, []);
-
- // Mock loading and error states - always false for mock data
- const userLoading = false;
- const rankingsLoading = false;
- const userError: Error | null = null;
-
- if (userLoading || rankingsLoading) {
- return (
-
- );
- }
-
- if (userError) {
- return (
-
-
- An error occurred
-
-
- );
- }
-
- return (
-
- {/* Header - absolute positioned */}
-
- setIsTutorialModalOpen(true)}
- onShowSetting={() => setIsSettingModalOpen(true)}
- onShowRanking={() => setIsRankingModalOpen(true)}
- />
-
-
- {/* Main Content */}
-
-
- {/* Withdraw Modal */}
- {isWithdrawModalOpen && (
-
setIsWithdrawModalOpen(false)}
- onConfirm={handleWithdrawAccount}
- />
- )}
-
- {/* Edit Profile Modal */}
- {isEditModalOpen && (
- setIsEditModalOpen(false)}
- onSave={async (nextNickname: string) => {
- try {
- await upDateMyProfile({ nickname: nextNickname });
- setProfile((prev) => ({ ...prev, nickname: nextNickname }));
- alert("프로필이 업데이트되었습니다.");
- } catch (e) {
- alert("프로필 업데이트에 실패했습니다.");
- } finally {
- setIsEditModalOpen(false);
- }
- }}
- />
- )}
-
- {/* Header Modals */}
- {isTutorialModalOpen && (
- setIsTutorialModalOpen(false)} />
- )}
-
- {isRankingModalOpen && (
- setIsRankingModalOpen(false)} />
- )}
-
- {isSettingModalOpen && (
- setIsSettingModalOpen(false)} />
- )}
-
- );
-
- // Main Content Component
- function MainContent() {
- return (
-
- {/* Profile Panel */}
-
-
- {/* Rankings Panel */}
-
-
- );
- }
-
- // Profile Panel Component
- function ProfilePanel() {
- return (
-
- {/* Profile Header */}
-
-
- {/* Follower Statistics */}
-
-
- {/* Combined Information Section */}
-
-
- );
- }
-
- // Profile Header Component
- function ProfileHeader() {
- return (
-
- {/* NICKNAME Label */}
-
- NICKNAME
-
-
- {/* Profile Image */}
-
-
- );
- }
-
- // Follower Statistics Component
- function FollowerStatistics() {
- return (
-
- {/* Followers Box */}
-
navigate("/follower")}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor = "#f3f4f6";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "#ffffff";
- }}
- >
- {formatNumber(profile.followersCount)} Followers
-
-
- {/* Following Box */}
-
navigate("/following")}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor = "#f3f4f6";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "#ffffff";
- }}
- >
- {formatNumber(profile.followingCount)} Following
-
-
- );
- }
-
- // Combined Information Section Component
- function CombinedInformationSection() {
- return (
-
- {/* Account Information */}
-
- {/* ID/PW Section */}
-
- ID/PW: {profile.email}
- {/* Connected Accounts */}
-
- {profile.connectedAccounts.google && (
-
- )}
- {profile.connectedAccounts.kakao && (
-
- )}
-
-
-
-
- {/* Top Records */}
-
-
- Top Records
-
-
- {Object.entries(profile.topRecords).map(([language, score]) => (
-
-
{language}:
-
{score !== null ? score : "-"}
-
- ))}
-
-
- {/* Edit & Withdraw Buttons */}
-
-
setIsEditModalOpen(true)}
- style={{
- backgroundColor: "rgba(59, 130, 246, 0.9)",
- color: "#ffffff",
- borderRadius: "0.25rem",
- padding: "0.75rem",
- textAlign: "center",
- fontSize: "0.875rem",
- fontWeight: "600",
- cursor: "pointer",
- transition: "all 0.15s ease-in-out",
- marginBottom: "0.5rem",
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor = "rgba(37, 99, 235, 1)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "rgba(59, 130, 246, 0.9)";
- }}
- >
- 정보 수정
-
-
setIsWithdrawModalOpen(true)}
- style={{
- backgroundColor: "rgba(239, 68, 68, 0.8)",
- color: "#ffffff",
- borderRadius: "0.25rem",
- padding: "0.75rem",
- textAlign: "center",
- fontSize: "0.875rem",
- fontWeight: "600",
- cursor: "pointer",
- transition: "all 0.15s ease-in-out",
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 1)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "rgba(239, 68, 68, 0.8)";
- }}
- >
- 탈퇴하기
-
-
-
- );
- }
-
- // Connected Badge Component
- function ConnectedBadge({ type, label }: ConnectedBadgeProps) {
- return (
-
-
- Connected with
-
- {type === "google" ? (
-
-
-
-
-
-
- ) : (
-
-
-
- )}
- {/* Visually hidden label for screen readers */}
-
- {label}
-
-
- );
- }
-
- // Rankings Panel Component
- function RankingsPanel() {
- return (
-
- {/* Monthly Rankings - 위에 별도 배치 */}
-
-
- {/* Performance Graph Card */}
-
-
- );
- }
-
- // Monthly Rankings Card Component
- function MonthlyRankingsCard() {
- return (
-
-
-
- );
- }
-
- // Monthly Rankings Component - Row 형태로 수정
- function MonthlyRankings() {
- return (
-
-
- Monthly Rankings
-
-
- {/* 5개 항목을 row 형태로 표시 */}
-
- {Object.entries(profile.monthlyRankings).map(([language, rank]) => (
-
-
- {language}
-
-
- {rank !== null ? formatRank(rank) : "-"}
-
-
- ))}
-
-
- );
- }
-
- // Search and Filters Row Component
- function SearchAndFiltersRow() {
- return (
-
- {/* Search Bar - 왼쪽에 고정 */}
-
-
-
-
- {/* Filters Container - 오른쪽에 세로 배치 */}
-
- {/* Language Filter Tags */}
-
-
- {/* Time Filter Buttons */}
-
-
-
- );
- }
-
- // Performance Graph Card Component
- function PerformanceGraphCard() {
- return (
-
- {/* Search Bar and Filters Row */}
-
-
- {/* Graph Options */}
-
-
- {/* Graph Area */}
-
-
- {/* Legend */}
-
-
- );
- }
-
- // Language Filter Tags Component
- function LanguageFilterTags() {
- return (
- {
- if (value.length > 0) {
- handleLanguageFilter(value[0]);
- }
- }}
- size="sm"
- variant="default"
- />
- );
- }
-
- // Time Filter Buttons Component
- function TimeFilterButtons() {
- return (
- {
- if (value.length > 0) {
- handleTimePeriodFilter(value[0] as "daily" | "weekly" | "annually");
- }
- }}
- size="sm"
- variant="outline"
- />
- );
- }
-
- // Graph Options Component
- function GraphOptions() {
- return (
-
-
-
-
- );
- }
-
- // Graph Area Component
- function GraphArea() {
- return (
-
- {/* Graph will be implemented with a charting library like Chart.js or Recharts */}
-
-
- );
- }
-
- // Performance Chart Component
- function PerformanceChart({
- data,
- language,
- timePeriod,
- showGridLines,
- showDataPoints,
- }: PerformanceChartProps & {
- showGridLines: boolean;
- showDataPoints: boolean;
- }) {
- // This would integrate with a charting library
- // For now, showing a placeholder
- return (
-
- {/* Grid Lines */}
- {showGridLines && (
-
- )}
-
-
-
- Performance Chart
-
-
- {language} - {timePeriod}
-
-
- Grid Lines: {showGridLines ? "ON" : "OFF"} | Data Points:{" "}
- {showDataPoints ? "ON" : "OFF"}
-
-
-
- );
- }
-
- // Graph Legend Component
- function GraphLegend() {
- return (
-
- {/* User Legend */}
-
-
- {/* Comparison Legend */}
-
-
-
-
- {searchQuery ? searchQuery : "Following Average"}
-
-
-
- );
- }
-
- // Withdraw Modal Component
- function WithdrawModal({
- onClose,
- onConfirm,
- }: {
- onClose: () => void;
- onConfirm: () => void;
- }) {
- return (
-
-
-
- 정말로 탈퇴하시겠습니까?
-
-
- 탈퇴 후에는 모든 데이터가 삭제되며 복구할 수 없습니다.
-
-
-
-
- );
- }
-};
-
-export default MyPage;
-
-// Edit Profile Modal Component
-function EditProfileModal({
- initialNickname,
- onClose,
- onSave,
-}: {
- initialNickname: string;
- onClose: () => void;
- onSave: (nickname: string) => void | Promise;
-}) {
- const [nickname, setNickname] = useState(initialNickname);
- const [saving, setSaving] = useState(false);
-
- return (
-
-
-
- 프로필 정보 수정
-
-
-
-
- Nickname
-
- setNickname(e.target.value)}
- style={{
- padding: "0.5rem 0.75rem",
- borderRadius: "0.25rem",
- border: "1px solid rgba(255,255,255,0.2)",
- background: "rgba(255,255,255,0.95)",
- color: "#111827",
- }}
- placeholder="닉네임을 입력하세요"
- />
-
-
-
-
- 취소
-
- {
- if (!nickname.trim()) {
- alert("닉네임을 입력하세요.");
- return;
- }
- setSaving(true);
- await onSave(nickname.trim());
- setSaving(false);
- }}
- disabled={saving}
- style={{
- backgroundColor: saving
- ? "rgba(59,130,246,0.6)"
- : "rgba(59, 130, 246, 0.9)",
- color: "#ffffff",
- borderRadius: "0.25rem",
- padding: "0.5rem 1rem",
- fontSize: "0.875rem",
- cursor: "pointer",
- border: "none",
- }}
- >
- 저장
-
-
-
-
- );
-}
diff --git a/src/pages/mypage/MyReport.tsx b/src/pages/mypage/MyReport.tsx
deleted file mode 100644
index 41005fc..0000000
--- a/src/pages/mypage/MyReport.tsx
+++ /dev/null
@@ -1,274 +0,0 @@
-import backgroundImg from "../../assets/images/single_background.jpg";
-// import ReportImg from '../../assets/images/report.png'
-import box from "../../assets/images/board1.jpg";
-import Header from "../../components/common/Header";
-import leftBtn from "../../assets/images/left_btn.png";
-import rightBtn from "../../assets/images/right_btn.png";
-import titleBox from "../../assets/images/logo_remove4.png";
-import TutoModal from "../../components/common/TutoModal";
-import SettingModal from "../../components/modal/SettingModal";
-import RankingModal from "../../components/modal/RankingModal";
-
-import { useState } from "react";
-
-interface Category {
- title: string;
- reports: string[];
-}
-
-interface Report {
- id: number;
- date: string;
- words: string[];
-}
-
-const MyReport: React.FC = () => {
- const categories: Category[] = [
- {
- title: "디자인패턴",
- reports: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "의존성",
- "implements",
- ],
- },
- {
- title: "데이터베이스",
- reports: ["OSI 7계층", "TCP/IP", "HTTP", "DNS", "ARP"],
- },
- {
- title: "자료구조",
- reports: ["프로세스", "스레드", "메모리 관리", "스케줄링", "교착 상태"],
- },
- {
- title: "네트워크",
- reports: ["OSI 7계층", "TCP/IP", "HTTP", "DNS", "ARP"],
- },
- {
- title: "운영체제",
- reports: ["프로세스", "스레드", "메모리 관리", "스케줄링", "교착 상태"],
- },
- ];
-
- const reports: Report[] = [
- {
- id: 1,
- date: "2025_04_22_5",
- words: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "에라 모르겠다",
- "수수수코드노바",
- ],
- },
- { id: 2, date: "2025_04_23_4", words: ["의존성", "implements"] },
- { id: 3, date: "2025_04_24_3", words: ["전략 패턴", "옵저버 패턴"] },
- {
- id: 4,
- date: "2025_04_22_2",
- words: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "에라 모르겠다",
- "수수수코드노바",
- ],
- },
- {
- id: 5,
- date: "2025_04_22_1",
- words: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "에라 모르겠다",
- "수수수코드노바",
- ],
- },
- {
- id: 6,
- date: "2025_04_22_1",
- words: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "에라 모르겠다",
- "수수수코드노바",
- ],
- },
- {
- id: 7,
- date: "2025_04_22_1",
- words: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "에라 모르겠다",
- "수수수코드노바",
- ],
- },
- {
- id: 8,
- date: "2025_04_22_1",
- words: [
- "싱글톤 패턴",
- "팩토리 패턴",
- "MVC 패턴",
- "에라 모르겠다",
- "수수수코드노바",
- ],
- },
- ];
-
- const [selectedWords, selSelectedWords] = useState([]);
- const [currentIndex, setCurrentIndex] = useState(0);
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
-
- const handlePrev = (): void => {
- setCurrentIndex(
- (prev) => (prev - 1 + categories.length) % categories.length
- );
- };
-
- const handleNext = (): void => {
- setCurrentIndex((prev) => (prev + 1) % categories.length);
- };
-
- const currentCategory: Category = categories[currentIndex];
-
- const handleReportClick = (words: string[]): void => {
- selSelectedWords(words);
- };
-
- return (
-
- {showTutoModal && (
-
- setShowTutoModal(false)} />
-
- )}
- {showSettingModal && (
-
setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
- );
-};
-
-export default MyReport;
diff --git a/src/pages/ranking/Ranking.tsx b/src/pages/ranking/Ranking.tsx
deleted file mode 100644
index f8382e6..0000000
--- a/src/pages/ranking/Ranking.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-// @ts-nocheck
-import bgImg from "../../assets/images/multi_background.png";
-import Board2Container from "../../components/single/BoardContainer";
-import goldMedal from "../../assets/images/gold_medal.png";
-import silverMedal from "../../assets/images/silver_medal.png";
-import bronzeMedal from "../../assets/images/dong_medal.png";
-import leftBtn from "../../assets/images/left_btn.png";
-import rightBtn from "../../assets/images/right_btn.png";
-import leftBtn2 from "../../assets/images/less-than_black.png";
-import rightBtn2 from "../../assets/images/greater-than_black.png";
-import xBtn from "../../assets/images/x_btn.png";
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { useQuery } from "@apollo/client/react";
-import { GET_RANKINGS } from "@/features/ranking/graphql/queries";
-import Header from "../../components/common/Header";
-import TutoModal from "../../components/common/TutoModal";
-import SettingModal from "../../components/modal/SettingModal";
-
-const Ranking = () => {
- const navigate = useNavigate();
-
- const languages = ["JAVA", "PYTHON", "SQL", "JS"];
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [currentLangIndex, setCurrentLangIndex] = useState(0);
-
- const [ranking, setRanking] = useState([null, null, null, null]); //언어별 랭킹
- const [period, setPeriod] = useState("all"); // daily|weekly|monthly|all
-
- const btn_class =
- "cursor-pointer scale-75 transition-all duration-150 hover:brightness-110 hover:translate-y-[2px] hover:scale-[0.98] active:scale-[0.95]";
-
- const [userType, setUserType] = useState(null);
- const [showSettingModal, setShowSettingModal] = useState(false);
-
- useEffect(() => {
- const auth = JSON.parse(localStorage.getItem("auth-storage") || "{}");
- setUserType(auth?.state?.user?.userType);
- }, []);
-
- const lang = languages[currentLangIndex];
- const { data, loading, error } = useQuery(GET_RANKINGS, {
- variables: { game: lang, period, limit: 50 },
- fetchPolicy: "cache-and-network",
- });
-
- useEffect(() => {
- if (data?.rankings) {
- const content = {
- top10: data.rankings.items?.slice(0, 10) || [],
- myRank: data.rankings.myRank || null,
- };
- const newRanking = [...ranking];
- newRanking[currentLangIndex] = content;
- setRanking(newRanking);
- }
- }, [data]);
-
- // useEffect(() => {
- // console.log(ranking);
- // }, [ranking])
-
- const handlePrev = () => {
- setCurrentLangIndex(
- (prev) => (prev - 1 + languages.length) % languages.length
- );
- };
-
- const handleNext = () => {
- setCurrentLangIndex((prev) => (prev + 1) % languages.length);
- };
-
- return (
-
- {/* 튜토리얼 모달 조건부 렌더링 */}
- {showTutoModal && (
-
- setShowTutoModal(false)} />
-
- )}
- {showSettingModal && (
-
- setShowSettingModal(false)} />
-
- )}
-
-
setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- />
-
-
-
-
- {/* 아이콘에 호버 애니메이션 추가 */}
-
- ?
-
-
- {/* 툴팁에 페이드 + 슬라이드 효과 */}
-
- 언어 옆에 화살표를 클릭하시면
-
- 다른 언어의 랭킹을 확인하실수 있습니다!
-
-
-
-
-
-
-
-
-
{languages[currentLangIndex]}
-
- {[
- { key: "daily", label: "Daily" },
- { key: "weekly", label: "Weekly" },
- { key: "monthly", label: "Monthly" },
- { key: "all", label: "All" },
- ].map((p) => (
- setPeriod(p.key)}
- >
- {p.label}
-
- ))}
-
-
-
-
-
- navigate(-1)}
- />
-
-
-
-
-
-
-
-
- {ranking[currentLangIndex]?.top10?.[0]?.nickname || "없음"}
-
-
- {ranking[currentLangIndex]?.top10?.[0]?.typingSpeed != null
- ? Math.floor(
- ranking[currentLangIndex]?.top10?.[0]?.typingSpeed
- )
- : 0}
- 타
-
-
-
-
-
-
-
-
-
- {ranking[currentLangIndex]?.top10?.[1]?.nickname || "없음"}
-
-
- {ranking[currentLangIndex]?.top10?.[1]?.typingSpeed != null
- ? Math.floor(
- ranking[currentLangIndex]?.top10?.[1]?.typingSpeed
- )
- : 0}
- 타
-
-
-
-
-
-
-
-
-
- {ranking[currentLangIndex]?.top10?.[2]?.nickname || "없음"}
-
-
- {ranking[currentLangIndex]?.top10?.[2]?.typingSpeed != null
- ? Math.floor(
- ranking[currentLangIndex]?.top10?.[2]?.typingSpeed
- )
- : 0}
- 타
-
-
-
-
-
-
-
- {Array(7)
- .fill(0)
- .map((_, idx) => {
- const nickname =
- ranking[currentLangIndex]?.top10?.[idx + 3]?.nickname ||
- "없음";
- const speed =
- ranking[currentLangIndex]?.top10?.[idx + 3]?.typingSpeed || 0;
- return (
-
- {idx + 4}. {nickname} ({Math.floor(speed)})
-
- );
- })}
- {userType !== "guest" && (
-
- 내 등수:{" "}
- {ranking[currentLangIndex]?.myRank?.rank != null
- ? ` ${Math.floor(ranking[currentLangIndex].myRank.rank)}`
- : " - "}
- 등
- {ranking[currentLangIndex]?.myRank?.typingSpeed != null
- ? ` ${Math.floor(ranking[currentLangIndex].myRank.typingSpeed)}`
- : " - "}
- 타
-
- )}
-
-
-
-
- );
-};
-
-export default Ranking;
diff --git a/src/pages/single/CsSelectPage.tsx b/src/pages/single/CsSelectPage.tsx
deleted file mode 100644
index 88709e1..0000000
--- a/src/pages/single/CsSelectPage.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import backgroundImg from "../../assets/images/single_background.svg";
-import designPattenBtn from "../../assets/images/design_patten_btn.png";
-import networkBtn from "../../assets/images/network_btn.png";
-import dataStrBtn from "../../assets/images/data_str_btn.png";
-import dbBtn from "../../assets/images/db_btn.png";
-import osBtn from "../../assets/images/os_btn.png";
-import cancelBtn from "../../assets/images/cancel_btn.png";
-import BoardContainer from "../../components/single/BoardContainer";
-import Header from "../../components/common/Header";
-import TutoModal from "../../components/common/TutoModal";
-import SettingModal from "../../components/modal/SettingModal";
-import RankingModal from "../../components/modal/RankingModal";
-import { Box } from "../../../styled-system/jsx";
-import { css } from "../../../styled-system/css";
-import { useNavigate } from "react-router-dom";
-import { useState } from "react";
-
-const CsSelectPage: React.FC = () => {
- const navigate = useNavigate();
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
-
- return (
-
- {showTutoModal && (
-
- setShowTutoModal(false)} />
-
- )}
- {showSettingModal && (
- setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
- {/* Header - absolute positioned */}
-
- setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- onShowRanking={() => setShowRankingModal(true)}
- />
-
-
-
- {/* 타이틀 텍스트 */}
-
- 단계선택
-
-
- {/* 버튼 이미지들 */}
-
-
- navigate("/single/game/cs?category=COMPUTER_STRUCTURE")
- }
- />
-
navigate("/single/game/cs?category=NETWORK")}
- />
-
navigate("/single/game/cs?category=DATABASE")}
- />
-
navigate("/single/game/cs?category=DATA_STRUCTURE")}
- />
-
navigate("/single/game/cs?category=OS")}
- />
-
-
- {/* 취소 버튼 */}
-
-
navigate("/single/select/language")}
- />
-
-
-
- );
-};
-
-export default CsSelectPage;
diff --git a/src/pages/single/GamePlayingPage.tsx b/src/pages/single/GamePlayingPage.tsx
deleted file mode 100644
index e599f91..0000000
--- a/src/pages/single/GamePlayingPage.tsx
+++ /dev/null
@@ -1,951 +0,0 @@
-// @ts-nocheck
-import backgroundImg from "../../assets/images/single_background.jpg";
-import boardImg from "../../assets/images/board1_cut.jpg";
-import boardBg from "../../assets/images/board4.png";
-import logo from "../../assets/images/logo.png";
-import javaCharacter from "../../assets/images/Java.png";
-import pythonCharacter from "../../assets/images/python.png";
-import javascriptCharacter from "../../assets/images/js.png";
-import sqlCharacter from "../../assets/images/SQL.png";
-import { Box } from "../../../styled-system/jsx";
-import { css } from "../../../styled-system/css";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { useParams, useNavigate } from "react-router-dom";
-
-import hljs from "highlight.js/lib/core";
-import javascript from "highlight.js/lib/languages/javascript";
-import java from "highlight.js/lib/languages/java";
-import python from "highlight.js/lib/languages/python";
-import sql from "highlight.js/lib/languages/sql";
-import "highlight.js/styles/atom-one-dark.css";
-import "../../styles/single/SinglePage.css";
-
-import ProgressBox from "../../components/single/ProgressBox";
-import Keyboard from "../../components/keyboard/Keyboard";
-import FinishPage from "./modal/FinishPage";
-
-import {
- singleLangCode,
- verifiedRecord,
- postRecord,
-} from "../../api/singleApi";
-import {
- processCode,
- getProgress,
- compareInputWithLineEnter,
- compareInputWithLine,
- calculateCPM,
- calculateCurrentLineTypedChars,
-} from "../../utils/typingUtils";
-import { encryptWithSessionKey } from "../../utils/cryptoUtils";
-
-hljs.registerLanguage("java", java);
-hljs.registerLanguage("python", python);
-hljs.registerLanguage("javascript", javascript);
-hljs.registerLanguage("sql", sql);
-
-interface KeyLog {
- key: string;
- timestamp: number;
-}
-
-// Mock data for testing
-const MOCK_CODE: Record = {
- java: `public class Sum {
- public static void main(String[] args) {
- int a = 5, b = 3;
- System.out.println(a + b);
- }
-}`,
- python: `def calculate():
- a = 5
- b = 3
- print(a + b)
-
-calculate()`,
- javascript: `function sum() {
- const a = 5;
- const b = 3;
- console.log(a + b);
-}
-
-sum();`,
- js: `function sum() {
- const a = 5;
- const b = 3;
- console.log(a + b);
-}
-
-sum();`,
- sql: `SELECT
- customer_id,
- COUNT(*) as order_count
-FROM orders
-GROUP BY customer_id;`,
-};
-
-// Language to Character mapping
-const getCharacterByLanguage = (lang: string | undefined): string => {
- if (!lang) return javaCharacter;
- const normalizedLang = lang.toLowerCase();
-
- // Map languages to characters
- const characterMap: Record = {
- java: javaCharacter,
- python: pythonCharacter,
- javascript: javascriptCharacter,
- js: javascriptCharacter,
- sql: sqlCharacter,
- };
-
- return characterMap[normalizedLang] || javaCharacter;
-};
-
-const GamePlayingPage: React.FC = () => {
- const { lang } = useParams<{ lang: string }>();
- const navigate = useNavigate();
-
- // code and typing state
- const [codeId, setCodeId] = useState(null);
- const [lines, setLines] = useState([]);
- const [space, setSpace] = useState([]);
- const [lineCharCounts, setLineCharCounts] = useState([]);
- const [currentLineIndex, setCurrentLineIndex] = useState(0);
- const [currentInput, setCurrentInput] = useState("");
- const [currentCharIndex, setCurrentCharIndex] = useState(0);
- const [wrongChar, setWrongChar] = useState(false);
- const [shake, setShake] = useState(false);
- const [pressedKey, setPressedKey] = useState(null);
-
- // meta
- const [requestId, setRequestId] = useState("");
- const [startTime, setStartTime] = useState(null);
- const [elapsedTime, setElapsedTime] = useState(0);
- const [isStarted, setIsStarted] = useState(false);
- const [isFinished, setIsFinished] = useState(false);
- const [progress, setProgress] = useState(0);
- const [totalTypedChars, setTotalTypedChars] = useState(0);
- const [cpm, setCpm] = useState(0);
- const [isLoading, setIsLoading] = useState(true);
- const [loadError, setLoadError] = useState(null);
-
- const inputRef = useRef(null);
- const codeRef = useRef(null);
- const keyLogsRef = useRef([]);
- const verifiedOnceRef = useRef(false);
- const pressedKeyTimeoutRef = useRef(null);
-
- // Refs to avoid recreating callbacks
- const linesRef = useRef([]);
- const isStartedRef = useRef(false);
- const currentLineIndexRef = useRef(0);
- const startTimeRef = useRef(null);
-
- // Keep refs updated
- useEffect(() => {
- linesRef.current = lines;
- }, [lines]);
-
- useEffect(() => {
- isStartedRef.current = isStarted;
- }, [isStarted]);
-
- useEffect(() => {
- currentLineIndexRef.current = currentLineIndex;
- }, [currentLineIndex]);
-
- useEffect(() => {
- if (!lang) {
- setLoadError("언어가 선택되지 않았습니다");
- setIsLoading(false);
- return;
- }
-
- // USE MOCK DATA FOR DEBUGGING
- const useMockData = true;
-
- if (useMockData) {
- setIsLoading(true);
- // Simulate API delay
- setTimeout(() => {
- const mockContent = MOCK_CODE[lang.toLowerCase()] || MOCK_CODE.java;
- const { lines, space, charCount } = processCode(mockContent);
- setCodeId(999); // Mock code ID
- setLines(lines);
- setSpace(space);
- setLineCharCounts(charCount);
- setRequestId("mock-request-id");
- setIsLoading(false);
- setLoadError(null);
- console.log("Mock data loaded:", { lang, lines: lines.length });
- }, 500);
- return;
- }
-
- // Original API call (disabled for debugging)
- setIsLoading(true);
- singleLangCode(lang)
- .then((data) => {
- if (!data || !data.content) {
- setLoadError("코드를 불러올 수 없습니다");
- setIsLoading(false);
- return;
- }
- const { lines, space, charCount } = processCode(data.content);
- setCodeId(data.codeId);
- setLines(lines);
- setSpace(space);
- setLineCharCounts(charCount);
- setRequestId(data.requestId);
- setIsLoading(false);
- setLoadError(null);
- })
- .catch((error) => {
- console.error("Failed to load code:", error);
- setLoadError("코드를 불러오는 중 오류가 발생했습니다");
- setIsLoading(false);
- });
- }, [lang]);
-
- useEffect(() => {
- if (inputRef.current) inputRef.current.focus();
- const keepFocus = (e: MouseEvent) => {
- if (inputRef.current && !inputRef.current.contains(e.target as Node)) {
- e.preventDefault();
- inputRef.current.focus();
- }
- };
- document.addEventListener("click", keepFocus);
- return () => document.removeEventListener("click", keepFocus);
- }, []);
-
- // Focus input after content is loaded and input is mounted
- useEffect(() => {
- if (!isLoading && !loadError && lines.length > 0 && inputRef.current) {
- inputRef.current.focus();
- }
- }, [isLoading, loadError, lines.length]);
-
- useEffect(() => {
- let timer: any;
- if (isStarted && !isFinished && startTime) {
- timer = setInterval(() => {
- const elapsed = Date.now() - startTime;
- setElapsedTime(elapsed);
- }, 10);
- }
- return () => {
- if (timer) {
- clearInterval(timer);
- }
- };
- }, [isStarted, startTime, isFinished]);
-
- useEffect(() => {
- setCpm(calculateCPM(totalTypedChars, elapsedTime / 1000));
- }, [elapsedTime, totalTypedChars]);
-
- // Cleanup timeout on unmount
- useEffect(() => {
- return () => {
- if (pressedKeyTimeoutRef.current) {
- clearTimeout(pressedKeyTimeoutRef.current);
- }
- };
- }, []);
-
- // Calculate total characters for progress
- const totalChars = useMemo(() => {
- return lineCharCounts.reduce((sum, count) => sum + count, 0);
- }, [lineCharCounts]);
-
- // Update progress based on typed characters (char by char)
- useEffect(() => {
- if (totalChars === 0) {
- setProgress(0);
- } else {
- const newProgress = Math.floor((totalTypedChars / totalChars) * 100);
- setProgress(newProgress);
- }
- }, [totalTypedChars, totalChars]);
-
- useEffect(() => {
- if (lines.length > 0 && currentLineIndex === lines.length) {
- handleFinish();
- }
- if (codeRef.current && currentLineIndex > 0) {
- const rows = codeRef.current.querySelectorAll(".codeLine");
- const lineHeight =
- (rows[currentLineIndex] as HTMLElement)?.getBoundingClientRect()
- .height || 28;
- codeRef.current.scrollTop += lineHeight;
- codeRef.current.scrollLeft = 0;
- }
- }, [currentLineIndex, lines.length]);
-
- const languageClass = useMemo(() => {
- const l = (lang || "").toLowerCase();
- if (l === "java") return "language-java";
- if (l === "python") return "language-python";
- if (l === "js") return "language-javascript";
- if (l === "sql") return "language-sql";
- return "";
- }, [lang]);
-
- // Memoize character image to prevent re-renders
- const characterImg = useMemo(() => getCharacterByLanguage(lang), [lang]);
-
- const handleKeyDown = (
- e: React.KeyboardEvent
- ) => {
- if (!isStarted) {
- setIsStarted(true);
- setStartTime(Date.now());
- }
- const key = e.key;
- const typingKey = key.length === 1;
-
- // Update pressed key for virtual keyboard (preserve case for letters)
- let physicalKey = key;
- // Only lowercase letters for matching keyboard layout
- if (physicalKey.length === 1 && /[a-zA-Z]/.test(physicalKey)) {
- physicalKey = physicalKey.toLowerCase();
- }
- // Map special characters to their base keys
- const specialCharToBase: Record = {
- "!": "1",
- "@": "2",
- "#": "3",
- $: "4",
- "%": "5",
- "^": "6",
- "&": "7",
- "*": "8",
- "(": "9",
- ")": "0",
- _: "-",
- "+": "=",
- "{": "[",
- "}": "]",
- "|": "\\",
- ":": ";",
- '"': "'",
- "<": ",",
- ">": ".",
- "?": "/",
- "~": "`",
- };
- if (specialCharToBase[physicalKey]) {
- physicalKey = specialCharToBase[physicalKey];
- }
-
- // Clear any existing timeout
- if (pressedKeyTimeoutRef.current) {
- clearTimeout(pressedKeyTimeoutRef.current);
- }
-
- setPressedKey(physicalKey);
-
- const isTooLong =
- currentInput.length >= (lines[currentLineIndex]?.length || 0);
- const ALWAYS_LOG = ["Enter", "Backspace", "Tab", "ArrowLeft", "ArrowRight"];
- const PREVENT = [
- "Tab",
- "ArrowUp",
- "ArrowDown",
- "ArrowRight",
- "ArrowLeft",
- "Alt",
- ];
-
- const shouldLog = !isTooLong || !typingKey || ALWAYS_LOG.includes(key);
- if (shouldLog) {
- keyLogsRef.current.push({ key, timestamp: Date.now() });
- }
- if ((e.ctrlKey || e.metaKey) && key.toLowerCase() === "v")
- e.preventDefault();
-
- if (key === "Enter") {
- e.preventDefault();
- const currentLine = lines[currentLineIndex] || [];
- const normalized = currentInput.split("");
-
- if (compareInputWithLineEnter(normalized, currentLine)) {
- setCurrentLineIndex((v) => v + 1);
- setCurrentInput("");
- setCurrentCharIndex(0);
- } else {
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- } else if (PREVENT.includes(key)) {
- e.preventDefault();
- }
- // 타이핑 키는 onChange에서 자동으로 처리됨
- };
-
- const handleKeyUp = () => {
- // Clear any existing timeout
- if (pressedKeyTimeoutRef.current) {
- clearTimeout(pressedKeyTimeoutRef.current);
- pressedKeyTimeoutRef.current = null;
- }
- setPressedKey(null);
- };
-
- const handleInputChange = (e: React.ChangeEvent) => {
- const value = e.target.value;
- const currentLine = lines[currentLineIndex] || [];
-
- // Detect which key was pressed for virtual keyboard
- if (value.length > currentInput.length) {
- // Character was added
- const addedChar = value[value.length - 1];
-
- // Clear any existing timeout
- if (pressedKeyTimeoutRef.current) {
- clearTimeout(pressedKeyTimeoutRef.current);
- }
-
- // For letters, use lowercase for keyboard animation
- const keyForAnimation = /[a-zA-Z]/.test(addedChar)
- ? addedChar.toLowerCase()
- : addedChar === " "
- ? " "
- : addedChar;
-
- setPressedKey(keyForAnimation);
-
- // Clear after a short delay
- pressedKeyTimeoutRef.current = setTimeout(() => {
- setPressedKey(null);
- pressedKeyTimeoutRef.current = null;
- }, 100);
- }
-
- // Start timer on first input
- if (!isStarted && value.length > 0) {
- setIsStarted(true);
- setStartTime(Date.now());
- }
-
- // 라인 길이 제한
- if (value.length <= currentLine.length) {
- setCurrentInput(value);
- setCurrentCharIndex(value.length);
- }
- };
-
- useEffect(() => {
- let prev = 0;
- for (let i = 0; i < currentLineIndex; i++) prev += lineCharCounts[i] || 0;
- const curLine = lines[currentLineIndex] || [];
- const curTyped = calculateCurrentLineTypedChars(currentInput, curLine);
- setTotalTypedChars(prev + curTyped);
- const wrong = compareInputWithLine(currentInput, curLine);
- setWrongChar(wrong);
- }, [currentInput, currentLineIndex, lines, lineCharCounts]);
-
- const handleFinish = async () => {
- if (verifiedOnceRef.current) {
- setIsFinished(true);
- return;
- }
- verifiedOnceRef.current = true;
- try {
- const payload = {
- codeId,
- language: (lang || "").toUpperCase(),
- keyLogs: keyLogsRef.current,
- requestId,
- };
- const encrypted = encryptWithSessionKey(payload);
- const res = await verifiedRecord(encrypted);
- if (res?.status?.code === 200) {
- setCpm(res.content.typingSpeed);
- await postRecord(res.content.verifiedToken, requestId);
- }
- } catch {}
- setIsFinished(true);
- };
-
- const handleVirtualKeyPress = useCallback(
- (k: string) => {
- // Start timer on first keystroke
- if (!isStartedRef.current) {
- const now = Date.now();
- isStartedRef.current = true;
- startTimeRef.current = now;
- setIsStarted(true);
- setStartTime(now);
- }
-
- if (!inputRef.current) return;
-
- // Update using functional updates to avoid stale state
- if (k.length === 1) {
- // Typing key
- setCurrentInput((prev) => {
- const currentLine =
- linesRef.current[currentLineIndexRef.current] || [];
- const newValue = prev + k;
- if (newValue.length <= currentLine.length) {
- if (inputRef.current) {
- inputRef.current.value = newValue;
- }
- setCurrentCharIndex(newValue.length);
- // Total typed chars is calculated in useEffect, don't update here
- return newValue;
- }
- return prev;
- });
- } else if (k === "Backspace") {
- // Backspace
- setCurrentInput((prev) => {
- const newValue = prev.slice(0, -1);
- if (inputRef.current) {
- inputRef.current.value = newValue;
- }
- setCurrentCharIndex(newValue.length);
- return newValue;
- });
- } else if (k === "Enter") {
- // Enter - simplified logic to avoid nested setState
- const currentIdx = currentLineIndexRef.current;
- const currentLine = linesRef.current[currentIdx] || [];
- const currentInputValue = inputRef.current?.value || "";
- const normalized = currentInputValue.split("");
-
- if (compareInputWithLineEnter(normalized, currentLine)) {
- setCurrentLineIndex(currentIdx + 1);
- setCurrentInput("");
- setCurrentCharIndex(0);
- if (inputRef.current) {
- inputRef.current.value = "";
- }
- } else {
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- }
- },
- [] // Empty dependency array - callback never changes
- );
-
- // Show loading or error state
- if (isLoading || loadError || lines.length === 0) {
- return (
-
-
- {isLoading ? (
-
-
- 게임 준비 중...
-
-
- 코드를 불러오는 중입니다
-
-
- ) : loadError ? (
-
-
- 오류 발생
-
-
{loadError}
-
navigate("/single/select/language")}
- >
- 언어 선택으로 돌아가기
-
-
- ) : (
-
코드가 없습니다
- )}
-
-
- );
- }
-
- return (
-
- {/* Container for Board with Logo */}
-
- {/* Main Board Container */}
-
- {/* CODENOVA Logo - On Top Border */}
-
-
-
-
- {/* Main Content Row */}
-
- {/* Left - Black Board Container */}
-
- {/* Code View (scrollable) - Inner Black Board */}
-
-
-
- {lines.map((line, idx) => {
- const normalized = currentInput.split("");
- const indent = space[idx];
- return (
-
- {/* left spaces */}
- {new Array(indent).fill("\u00A0").map((_, i) => (
-
- ))}
- {idx < currentLineIndex && (
-
- {line.map((ch, i) => (
-
- {ch}
-
- ))}
-
- )}
- {idx === currentLineIndex && (
-
- {line.map((ch, i) => {
- const inputCh = normalized[i];
- const isPending = inputCh == null;
- const isMatch = !isPending && inputCh === ch;
- const isWrong = !isPending && !isMatch;
- const className = isPending
- ? "pending currentLine"
- : isMatch
- ? "typed currentLine"
- : "wrong currentLine";
- const isSpace = ch === " ";
- const content = isSpace ? "\u00A0" : ch;
-
- return (
-
- {i === normalized.length && (
-
- )}
- {isWrong && isSpace ? (
-
- {content}
-
- ) : (
-
- {content}
-
- )}
-
- );
- })}
-
- )}
- {idx > currentLineIndex && (
-
- {line.map((ch, i) => (
-
- {ch}
-
- ))}
-
- )}
-
- );
- })}
-
-
-
-
- {/* Input Field */}
-
e.preventDefault()}
- />
-
- {/* Virtual Keyboard - Bottom */}
-
-
-
-
-
- {/* Right - Avatar Panel */}
-
-
-
-
-
- {isFinished && (
-
- {}}
- />
-
- )}
-
- );
-};
-
-export default GamePlayingPage;
diff --git a/src/pages/single/SingleLanguageSelectPage.test.tsx b/src/pages/single/SingleLanguageSelectPage.test.tsx
deleted file mode 100644
index e9f75aa..0000000
--- a/src/pages/single/SingleLanguageSelectPage.test.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { render, screen, fireEvent } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { BrowserRouter } from "react-router-dom";
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import SingleLanguageSelectPage from "./SingleLanguageSelectPage";
-
-const mockNavigate = vi.fn();
-
-vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual("react-router-dom");
- return {
- ...actual,
- useNavigate: () => mockNavigate,
- };
-});
-
-describe("SingleLanguageSelectPage", () => {
- beforeEach(() => {
- mockNavigate.mockClear();
- });
-
- const renderComponent = () => {
- return render(
-
-
-
- );
- };
-
- it("should render language selection title", () => {
- renderComponent();
- expect(screen.getByText("언어선택")).toBeInTheDocument();
- });
-
- it("should render all language buttons", () => {
- renderComponent();
- expect(screen.getByRole("button", { name: "자바" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "파이썬" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "SQL" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "js" })).toBeInTheDocument();
- expect(screen.getByAltText("go")).toBeInTheDocument();
- });
-
- it("should render cancel button", () => {
- renderComponent();
- expect(screen.getByRole("button", { name: "취소" })).toBeInTheDocument();
- });
-
- it("should navigate to java game page when java button is clicked", () => {
- renderComponent();
- const javaBtn = screen.getByRole("button", { name: "자바" });
- fireEvent.click(javaBtn);
- expect(mockNavigate).toHaveBeenCalledWith("/single/game/java");
- });
-
- it("should navigate to python game page when python button is clicked", () => {
- renderComponent();
- const pythonBtn = screen.getByRole("button", { name: "파이썬" });
- fireEvent.click(pythonBtn);
- expect(mockNavigate).toHaveBeenCalledWith("/single/game/python");
- });
-
- it("should navigate to sql game page when sql button is clicked", () => {
- renderComponent();
- const sqlBtn = screen.getByRole("button", { name: "SQL" });
- fireEvent.click(sqlBtn);
- expect(mockNavigate).toHaveBeenCalledWith("/single/game/sql");
- });
-
- it("should navigate to js game page when js button is clicked", () => {
- renderComponent();
- const jsBtn = screen.getByRole("button", { name: "js" });
- fireEvent.click(jsBtn);
- expect(mockNavigate).toHaveBeenCalledWith("/single/game/js");
- });
-
- it("should navigate to main page when cancel button is clicked", () => {
- renderComponent();
- const cancelBtn = screen.getByRole("button", { name: "취소" });
- fireEvent.click(cancelBtn);
- expect(mockNavigate).toHaveBeenCalledWith("/main");
- });
-
- it("should have proper button sizes - not too large", () => {
- const { container } = renderComponent();
- const javaBtn = screen.getByRole("button", {
- name: "자바",
- }) as HTMLImageElement;
-
- // className을 통해 스타일이 적용되었는지 확인
- expect(javaBtn.className).toBeTruthy();
- expect(javaBtn).toBeInTheDocument();
-
- // 모든 언어 버튼이 렌더링되었는지 확인
- expect(screen.getByRole("button", { name: "자바" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "파이썬" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "SQL" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "js" })).toBeInTheDocument();
- });
-
- it("should have BoardContainer with appropriate styling", () => {
- const { container } = renderComponent();
-
- // BoardContainer의 배경 이미지가 있는지 확인
- const boardContainer = container.querySelector('[style*="background"]');
- expect(boardContainer).not.toBeNull();
-
- // "언어선택" 텍스트가 BoardContainer 내부에 있는지 확인
- expect(screen.getByText("언어선택")).toBeInTheDocument();
- });
-
- it("should show go language as locked", () => {
- renderComponent();
- const goBtn = screen.getByAltText("go");
- const lockIcon = screen.getByAltText("lock");
-
- expect(goBtn).toBeInTheDocument();
- expect(lockIcon).toBeInTheDocument();
- });
-
- it("should open tutorial modal when header button is clicked", () => {
- renderComponent();
- // Note: Header의 튜토리얼 버튼을 클릭하는 테스트는 Header 컴포넌트의 구현에 따라 다를 수 있음
- // 여기서는 모달이 조건부 렌더링되는 것만 확인
- });
-
- describe("Responsive Layout", () => {
- it("should have responsive button sizes", () => {
- const { container } = renderComponent();
- const javaBtn = screen.getByRole("button", { name: "자바" });
-
- // 기본 스타일이 적용되었는지 확인
- expect(javaBtn).toBeInTheDocument();
- expect(javaBtn.className).toBeTruthy();
- });
-
- it("should have responsive container layout", () => {
- const { container } = renderComponent();
-
- // 버튼 컨테이너가 존재하는지 확인
- const buttonContainer = container.querySelector('[class*="flex"]');
- expect(buttonContainer).toBeInTheDocument();
- });
-
- it("should have proper spacing between elements", () => {
- renderComponent();
-
- // 모든 언어 버튼이 렌더링되었는지 확인
- expect(screen.getByRole("button", { name: "자바" })).toBeInTheDocument();
- expect(
- screen.getByRole("button", { name: "파이썬" })
- ).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "SQL" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "js" })).toBeInTheDocument();
- expect(screen.getByAltText("go")).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "취소" })).toBeInTheDocument();
- });
-
- it("should have responsive title positioning", () => {
- renderComponent();
-
- const title = screen.getByText("언어선택");
- expect(title).toBeInTheDocument();
- expect(title.className).toBeTruthy();
- });
-
- it("should have proper hover effects", () => {
- renderComponent();
-
- const javaBtn = screen.getByRole("button", { name: "자바" });
- expect(javaBtn).toBeInTheDocument();
-
- // hover 효과가 CSS에 정의되어 있는지 확인 (Panda CSS 클래스명)
- expect(javaBtn.className).toContain("hover:");
- });
-
- it("should have proper accessibility attributes", () => {
- renderComponent();
-
- // 모든 버튼이 적절한 role과 alt 속성을 가지는지 확인
- expect(screen.getByRole("button", { name: "자바" })).toBeInTheDocument();
- expect(
- screen.getByRole("button", { name: "파이썬" })
- ).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "SQL" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "js" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "취소" })).toBeInTheDocument();
-
- // GO 버튼은 비활성화되어 있으므로 alt 속성만 확인
- expect(screen.getByAltText("go")).toBeInTheDocument();
- expect(screen.getByAltText("lock")).toBeInTheDocument();
- });
-
- it("should handle different screen sizes gracefully", () => {
- // 모바일 크기 시뮬레이션
- Object.defineProperty(window, "innerWidth", {
- writable: true,
- configurable: true,
- value: 375,
- });
-
- renderComponent();
-
- // 모든 요소가 여전히 렌더링되는지 확인
- expect(screen.getByText("언어선택")).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "자바" })).toBeInTheDocument();
- });
- });
-});
diff --git a/src/pages/single/SingleLanguageSelectPage.tsx b/src/pages/single/SingleLanguageSelectPage.tsx
deleted file mode 100644
index f94f810..0000000
--- a/src/pages/single/SingleLanguageSelectPage.tsx
+++ /dev/null
@@ -1,309 +0,0 @@
-import backgroundImg from "../../assets/images/single_background.jpg";
-import javaBtn from "../../assets/images/java_button.png";
-import pythonBtn from "../../assets/images/python_button.png";
-import sqlBtn from "../../assets/images/SQL_button.png";
-import jsBtn from "../../assets/images/js_button.png";
-import goBtn from "../../assets/images/go_button.png";
-import lockIcon from "../../assets/images/lock_icon.png";
-import cancelBtn from "../../assets/images/cancel_btn.png";
-import BoardContainer from "../../components/single/BoardContainer";
-import Header from "../../components/common/Header";
-import TutoModal from "../../components/common/TutoModal";
-import { useNavigate } from "react-router-dom";
-import { useState } from "react";
-import SettingModal from "../../components/modal/SettingModal";
-import RankingModal from "../../components/modal/RankingModal";
-import { Box } from "../../../styled-system/jsx";
-import { css } from "../../../styled-system/css";
-
-const SingleLanguageSelectPage: React.FC = () => {
- const navigate = useNavigate();
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
-
- return (
-
- {showTutoModal && (
-
- setShowTutoModal(false)} />
-
- )}
- {showSettingModal && (
- setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
-
- setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- onShowRanking={() => setShowRankingModal(true)}
- />
-
-
-
-
- 언어선택
-
-
- {/* 버튼 컨테이너 - 크기와 gap 조정 */}
-
-
navigate("/single/game/java")}
- />
-
navigate("/single/game/python")}
- />
-
navigate("/single/game/sql")}
- />
-
navigate("/single/game/js")}
- />
-
-
-
-
- GO언어 추후 출시 예정!!
-
-
-
-
- {/* 취소 버튼 - 크기 조정 */}
-
-
navigate("/main")}
- />
-
-
-
- );
-};
-
-export default SingleLanguageSelectPage;
diff --git a/src/pages/single/SinglePage.tsx b/src/pages/single/SinglePage.tsx
deleted file mode 100644
index a5e80c8..0000000
--- a/src/pages/single/SinglePage.tsx
+++ /dev/null
@@ -1,759 +0,0 @@
-import backgroundImg from "../../assets/images/single_background.jpg";
-import box from "../../assets/images/board1_cut.jpg";
-import logo from "../../assets/images/logo.png";
-import Keyboard from "../../components/keyboard/Keyboard";
-import { Box } from "../../../styled-system/jsx";
-import { css } from "../../../styled-system/css";
-
-import { getAccessToken } from "../../utils/tokenUtils";
-import { useNavigate, useParams } from "react-router-dom";
-import { useEffect, useState, useRef } from "react";
-
-import hljs from "highlight.js/lib/core";
-import javascript from "highlight.js/lib/languages/javascript";
-import java from "highlight.js/lib/languages/java";
-import python from "highlight.js/lib/languages/python";
-import sql from "highlight.js/lib/languages/sql";
-import "highlight.js/styles/atom-one-dark.css";
-import "../../styles/single/SinglePage.css";
-import ProgressBox from "../../components/single/ProgressBox";
-
-import {
- calculateCPM,
- getProgress,
- processCode,
- compareInputWithLineEnter,
- compareInputWithLine,
- calculateCurrentLineTypedChars,
-} from "../../utils/typingUtils";
-import FinishPage from "../single/modal/FinishPage";
-
-import {
- singleLangCode,
- getLangCode,
- verifiedRecord,
- postRecord,
-} from "../../api/singleApi";
-import { userColorStore } from "../../store/userSettingStore";
-import CodeDescription from "../../components/single/CodeDescription";
-import { encryptWithSessionKey } from "../../utils/cryptoUtils";
-
-// 등록
-hljs.registerLanguage("java", java);
-hljs.registerLanguage("python", python);
-hljs.registerLanguage("javascript", javascript);
-hljs.registerLanguage("sql", sql);
-
-interface KeyLog {
- key: string;
- timestamp: number;
-}
-
-const SinglePage: React.FC = () => {
- const navigate = useNavigate();
- const { lang } = useParams<{ lang: string }>();
-
- const [userType, setUserType] = useState(null);
-
- // 코드 입력 관련 상태관리
- const [codeId, setCodeId] = useState(null);
- const [lines, setLines] = useState([]);
- const [linesCharCount, setlinesCharCount] = useState([]);
- const [space, setSpace] = useState([]);
- const [currentLineIndex, setCurrentLineIndex] = useState(0);
- const [currentInput, setCurrentInput] = useState("");
- const [currentCharIndex, setCurrentCharIndex] = useState(0);
- const [wrongChar, setWrongChar] = useState(false);
- const [shake, setShake] = useState(false);
-
- // 포커스 관련 상태관리
- const inputAreaRef = useRef(null);
- const [isFocused, setIsFocused] = useState(false);
-
- // 시간 및 달성률 상태관리
- const [startTime, setStartTime] = useState(null);
- const [elapsedTime, setElapsedTime] = useState(0);
- const [isStarted, setIsStarted] = useState(false);
-
- const [progress, setProgress] = useState(0);
-
- // 전체 타이핑한 글자수 상태관리
- const [totalTypedChars, setTotalTypedChars] = useState(0);
- const [cpm, setCpm] = useState(0);
-
- // 완료 상태 관리
- const [isFinished, setIsFinished] = useState(false);
-
- const [requestId, setRequestId] = useState("");
-
- // 자동으로 내려가게
- const codeContainerRef = useRef(null);
-
- const [logCount, setLogCount] = useState(0);
- const keyLogsRef = useRef([]);
- const hasVerifiedRef = useRef(false);
-
- const initColors = userColorStore((state: any) => state.initColors);
-
- const [showCodeDescription, setShowCodeDescription] =
- useState(false);
-
- useEffect(() => {
- const auth = JSON.parse(localStorage.getItem("auth-storage") || "{}");
- setUserType(auth?.state?.user?.userType);
- initColors();
-
- if (inputAreaRef.current) {
- inputAreaRef.current.focus();
- }
- document.addEventListener("click", handleClickOutside);
- return () => {
- document.removeEventListener("click", handleClickOutside);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- // 포커스를 항상 유지
- useEffect(() => {
- if (inputAreaRef.current && isFocused && !isFinished) {
- inputAreaRef.current.focus();
- }
- }, [isFocused, isFinished]);
-
- useEffect(() => {
- if (!isFinished) {
- document.addEventListener("click", handleClickOutside);
- } else {
- document.removeEventListener("click", handleClickOutside);
- }
-
- return () => {
- document.removeEventListener("click", handleClickOutside);
- };
- }, [isFinished]);
-
- // 외부 클릭시 포커스를 유지
- const handleClickOutside = (e: MouseEvent): void => {
- if (
- inputAreaRef.current &&
- !inputAreaRef.current.contains(e.target as Node)
- ) {
- e.preventDefault();
- inputAreaRef.current.focus();
- }
- };
-
- useEffect(() => {
- if (lang) {
- singleLangCode(lang)
- .then((data) => {
- const { lines, space, charCount } = processCode(data.content);
- setCodeId(data.codeId);
- setLines(lines);
- setSpace(space);
- setlinesCharCount(charCount);
- setRequestId(data.requestId);
- })
- .catch((e) => {
- // console.error("api 요청 실패:" , e)
- });
- }
- }, [lang]);
-
- const getLanguageClass = (lang: string | undefined): string => {
- if (!lang) {
- return "";
- }
-
- const lowerLang = lang.toLowerCase();
-
- if (lowerLang === "java") return "language-java";
- else if (lowerLang === "python") return "language-python";
- else if (lowerLang === "js") return "language-javascript";
- else if (lowerLang === "sql") return "language-sql";
- else return "";
- };
-
- const handleKeyDown = (e: React.KeyboardEvent): void => {
- if (!isStarted) {
- setStartTime(Date.now());
- setIsStarted(true);
- }
-
- const key = e.key;
-
- // ↓ 입력 길이 제한 확인
- const isTypingKey = key.length === 1;
- const isInputTooLong =
- currentInput.length >= lines[currentLineIndex]?.length;
- const ALWAYS_LOG_KEYS = [
- "Enter",
- "Backspace",
- "Tab",
- "ArrowLeft",
- "ArrowRight",
- ];
- const PREVENT_KEYS = [
- "Tab",
- "ArrowUp",
- "ArrowDown",
- "ArrowRight",
- "ArrowLeft",
- "Alt",
- ];
-
- const shouldLog =
- !isInputTooLong || !isTypingKey || ALWAYS_LOG_KEYS.includes(key);
-
- if (shouldLog) {
- const newLog: KeyLog = {
- key: key,
- timestamp: Date.now(),
- };
- keyLogsRef.current.push(newLog);
- setLogCount((prev) => prev + 1);
- }
-
- if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "v") {
- e.preventDefault();
- }
-
- if (key === "Enter") {
- e.preventDefault();
-
- const currentLine = lines[currentLineIndex];
- const normalizedInput = currentInput.split("");
-
- if (compareInputWithLineEnter(normalizedInput, currentLine)) {
- setCurrentLineIndex((prev) => prev + 1);
- setCurrentInput("");
- setCurrentCharIndex(0);
- } else {
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- } else if (PREVENT_KEYS.includes(key)) {
- e.preventDefault();
- } else if (key === "Backspace") {
- if (currentCharIndex > 0) {
- setCurrentCharIndex((prev) => prev - 1);
- }
- }
- };
-
- // 터치용
- const handleVirtualKeyInput = (key: string): void => {
- if (!isStarted) {
- setStartTime(Date.now());
- setIsStarted(true);
- }
-
- const isTypingKey = key.length === 1;
- const isInputTooLong =
- currentInput.length >= lines[currentLineIndex]?.length;
- const ALWAYS_LOG_KEYS = [
- "Enter",
- "Backspace",
- "Tab",
- "ArrowLeft",
- "ArrowRight",
- ];
- const shouldLog =
- !isInputTooLong || !isTypingKey || ALWAYS_LOG_KEYS.includes(key);
-
- if (shouldLog) {
- const newLog: KeyLog = {
- key: key,
- timestamp: Date.now(),
- };
- keyLogsRef.current.push(newLog);
- setLogCount((prev) => prev + 1);
- }
-
- if (key === "Enter") {
- const currentLine = lines[currentLineIndex];
- const normalizedInput = currentInput.split("");
-
- if (compareInputWithLineEnter(normalizedInput, currentLine)) {
- setCurrentLineIndex((prev) => prev + 1);
- setCurrentInput("");
- setCurrentCharIndex(0);
- } else {
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- } else if (key === "Backspace") {
- if (currentCharIndex > 0) {
- setCurrentInput((prev) => prev.slice(0, -1));
- setCurrentCharIndex((prev) => prev - 1);
- }
- } else if (isTypingKey) {
- const updated = currentInput + key;
- const currentLine = lines[currentLineIndex];
- if (updated.length <= currentLine.length) {
- setCurrentInput(updated);
- setCurrentCharIndex((prev) => prev + 1);
- }
- }
- };
-
- useEffect(() => {
- let timer: NodeJS.Timeout;
-
- if (isStarted && !isFinished && startTime) {
- timer = setInterval(() => {
- setElapsedTime(Date.now() - startTime);
- }, 10);
- }
-
- return () => {
- if (timer) clearInterval(timer);
- };
- }, [isStarted, startTime, isFinished]);
-
- useEffect(() => {
- setCpm(calculateCPM(totalTypedChars, elapsedTime / 1000));
- }, [elapsedTime, totalTypedChars]);
-
- const verifiedResult = async (): Promise => {
- if (hasVerifiedRef.current) return;
- hasVerifiedRef.current = true;
-
- const data = {
- codeId: codeId,
- language: lang?.toUpperCase() || "",
- keyLogs: keyLogsRef.current,
- requestId: requestId,
- };
- try {
- const encryptedData = encryptWithSessionKey(data);
- const response = await verifiedRecord(encryptedData);
- const { code, message } = response.status;
- if (code === 200) {
- setCpm(response.content.typingSpeed);
- await postResult(response.content.verifiedToken);
- }
- } catch (e) {
- // console.log(e)
- }
- };
-
- // 검증완료했으면 저장 로직 수행
- const postResult = async (token: string): Promise => {
- try {
- const response = await postRecord(token, requestId);
- const { code, message } = response.status;
-
- if (code === 200) {
- if (response.content.isNewRecord) {
- alert(message);
- }
- }
- } catch (e) {
- // console.error("postResult error:", e);
- }
- };
-
- useEffect(() => {
- setProgress(getProgress(currentLineIndex, lines.length));
-
- if (lines.length > 0 && currentLineIndex === lines.length) {
- if (userType === "member") {
- verifiedResult();
- }
- setIsFinished(true);
- }
-
- if (codeContainerRef.current && currentLineIndex > 0) {
- const lineElements =
- codeContainerRef.current.querySelectorAll(".codeLine");
-
- const lineHeight =
- lineElements[currentLineIndex]?.getBoundingClientRect().height || 28;
-
- codeContainerRef.current.scrollTop += lineHeight;
- codeContainerRef.current.scrollLeft = 0;
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentLineIndex, lines.length, userType]);
-
- useEffect(() => {
- const container = codeContainerRef.current;
- const cursorEl = document.querySelector(".cursor");
- if (container && cursorEl) {
- const containerRect = container.getBoundingClientRect();
- const cursorRect = cursorEl.getBoundingClientRect();
-
- const padding = 50;
-
- if (cursorRect.right > containerRect.right - padding) {
- container.scrollLeft += 400;
- }
-
- if (cursorRect.left < containerRect.left + 20) {
- container.scrollLeft -= 400;
- }
- }
- }, [currentInput]);
-
- const handleInputChange = (e: React.ChangeEvent): void => {
- const value = e.target.value;
- const currentLine = lines[currentLineIndex] || [];
-
- if (value.length <= currentLine.length) {
- setCurrentInput(value);
- } else {
- setCurrentInput(value.slice(0, currentLine.length));
- }
- };
-
- useEffect(() => {
- updateTotalTypedChars();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentInput, currentLineIndex]);
-
- const updateTotalTypedChars = (): void => {
- let previousLinesChars = 0;
- for (let i = 0; i < currentLineIndex; i++) {
- previousLinesChars += linesCharCount[i] || 0;
- }
-
- const currentLine = lines[currentLineIndex] || [];
- const currentLineChars = calculateCurrentLineTypedChars(
- currentInput,
- currentLine
- );
- setTotalTypedChars(previousLinesChars + currentLineChars);
-
- const hasWrongChar = compareInputWithLine(currentInput, currentLine);
- setWrongChar(hasWrongChar);
- };
-
- return (
-
- {/* 타자게임 박스 */}
-
-
-
-
-
- {/* 콘텐츠 박스들 */}
-
- {/* 왼쪽 컨텐츠 영역 */}
-
-
setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
- tabIndex={0}
- onKeyDown={handleKeyDown}
- >
-
-
- {lines.map((line, idx) => {
- const normalizedInput = currentInput.split("");
- const currentLine = line;
-
- const lineWithSpace = space[idx];
-
- return (
-
- {idx < currentLineIndex ? (
- // 이미 완료한 줄
-
- {new Array(lineWithSpace)
- .fill("\u00A0")
- .map((_, spaceIndex) => (
-
- ))}
- {line.map((char, i) => (
-
- {char}
-
- ))}
-
- ) : idx === currentLineIndex ? (
- // 현재 타이핑 중인 줄
-
- {new Array(lineWithSpace)
- .fill("\u00A0")
- .map((_, spaceIndex) => (
-
- ))}
- {currentLine.map((char, i) => {
- const inputChar = normalizedInput[i];
-
- let className = "";
-
- if (inputChar == null) {
- className = "pending currentLine";
- } else if (inputChar === char) {
- className = "typed currentLine";
- } else {
- if (char === " ") {
- className = "wrong currentLine";
- } else {
- className = "wrong currentLine";
- }
- }
-
- return (
-
- {i === normalizedInput.length && (
-
- )}
-
- {char === " " ? "\u00A0" : char}
-
-
- );
- })}
-
- ) : (
- // 아직 안친 줄
-
- {new Array(lineWithSpace)
- .fill("\u00A0")
- .map((_, spaceIndex) => (
-
- ))}
- {line.map((char, i) => (
-
- {char}
-
- ))}
-
- )}
-
- );
- })}
-
-
-
- {/* 유저가 타이핑한 코드가 보이는 곳 */}
-
setIsFocused(true)}
- placeholder="여기에 타이핑하세요"
- style={{ pointerEvents: "none" }}
- onPaste={(e) => e.preventDefault()}
- />
-
-
-
-
-
-
-
- {/* 오른쪽 콘텐츠 박스 */}
-
-
-
-
- {isFinished && (
-
- setShowCodeDescription(true)}
- />
-
- )}
- {showCodeDescription &&
- (userType === "member" ? (
-
- setShowCodeDescription(false)}
- lang={lang?.toUpperCase() || ""}
- codeId={codeId ?? 0}
- />
-
- ) : (
- <>
- {/* 경고 메시지 띄우기 */}
-
-
- 회원 전용 기능입니다.
- setShowCodeDescription(false)}
- >
- 닫기
-
-
-
- >
- ))}
-
- );
-};
-
-export default SinglePage;
diff --git a/src/pages/single/SinglePageV1.tsx b/src/pages/single/SinglePageV1.tsx
deleted file mode 100644
index b963204..0000000
--- a/src/pages/single/SinglePageV1.tsx
+++ /dev/null
@@ -1,382 +0,0 @@
-// @ts-nocheck
-import backgroundImg from '../../assets/images/single_background.jpg'
-import box from '../../assets/images/board1_cut.jpg'
-import logo from '../../assets/images/logo.png'
-import Keyboard from '../../components/keyboard/Keyboard'
-import Header from "../../components/common/Header"
-
-import { getAccessToken } from "../../utils/tokenUtils";
-import { useNavigate, useParams, useLocation } from 'react-router-dom'
-import { useEffect, useState, useRef } from 'react'
-
-import hljs from 'highlight.js/lib/core';
-import javascript from 'highlight.js/lib/languages/javascript';
-import java from 'highlight.js/lib/languages/java';
-import python from 'highlight.js/lib/languages/python';
-import sql from 'highlight.js/lib/languages/sql';
-import 'highlight.js/styles/atom-one-dark.css';
-import '../../styles/single/SinglePage.css';
-import ProgressBox from '../../components/single/ProgressBox'
-
-import { calculateCPM, getProgress } from '../../utils/typingUtils';
-import FinishPage from '../single/modal/FinishPage';
-
-import { singleCsCode, singleLangCode, getLangCode } from '../../api/singleApi'
-
-// 등록
-hljs.registerLanguage('java', java);
-hljs.registerLanguage('python', python);
-hljs.registerLanguage('javascript', javascript);
-hljs.registerLanguage('sql', sql);
-
-
-const SinglePage = () => {
-
- const navigate = useNavigate();
- const location = useLocation();
- const query = new URLSearchParams(location.search);
- const category = query.get('category') // "DATABASE", "NETWORK", "OS", "DATA_STRUCTURE", "COMPUTER_STRUCTURE"
- const { lang } = useParams();
-
-
- // 코드 입력 관련 상태관리
- const [rawCode, setRawCode] = useState(""); // API 받은 순수 코드
- // const [highlightedCode, setHighlightedCode] = useState(""); // 하이라이트된 HTML 코드 안써도 될듯 이거
- const [lines, setLines] = useState([]);
- const [currentLineIndex, setCurrentLineIndex] = useState(0);
- const [currentInput, setCurrentInput] = useState(""); //사용자가 입력한 문자열
- const [wrongChar, setWrongChar] = useState(false); // 현재까지 입력한 input중에 틀림 존재 여부 상태 관리
- const [shake, setShake] = useState(false); // 오타 입력창 흔들기 모션션
-
- // 포커스 관련 상태관리
- const inputAreaRef = useRef(null);
- const [isFocused, setIsFocused] = useState(false);
-
-
- // 시간 및 달성률 상태관리
- const [startTime, setStartTime] = useState(null);
- const [elapsedTime, setElapsedTime] = useState(0);
- const [isStarted, setIsStarted] = useState(false);
-
- const [progress, setProgress] = useState(0);
-
- // 전체 타이핑한 글자수 상태관리
- const [totalTypedChars, setTotalTypedChars] = useState(0);
- const [cpm, setCpm] = useState(0);
-
- // 완료 상태 관리
- const [isFinished, setIsFinished] = useState(false);
-
- // 자동으로 내려가게
- const codeContainerRef = useRef(null);
-
- const [CScode, setCScode] = useState([]);
- const [isCs , setIsCs] = useState(false);
-
- useEffect(() => {
- if (inputAreaRef.current) {
- inputAreaRef.current.focus();
- }
- },[])
-
- useEffect(() => {
- const accessToken = getAccessToken();
- // console.log(accessToken)
- if (!accessToken) {
- alert("로그인이 필요합니다");
- navigate("/auth/login");
- }
- }, [navigate]);
-
- useEffect(() => {
- if (lang) {
- if (lang === 'cs') {
- setIsCs(true);
- singleCsCode(category)
- .then(data => {
- setCScode(data);
- })
- .catch(e => {
- // console.error("api 요청 실패:" , e)
- })
- } else {
- setIsCs(false);
- singleLangCode(lang)
- // getLangCode(97)
- .then(data => {
- // console.log("api 결과", data);
- setRawCode(data);
- })
- .catch(e => {
- // console.error("api 요청 실패:" , e)
- })
-
- }
-
- }
- },[lang])
-
- useEffect(() => { //줄 단위로 나누기
- // console.log(rawCode);
- // setLines(rawCode.split('\n'));
- setLines(rawCode.split('\n').filter(line => line.trim() !== '')); // 양쪽에 빈 공백이 있음
-
- }, [rawCode]);
-
- useEffect(() => {
- if (!Array.isArray(CScode)) return;
-
- // console.log(CScode);
- // const allLines = CScode.map((item) => `${item.keyword} - ${item.content}`);
- // setLines(allLines); // 하나의 배열로 상태 저장
- const allLines = CScode.map((item) => `${item.keyword} - ${item.content}`);
- setLines(allLines); // 하나의 배열로 상태 저장
- }, [CScode])
-
- const normalizeLineReTab = (line) => {
- // 앞뒤 공백과 탭을 제거
- return line.trim().replace(/\t/g, '');
- }
-
- const getLanguageClass = (lang) => {
- if (!lang) {
- return '';
- }
-
- const lowerLang = lang.toLowerCase();
-
- if (lowerLang === "java") return 'language-java';
- else if (lowerLang === "python") return 'language-python';
- else if (lowerLang === "js") return 'language-javascript';
- else if (lowerLang === "sql") return 'language-sql';
- else return '';
-
- }
-
- const handleKeyDown = (e) => {
-
- if (!isStarted) {
- setStartTime(Date.now())
- setIsStarted(true);
- }
-
- const key = e.key;
-
- if (key === 'Enter') {
- e.preventDefault(); // 기본적으로 엔터줄바꾸는거 막기
-
- const currentLine = lines[currentLineIndex];
- const normalizedInput = normalizeLineReTab(currentInput);
- const normalizedLine = normalizeLineReTab(currentLine);
-
- if (normalizedInput === normalizedLine) { //다 맞게 쳤으면
- setCurrentLineIndex((prev) => prev + 1);
- setCurrentInput('');
-
- } else { // 틀렸으면
- // console.log('현재 줄을 정확히 입력하지 않음')
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- }
- else if (key === 'Tab') {
- e.preventDefault(); //
- // setCurrentInput((prev) => prev + '\t'); 일단 탭을 막아놓기
- }
-
- else if (key === 'Backspace') {
- setTotalTypedChars(prev => prev - 1); // 글자를 지웠을 때 타수 감소
- }
- };
-
-
-
- useEffect(() => {
- let timer;
-
- if (isStarted && !isFinished) {
- timer = setInterval(() => {
- setElapsedTime(Date.now() - startTime);
- }, 10);
- }
-
- return () => {
- if (timer) clearInterval(timer);
- };
-
- }, [isStarted, startTime, isFinished])
-
- useEffect(() => {
- setCpm(calculateCPM(totalTypedChars, elapsedTime / 1000 ))
- }, [elapsedTime])
-
- useEffect(() => {
- setProgress(getProgress(currentLineIndex, lines.length))
-
- if( lines.length > 0 && currentLineIndex === lines.length) {
- setIsFinished(true);
- }
-
- if (codeContainerRef.current && currentLineIndex > 0) {
- const lineElements = codeContainerRef.current.querySelectorAll('div');
-
- // 현재 줄의 높이를 계산하여, 스크롤 위치를 조정
- const lineHeight = lineElements[currentLineIndex]?.getBoundingClientRect().height || 28; // 한 줄의 높이 계산
-
- // 스크롤을 자동으로 내리기
- codeContainerRef.current.scrollTop += lineHeight + 10;
- }
- }, [currentLineIndex])
-
- const getLeadingWhitespaceCount = (line) => {
- // 앞부분에서 공백과 탭을 세는 정규식
- const match = line.match(/^(\t| {4})*/);
- return match ? match[0].length : 0; // 매칭된 부분의 길이를 반환
- }
-
- const handleInputChange = (e) => {
- const value = e.target.value;
- setCurrentInput(value)
- const normalizedLine = normalizeLineReTab(lines[currentLineIndex])
- const hasWrongChar = normalizedLine.slice(0, value.length) !== value;
- setWrongChar(hasWrongChar);
- if (!hasWrongChar) {
- // 타수 업데이트: 입력된 글자 수를 계산하여 타자 속도 측정
- setTotalTypedChars((prev) => prev + 1);
- }
- };
-
- return (
-
- {/*
*/}
- {/* 타자게임 박스 */}
-
-
-
-
-
-
- {/* 콘텐츠 박스들 */}
-
- {/* 왼쪽 컨텐츠 영역 */}
-
-
setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
- tabIndex={0}
- onKeyDown={handleKeyDown}
- style={{
- backgroundColor: '#1C1C1C',
- borderColor: '#51E2F5',
- }}
- >
-
- {/* */}
-
-
- {lines.map((line, idx) => {
-
- //line앞에 tab이 있는지 확인하는 메서드로 있는만큼 현재줄에 탭 넣어주게 할 예정
- const normalizedInput = normalizeLineReTab(currentInput);
-
- const normalizedLineReTab = normalizeLineReTab(line);
- const lineWithSpace = getLeadingWhitespaceCount(line);
- return (
-
- {idx < currentLineIndex ? (
-
- // 이미 완료 한 줄
- {line}
- ) : idx === currentLineIndex ? (
-
- // 현재 타이핑 중인 줄
- // 여기에 공백 넣기 으로
-
-
- {new Array(lineWithSpace).fill('\u00A0').map((_, spaceIndex) => (
- // 탭 크기만큼 공백 추가
- ))}
- { normalizedLineReTab.split('').map((char,i) => {
- const inputChar = normalizedInput[i];
- let className = '';
-
- if (inputChar == null) {
- className = 'pending currentLine';
- } else if (inputChar === char) {
- className = 'typed currentLine';
- } else {
- className = 'wrong currentLine';
- }
-
- return (
-
- {char === ' ' ? '\u00A0' : char}
-
- );
- })}
-
-
- ) : (
- // 아직 안친줄
- {line}
- )}
-
- );
- })}
-
-
-
-
- {/* 유저가 타이핑한 코드가 보이는 곳 */}
-
-
-
-
-
-
-
-
-
-
- {/* 오른쪽 콘텐츠 박스 */}
-
-
-
-
- {isFinished && (
-
-
-
- )}
-
- )
-};
-
-export default SinglePage
\ No newline at end of file
diff --git a/src/pages/single/SinglePageV2.tsx b/src/pages/single/SinglePageV2.tsx
deleted file mode 100644
index 6501456..0000000
--- a/src/pages/single/SinglePageV2.tsx
+++ /dev/null
@@ -1,475 +0,0 @@
-// @ts-nocheck
-import backgroundImg from '../../assets/images/single_background.jpg'
-import box from '../../assets/images/board1_cut.jpg'
-import logo from '../../assets/images/logo.png'
-import Keyboard from '../../components/keyboard/Keyboard'
-
-
-import { getAccessToken } from "../../utils/tokenUtils";
-import { useNavigate, useParams, useLocation } from 'react-router-dom'
-import { useEffect, useState, useRef } from 'react'
-
-import hljs from 'highlight.js/lib/core';
-import javascript from 'highlight.js/lib/languages/javascript';
-import java from 'highlight.js/lib/languages/java';
-import python from 'highlight.js/lib/languages/python';
-import sql from 'highlight.js/lib/languages/sql';
-import 'highlight.js/styles/atom-one-dark.css';
-import '../../styles/single/SinglePage.css';
-import ProgressBox from '../../components/single/ProgressBox'
-
-import { calculateCPM, getProgress, processCode, compareInputWithLineEnter, compareInputWithLine, calculateCurrentLineTypedChars } from '../../utils/typingUtils';
-import FinishPage from '../single/modal/FinishPage';
-
-import { singleCsCode, singleLangCode, getLangCode } from '../../api/singleApi'
-
-// 등록
-hljs.registerLanguage('java', java);
-hljs.registerLanguage('python', python);
-hljs.registerLanguage('javascript', javascript);
-hljs.registerLanguage('sql', sql);
-
-
-const SinglePage = () => {
-
- const navigate = useNavigate();
- const location = useLocation();
- const query = new URLSearchParams(location.search);
- const category = query.get('category') // "DATABASE", "NETWORK", "OS", "DATA_STRUCTURE", "COMPUTER_STRUCTURE"
- const { lang } = useParams();
-
-
- // 코드 입력 관련 상태관리
- // const [highlightedCode, setHighlightedCode] = useState(""); // 하이라이트된 HTML 코드 안써도 될듯 이거
- const [codeId, setCodeId] = useState(null);
- const [lines, setLines] = useState([]);
- const [linesCharCount, setlinesCharCount] = useState([]);
- const [space, setSpace] = useState([]);
- const [currentLineIndex, setCurrentLineIndex] = useState(0);
- const [currentInput, setCurrentInput] = useState(""); //사용자가 입력한 문자열
- const [currentCharIndex, setCurrentCharIndex] = useState(0);
- const [wrongChar, setWrongChar] = useState(false); // 현재까지 입력한 input중에 틀림 존재 여부 상태 관리
- const [shake, setShake] = useState(false); // 오타 입력창 흔들기 모션션
-
- // 포커스 관련 상태관리
- const inputAreaRef = useRef(null);
- const [isFocused, setIsFocused] = useState(false);
-
-
- // 시간 및 달성률 상태관리
- const [startTime, setStartTime] = useState(null);
- const [elapsedTime, setElapsedTime] = useState(0);
- const [isStarted, setIsStarted] = useState(false);
-
- const [progress, setProgress] = useState(0);
-
- // 전체 타이핑한 글자수 상태관리
- const [totalTypedChars, setTotalTypedChars] = useState(0);
- const [cpm, setCpm] = useState(0);
-
- // 완료 상태 관리
- const [isFinished, setIsFinished] = useState(false);
-
- // 자동으로 내려가게
- const codeContainerRef = useRef(null);
-
- const [CScode, setCScode] = useState([]);
- const [isCs , setIsCs] = useState(false);
-
- useEffect(() => {
- if (inputAreaRef.current) {
- inputAreaRef.current.focus();
- }
- },[])
-
- useEffect(() => {
- const accessToken = getAccessToken();
- // console.log(accessToken)
- if (!accessToken) {
- alert("로그인이 필요합니다");
- navigate("/auth/login");
- }
- }, [navigate]);
-
- // 일단 다시시작하면 그코드 다시 시작
- const resetGame = () => {
- //setLines([]); // 코드 줄 초기화
- //setlinesCharCount([]); // 줄별 글자 수 초기화
- //setSpace([]); // 공백 개수 초기화
- setCurrentLineIndex(0); // 현재 줄 인덱스 초기화
- setCurrentInput(""); // 현재 입력 초기화
- setCurrentCharIndex(0); // 현재 문자 인덱스 초기화
- setWrongChar(false); // 오타 여부 초기화
- setShake(false); // 흔들기 효과 초기화
-
- setStartTime(null); // 시작 시간 초기화
- setElapsedTime(0); // 경과 시간 초기화
- setIsStarted(false); // 게임 시작 상태 초기화
-
- setProgress(0); // 달성률 초기화
- setTotalTypedChars(0); // 전체 타자 수 초기화
- setCpm(0); // 타자 속도 초기화
-
- setIsFinished(false); // 완료 상태 초기화
-
- inputAreaRef.current?.focus();
- }
-
- useEffect(() => {
- if (lang) {
- if (lang === 'cs') {
- setIsCs(true);
- singleCsCode(category)
- .then(data => {
- setCScode(data);
- })
- .catch(e => {
- // console.error("api 요청 실패:" , e)
- })
- } else {
- setIsCs(false);
- singleLangCode(lang)
- // getLangCode(476) //476 : h만 있음
- .then(data => {
- // console.log("api 결과", data);
- const { lines , space, charCount } = processCode(data.content);
- setCodeId(data.codeId);
- setLines(lines);
- setSpace(space);
- setlinesCharCount(charCount)
- })
- .catch(e => {
- // console.error("api 요청 실패:" , e)
- })
-
- }
-
- }
- },[lang])
-
- useEffect(() => {
- if (!Array.isArray(CScode)) return;
-
- // console.log(CScode);
- // const allLines = CScode.map((item) => `${item.keyword} - ${item.content}`);
- // setLines(allLines); // 하나의 배열로 상태 저장
- const allLines = CScode.map((item) => `${item.keyword} - ${item.content}`);
- setLines(allLines); // 하나의 배열로 상태 저장
- }, [CScode])
-
- const getLanguageClass = (lang) => {
- if (!lang) {
- return '';
- }
-
- const lowerLang = lang.toLowerCase();
-
- if (lowerLang === "java") return 'language-java';
- else if (lowerLang === "python") return 'language-python';
- else if (lowerLang === "js") return 'language-javascript';
- else if (lowerLang === "sql") return 'language-sql';
- else return '';
-
- }
-
- const handleKeyDown = (e) => {
-
- if (!isStarted) {
- setStartTime(Date.now())
- setIsStarted(true);
- }
-
- const key = e.key;
-
- if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') { // ctrl+V or commend + V 막기기
- e.preventDefault();
- }
-
- if (key === 'Enter') {
- e.preventDefault(); // 기본적으로 엔터줄바꾸는거 막기
-
- const currentLine = lines[currentLineIndex];
- const normalizedInput = currentInput.split('');
-
- if (compareInputWithLineEnter(normalizedInput, currentLine)) { //다 맞게 쳤으면
- setCurrentLineIndex((prev) => prev + 1); // 다음줄로 넘김
- setCurrentInput(''); // 입력창 리셋
- setCurrentCharIndex(0); // 현재 입력 위치 리셋셋
-
- } else { // 틀렸으면
- // console.log('현재 줄을 정확히 입력하지 않음')
- setShake(true);
- setTimeout(() => setShake(false), 500);
- }
- }
- else if (key === 'Tab') {
- e.preventDefault(); //
- // setCurrentInput((prev) => prev + '\t'); 일단 탭을 막아놓기
- }
-
- else if (key === 'Backspace') {
- if (currentCharIndex > 0) {
- setCurrentCharIndex((prev) => prev - 1); // 지운 글자만큼 currentCharIndex 감소
- }
- }
- };
-
- useEffect(() => {
- let timer;
-
- if (isStarted && !isFinished) {
- timer = setInterval(() => {
- setElapsedTime(Date.now() - startTime);
- }, 10);
- }
-
- return () => {
- if (timer) clearInterval(timer);
- };
-
- }, [isStarted, startTime, isFinished])
-
- useEffect(() => {
- setCpm(calculateCPM(totalTypedChars, elapsedTime / 1000 ))
- }, [elapsedTime])
-
- useEffect(() => {
- setProgress(getProgress(currentLineIndex, lines.length))
-
- if( lines.length > 0 && currentLineIndex === lines.length) {
- setIsFinished(true);
- }
-
- if (codeContainerRef.current && currentLineIndex > 0) {
- // 코드의 각 줄을 가져옵니다.
- const lineElements = codeContainerRef.current.querySelectorAll('.codeLine');
-
- // 각 줄의 고정된 높이를 가져옵니다. 이 높이는 이미 max-h로 지정되어 있기 때문에 일정합니다.
- const lineHeight = lineElements[currentLineIndex]?.getBoundingClientRect().height || 28; // 한 줄의 높이 계산
-
- // 스크롤을 자동으로 내리기
- codeContainerRef.current.scrollTop += lineHeight;
- codeContainerRef.current.scrollLeft = 0; // 전줄에서 오른쪽 스클롤 한게 있으면 돌려야함
- }
- }, [currentLineIndex])
-
- useEffect(() =>{
-
- const container = codeContainerRef.current;
- const cursorEl = document.querySelector('.cursor');
- if ( container && cursorEl) {
-
- const containerRect = container.getBoundingClientRect();
- const cursorRect = cursorEl.getBoundingClientRect();
-
- const padding = 50; // 커서가 오른쪽으로 50px 남았을 때 스크롤 하기
-
- // 커서가 너무 오른쪽에 가까워졌는지 확인
-
- if (cursorRect.right > containerRect.right - padding) {
- // 오른쪽으로 약간 스크롤
- container.scrollLeft += 400;
- }
-
- // 커서가 왼쪽 밖으로 밀린 경우 (역방향 처리도 가능)
- if (cursorRect.left < containerRect.left + 20) {
- container.scrollLeft -= 400;
- }
- }
-
- }, [currentInput])
-
- const handleInputChange = (e) => {
- const value = e.target.value;
- setCurrentInput(value);
-
- };
-
- useEffect(()=> {
- updateTotalTypedChars();
- }, [currentInput, currentLineIndex])
-
- const updateTotalTypedChars = () => {
- let previousLinesChars = 0;
- for (let i = 0; i < currentLineIndex; i++) {
- previousLinesChars += linesCharCount[i] || 0;
- }
-
- // 현재 줄에서 올바르게 입력한 글자 수
- const currentLine = lines[currentLineIndex] || [];
- const currentLineChars = calculateCurrentLineTypedChars(currentInput, currentLine);
- // 전체 올바르게 입력한 글자 수 업데이트
- setTotalTypedChars(previousLinesChars + currentLineChars);
-
- // 현재 줄에 틀린 글자가 있는지 확인
- const hasWrongChar = compareInputWithLine(currentInput, currentLine);
- setWrongChar(hasWrongChar);
- }
-
- // useEffect(()=> {
- // console.log(totalTypedChars);
- // }, [totalTypedChars])
-
- // useEffect(()=>{
- // console.log(lines);
- // }, [lines])
-
- return (
-
- {/*
*/}
- {/* 타자게임 박스 */}
-
-
-
-
-
-
- {/* 콘텐츠 박스들 */}
-
- {/* 왼쪽 컨텐츠 영역 */}
-
-
setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
- tabIndex={0}
- onKeyDown={handleKeyDown}
- style={{
- backgroundColor: '#1C1C1C',
- borderColor: '#51E2F5',
- }}
- >
-
- {/* */}
-
-
- {lines.map((line, idx) => {
-
- // 현재 줄을 이차원 배열에서 문자를 하나씩 가져오기
- const normalizedInput = currentInput.split('');
- const currentLine = line;
-
- const lineWithSpace = space[idx];
-
- return (
-
- {idx < currentLineIndex ? (
- // 이미 완료한 줄
-
- {new Array(lineWithSpace).fill('\u00A0').map((_, spaceIndex) => (
- // 탭 크기만큼 공백 추가
- ))}
- {line.map((char, i) => (
- {char}
- ))}
-
- ) : idx === currentLineIndex ? (
-
- // 현재 타이핑 중인 줄
-
-
- {new Array(lineWithSpace).fill('\u00A0').map((_, spaceIndex) => (
- // 탭 크기만큼 공백 추가
- ))}
- {currentLine.map((char, i) => {
- const inputChar = normalizedInput[i]; // 입력된 문자
-
- let className = '';
-
- // 현재 문자가 일치하는지 확인
- if (inputChar == null) {
- className = 'pending currentLine'; // 아직 입력 안 된 문자
- } else if (inputChar=== char) {
- className = 'typed currentLine'; // 일치한 문자
- } else {
- if (char === ' '){
- className = 'wrong currentLine bg-red-400 '; // 공백이고 틀린 문자
- } else {
- className = 'wrong currentLine'; // 틀린 문자
- }
- }
-
- return (
-
- {i === normalizedInput.length && }
-
- {char === ' ' ? '\u00A0' : char}
-
-
- );
- })}
-
-
- ) : (
- // 아직 안친 줄
-
- {new Array(lineWithSpace).fill('\u00A0').map((_, spaceIndex) => (
- // 탭 크기만큼 공백 추가
- ))}
- {line.map((char, i) => (
- {char}
- ))}
-
- )}
-
- );
- })}
-
-
-
-
- {/* 유저가 타이핑한 코드가 보이는 곳 */}
-
e.preventDefault()} //마우스 붙여 넣기도 막기기
- />
-
-
-
-
-
-
-
-
-
- {/* 오른쪽 콘텐츠 박스 */}
-
-
-
-
- {isFinished && (
-
-
-
- )}
-
- )
-};
-
-export default SinglePage
diff --git a/src/pages/single/SingleTabPage.tsx b/src/pages/single/SingleTabPage.tsx
deleted file mode 100644
index 67ec4bc..0000000
--- a/src/pages/single/SingleTabPage.tsx
+++ /dev/null
@@ -1,439 +0,0 @@
-// @ts-nocheck
-// import backgroundImg from '../../assets/images/single_background.jpg'
-// import box from '../../assets/images/board1.jpg'
-// import logo from '../../assets/images/logo.png'
-// import pythonImg from '../../assets/images/python.png'
-// import javaImg from '../../assets/images/Java.png'
-// import cImg from '../../assets/images/C.png'
-// import csImg from '../../assets/images/CS.png'
-// import sqlImg from '../../assets/images/SQL.png'
-// import Keyboard from '../../components/keyboard/Keyboard'
-// import Header from "../../components/common/Header"
-
-// import authApi from "../../api/authAxiosConfig"
-// import { getAccessToken } from "../../utils/tokenUtils";
-// import { useNavigate, useParams } from 'react-router-dom'
-// import { useEffect, useState, useRef } from 'react'
-
-// import hljs from 'highlight.js/lib/core';
-// import javascript from 'highlight.js/lib/languages/javascript';
-// import java from 'highlight.js/lib/languages/java';
-// import python from 'highlight.js/lib/languages/python';
-// import sql from 'highlight.js/lib/languages/sql';
-// import 'highlight.js/styles/atom-one-dark.css';
-// import '../../styles/single/SinglePage.css';
-
-// // 등록
-// hljs.registerLanguage('java', java);
-// hljs.registerLanguage('python', python);
-// hljs.registerLanguage('javascript', javascript);
-// hljs.registerLanguage('sql', sql);
-
-// import { calculateWPM, calculateCPM, getSpeedProgress, getProgress } from '../../utils/typingUtils';
-// import { formatTime } from '../../utils/formatTimeUtils'
-// import FinishPage from '../single/modal/FinishPage';
-
-// const SinglePage = () => {
-
-// const navigate = useNavigate();
-// const { lang } = useParams();
-
-// // 코드 입력 관련 상태관리
-// const [rawCode, setRawCode] = useState(""); // API 받은 순수 코드
-// // const [highlightedCode, setHighlightedCode] = useState(""); // 하이라이트된 HTML 코드 안써도 될듯 이거
-// const [lines, setLines] = useState([]);
-// const [currentLineIndex, setCurrentLineIndex] = useState(0);
-// const [currentInput, setCurrentInput] = useState(""); //사용자가 입력한 문자열
-// const [wrongChar, setWrongChar] = useState(false); // 현재까지 입력한 input중에 틀림 존재 여부 상태 관리
-// const [shake, setShake] = useState(false); // 오타 입력창 흔들기 모션션
-
-// // 포커스 관련 상태관리
-// const inputAreaRef = useRef(null);
-// const [isFocused, setIsFocused] = useState(false);
-
-// // 언어별 캐릭터 이미지
-// const [langImg, setLangImg] = useState(null)
-
-// // 시간 및 달성률 상태관리
-// const [startTime, setStartTime] = useState(null);
-// const [elapsedTime, setElapsedTime] = useState(0);
-// const [isStarted, setIsStarted] = useState(false);
-
-// const [progress, setProgress] = useState(0);
-
-// // 전체 타이핑한 글자수 상태관리
-// const [totalTypedChars, setTotalTypedChars] = useState(0);
-// const [cpm, setCpm] = useState(0);
-
-// // 완료 상태 관리
-// const [isFinished, setIsFinished] = useState(false);
-
-// useEffect(() => {
-// if (inputAreaRef.current) {
-// inputAreaRef.current.focus();
-// }
-// },[])
-
-// useEffect(() => {
-// const accessToken = getAccessToken();
-// // console.log(accessToken)
-// if (!accessToken) {
-// alert("로그인이 필요합니다");
-// navigate("/auth/login");
-// }
-// }, [navigate]);
-
-// useEffect(() => {
-// if (lang) {
-// getLangImg(lang);
-// authApi.get('/api/single/code', {params: {language: lang.toUpperCase()}})
-// .then(res => {
-// console.log("api 결과", res.data)
-// const code = res.data.content.content
-// setRawCode(code)
-
-// // let langForHLJS = getLangForHLJS(lang);
-
-// // if (langForHLJS) {
-// // const highlighted = hljs.highlight(code, { language: langForHLJS }).value;
-// // setHighlightedCode(highlighted);
-// // } else {
-// // setHighlightedCode(code); // CS처럼 하이라이트 필요 없는 경우
-// // }
-
-// })
-// .catch(e => {
-// console.error("api 요청 실패:" , e)
-// })
-// }
-// },[lang])
-
-// // const getLangForHLJS = (lang) => {
-// // if (!lang) {
-// // return '';
-// // }
-
-// // const lowerLang = lang.toLowerCase();
-
-// // if (lowerLang === "java") return 'java';
-// // else if (lowerLang === "python") return 'python';
-// // else if (lowerLang === "js") return 'javascript';
-// // else if (lowerLang === "sql") return 'sql';
-// // else return '';
-
-// // }
-
-// useEffect(() => { //줄 단위로 나누기
-// console.log(rawCode);
-// // setLines(rawCode.split('\n'));
-// setLines(rawCode.split('\n').filter(line => line.trim() !== '')); // 양쪽에 빈 공백이 있음
-
-// }, [rawCode]);
-
-// const normalizeLine = (line) => {
-// const tabSize = 4;
-// return line.replace(/\t/g, ' '.repeat(tabSize))
-// }
-
-// const getLanguageClass = (lang) => {
-// if (!lang) {
-// return '';
-// }
-
-// const lowerLang = lang.toLowerCase();
-
-// if (lowerLang === "java") return 'language-java';
-// else if (lowerLang === "python") return 'language-python';
-// else if (lowerLang === "js") return 'language-javascript';
-// else if (lowerLang === "sql") return 'language-sql';
-// else return '';
-
-// }
-
-// const handleKeyDown = (e) => {
-
-// if (!isStarted) {
-// setStartTime(Date.now())
-// setIsStarted(true);
-// }
-
-// const key = e.key;
-
-// if (key === 'Enter') {
-// e.preventDefault(); // 기본적으로 엔터줄바꾸는거 막기
-
-// const currentLine = lines[currentLineIndex];
-// const normalizedInput = normalizeLine(currentInput);
-// const normalizedLine = normalizeLine(currentLine);
-
-// if (normalizedInput === normalizedLine) { //다 맞게 쳤으면
-// setCurrentLineIndex((prev) => prev + 1);
-// setCurrentInput('');
-
-// } else { // 틀렸으면
-// console.log('현재 줄을 정확히 입력하지 않음')
-// setShake(true);
-// setTimeout(() => setShake(false), 500);
-// }
-// }
-// else if (key === 'Tab') {
-// e.preventDefault(); //
-// setCurrentInput((prev) => prev + '\t');
-// }
-
-// else if (key.length === 1){ //글자 입력하면
-// // setCurrentInput((prev) => prev +key)
-// setCurrentInput((prev) => {
-// const newInput = prev + key;
-
-// const normalizedLine = normalizeLine(lines[currentLineIndex]);
-// const nextChar = normalizedLine[newInput.length - 1];
-
-// // 현재까지 입력한 값과 정답을 비교하여 틀린 글자가 있는지 확인
-// const hasWrongChar = normalizedLine.slice(0, newInput.length) !== newInput;
-// setWrongChar(hasWrongChar); // 틀린 글자가 있으면 빨간 테두리 유지
-
-// if (key === nextChar) {
-// setTotalTypedChars(prev => prev + 1);
-// }
-
-// return newInput;
-// })
-
-// }
-// else if (key === 'Backspace') //백스페이스 누르면 지우기
-// setCurrentInput((prev) => prev.slice(0,-1));
-// };
-
-// const getLangImg = (lang) => {
-// if (lang === "java") setLangImg(javaImg);
-// else if (lang === "python") setLangImg(pythonImg);
-// else if (lang === "c") setLangImg(cImg);
-// else if (lang === "sql") setLangImg(sqlImg);
-// else setLangImg(csImg);
-// }
-
-// useEffect(() => {
-// let timer;
-
-// if (isStarted && !isFinished) {
-// timer = setInterval(() => {
-// setElapsedTime(Date.now() - startTime);
-// }, 10);
-// }
-
-// return () => {
-// if (timer) clearInterval(timer);
-// };
-
-// }, [isStarted, startTime, isFinished])
-
-// useEffect(() => {
-// setCpm(calculateCPM(totalTypedChars, elapsedTime / 1000 ))
-// }, [elapsedTime])
-
-// useEffect(() => {
-// setProgress(getProgress(currentLineIndex, lines.length))
-
-// if( lines.length > 0 && currentLineIndex == lines.length) {
-// setIsFinished(true);
-// }
-// }, [currentLineIndex])
-
-
-// return (
-//
-//
-// {/* 타자게임 박스 */}
-//
-//
-
-//
-
-
-// {/* 콘텐츠 박스들 */}
-//
-// {/* 왼쪽 컨텐츠 영역 */}
-//
-//
setIsFocused(true)}
-// onBlur={() => setIsFocused(false)}
-// tabIndex={0}
-// onKeyDown={handleKeyDown}
-// style={{
-// backgroundColor: '#1C1C1C',
-// borderColor: '#51E2F5',
-// }}
-// >
-//
-// {/* */}
-
-//
-// {lines.map((line, idx) => {
-
-// const normalizedLine = normalizeLine(line);
-// const normalizedInput = normalizeLine(currentInput);
-// return (
-//
-// {idx < currentLineIndex ? (
-
-// // 이미 완료 한 줄
-// {line}
-// ) : idx === currentLineIndex ? (
-
-// // 현재 타이핑 중인 줄
-//
-// { normalizedLine.split('').map((char,i) => {
-// const inputChar = normalizedInput[i];
-// let className = '';
-
-// if (inputChar == null) {
-// className = 'pending currentLine';
-// } else if (inputChar === char) {
-// className = 'typed currentLine';
-// } else {
-// className = 'wrong currentLine';
-// }
-
-// return (
-//
-// {char === ' ' ? '\u00A0' : char}
-//
-// );
-// })}
-//
-
-// ) : (
-// // 아직 안친줄
-// {line}
-// )}
-//
-// );
-// })}
-//
-//
-
-
-// {/* 유저가 타이핑한 코드가 보이는 곳 */}
-//
-//
-// {currentInput.split('').map((char, idx) => (
-//
-// {char === '\t' ? '\u00A0\u00A0\u00A0\u00A0' : char}
-//
-// ))}
-// {/* 커서 */}
-// {isFocused && | }
-//
-//
-
-//
-
-//
-//
-
-//
-//
-
-// {/* 오른쪽 콘텐츠 박스 */}
-//
-// {/* 캐릭터 */}
-//
-
-//
-//
-
-// {/* 시간 */}
-//
시간
-//
-// {formatTime(elapsedTime)}
-//
-
-// {/* 타수 */}
-// {/*
타수
-//
-// {cpm}
-//
*/}
-
-//
-// 타수 : {cpm}
-//
-
-// {/* 기본 흐릿한 배경 */}
-//
-
-// {/* 진행률 바 */}
-//
-
-//
진행률: {progress}%
-
-//
-//
-//
-
-// {isFinished && (
-//
-//
-//
-// )}
-//
-// )
-// };
-
-// export default SinglePage
\ No newline at end of file
diff --git a/src/pages/single/modal/CsWordSelectPage.tsx b/src/pages/single/modal/CsWordSelectPage.tsx
deleted file mode 100644
index 27091ec..0000000
--- a/src/pages/single/modal/CsWordSelectPage.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import BoardContainer from "../../../components/single/BoardContainer";
-import cancelBtn from "../../../assets/images/cancel_btn.png";
-import createReportBtn from "../../../assets/images/create_report.png";
-import backgroundImg from "../../../assets/images/single_background.svg";
-import Header from "../../../components/common/Header";
-import TutoModal from "../../../components/common/TutoModal";
-import SettingModal from "../../../components/modal/SettingModal";
-import RankingModal from "../../../components/modal/RankingModal";
-import { useNavigate } from "react-router-dom";
-import { useState } from "react";
-import { Box } from "../../../../styled-system/jsx";
-import { css } from "../../../../styled-system/css";
-
-const CsWordSelectPage: React.FC = () => {
- const navigate = useNavigate();
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
-
- return (
-
- {showTutoModal && (
-
- setShowTutoModal(false)} />
-
- )}
- {showSettingModal && (
- setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
- {/* Header - absolute positioned */}
-
- setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- onShowRanking={() => setShowRankingModal(true)}
- />
-
-
-
- {/* 타이틀 텍스트 */}
-
- 단어 선택
-
-
- {/* 전체 컨텐츠 영역 */}
-
- {/* 왼쪽 체크리스트 */}
-
-
-
- {/* 취소 버튼 */}
-
-
navigate("/single/select/language")}
- />
-
-
-
-
- );
-};
-
-export default CsWordSelectPage;
diff --git a/src/pages/single/modal/FinishPage.tsx b/src/pages/single/modal/FinishPage.tsx
deleted file mode 100644
index b37ad00..0000000
--- a/src/pages/single/modal/FinishPage.tsx
+++ /dev/null
@@ -1,292 +0,0 @@
-import box from "../../../assets/images/board1.jpg";
-import cup from "../../../assets/images/cup.png";
-import restartBtn from "../../../assets/images/restart_btn.png";
-import stopBtn from "../../../assets/images/stop_btn.png";
-import { formatTime } from "../../../utils/formatTimeUtils";
-import { Box } from "../../../../styled-system/jsx";
-
-import { postRecord } from "../../../api/singleApi";
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import codeDescBtn from "../../../assets/images/codeDescriptionBtn.png";
-import codeDescBtn1 from "../../../assets/images/codeDescriptionBtn1.png";
-import codeDescBtn2 from "../../../assets/images/codeDescriptionBtn2.png";
-import codeDescBtn3 from "../../../assets/images/codeDescriptionBtn3.png";
-import codeDescBtn4 from "../../../assets/images/codeDescriptionBtn4.png";
-import mouseImg from "../../../assets/images/mouse.png";
-import CodeDescription from "../../../components/single/CodeDescription";
-
-interface FinishPageProps {
- codeId: number | null;
- lang: string;
- cpm: number;
- elapsedTime: number;
- strokes?: number;
- onShowCodeDescription: () => void;
-}
-
-interface Firework {
- id: number;
- left: string;
- top: string;
- size: number;
- color: string;
-}
-
-const FinishPage: React.FC = ({
- codeId,
- lang,
- cpm,
- elapsedTime,
- strokes,
- onShowCodeDescription,
-}) => {
- const navigate = useNavigate();
-
- const [userType, setUserType] = useState(null);
- const [fireworks, setFireworks] = useState([]);
-
- const [isApiLoading, setIsApiLoading] = useState(false);
-
- const codeBtns = [
- codeDescBtn,
- codeDescBtn1,
- codeDescBtn2,
- codeDescBtn3,
- codeDescBtn4,
- ];
- const [currentButtonIndex, setCurrentButtonIndex] = useState(0);
-
- useEffect(() => {
- const auth = JSON.parse(localStorage.getItem("auth-storage") || "{}");
- setUserType(auth?.state?.user?.userType);
-
- const interval = setInterval(() => {
- setFireworks((prev) => [
- ...prev,
- {
- id: Math.random(),
- left: `${Math.random() * 100}%`,
- top: `${Math.random() * 100}%`,
- size: Math.random() * 8 + 4,
- color: ["#ff0", "#f0f", "#0ff", "#0f0", "#f00"][
- Math.floor(Math.random() * 5)
- ],
- },
- ]);
- }, 80);
-
- const buttonInterval = setInterval(() => {
- setCurrentButtonIndex((prev) => (prev + 1) % 5);
- }, 800);
-
- // 30개 생성 후 멈추기
- setTimeout(() => clearInterval(interval), 40 * 80);
-
- return () => {
- clearInterval(interval);
- clearInterval(buttonInterval);
- };
- }, []);
-
- return (
-
- {/* 폭죽 레이어 */}
- {fireworks.map((fw) => (
-
- ))}
-
-
- {/* 타이틀 텍스트 */}
-
- 미션 성공
-
-
- {/* 코드 설명 보러 가기 버튼 */}
-
-
-
-
-
- {/* 모달 컨텐츠들 */}
-
- {/* 컨텐츠 타이틀 */}
-
-
-
{lang.toUpperCase()} 미션 성공
-
-
- {/* 컨텐츠 내용 */}
-
-
- 시간 : {formatTime(elapsedTime)}
-
-
-
- 타수 : {strokes ?? Math.floor(cpm)}
-
-
-
- {/* 버튼 컨테이너 */}
-
-
window.location.reload()}
- style={{
- width: "180px",
- height: "auto",
- borderRadius: "1.5rem",
- transition: "all 200ms",
- opacity: isApiLoading ? 0.5 : 1,
- cursor: isApiLoading ? "not-allowed" : "pointer",
- pointerEvents: isApiLoading ? "none" : "auto",
- }}
- />
-
navigate("/single/select/language")}
- alt="확인"
- style={{
- width: "180px",
- height: "auto",
- borderRadius: "1.5rem",
- transition: "all 200ms",
- opacity: isApiLoading ? 0.5 : 1,
- cursor: isApiLoading ? "not-allowed" : "pointer",
- pointerEvents: isApiLoading ? "none" : "auto",
- }}
- />
-
-
-
-
- );
-};
-
-export default FinishPage;
diff --git a/src/pages/store/PurchaseFailurePage.test.tsx b/src/pages/store/PurchaseFailurePage.test.tsx
deleted file mode 100644
index c9cd2a8..0000000
--- a/src/pages/store/PurchaseFailurePage.test.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { MemoryRouter, Route, Routes } from "react-router-dom";
-import { render, screen } from "@testing-library/react";
-import PurchaseFailurePage from "@/pages/store/PurchaseFailurePage";
-
-describe("PurchaseFailurePage", () => {
- it("renders error message and code from query params", () => {
- render(
-
-
- }
- />
-
-
- );
-
- // Page should show payment failure heading (appears multiple times - page + dialog)
- expect(screen.getAllByText("결제 실패").length).toBeGreaterThan(0);
-
- // Error message appears in both page and dialog
- expect(
- screen.getAllByText(/카드가 승인되지 않았습니다/).length
- ).toBeGreaterThan(0);
-
- // Error code appears in the page content (multiple times in page + dialog)
- const containers = screen.getAllByText(/CARD_DECLINED/);
- expect(containers.length).toBeGreaterThan(0);
- // Verify at least one container has the error code label
- const hasErrorLabel = containers.some((el) => {
- const container = el.closest("div");
- return container?.textContent?.includes("오류 코드");
- });
- expect(hasErrorLabel).toBe(true);
- });
-});
diff --git a/src/pages/store/PurchaseFailurePage.tsx b/src/pages/store/PurchaseFailurePage.tsx
deleted file mode 100644
index f195958..0000000
--- a/src/pages/store/PurchaseFailurePage.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { useMemo, useState } from "react";
-import { useSearchParams, useNavigate } from "react-router-dom";
-import { css } from "styled-system/css";
-import { PaymentErrorDialog } from "@/features/payment/components/PaymentErrorDialog";
-
-export default function PurchaseFailurePage() {
- const [params] = useSearchParams();
- const navigate = useNavigate();
- const [open, setOpen] = useState(true);
-
- const error = useMemo(() => {
- const code = params.get("code") || "UNKNOWN";
- const message = params.get("message") || "결제에 실패했습니다.";
- return { code, message };
- }, [params]);
-
- return (
-
-
-
- 결제 실패
-
-
- {error.message}
-
-
- 오류 코드: {error.code}
-
-
- navigate(-1)}
- >
- 뒤로가기
-
- navigate("/store")}
- >
- 스토어로 이동
-
-
-
-
- {open && (
-
setOpen(false)}
- onRetry={() => navigate(-1)}
- />
- )}
-
- );
-}
diff --git a/src/pages/store/StorePage.test.tsx b/src/pages/store/StorePage.test.tsx
deleted file mode 100644
index 5468873..0000000
--- a/src/pages/store/StorePage.test.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import React from "react";
-import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { BrowserRouter } from "react-router-dom";
-import { vi, describe, it, expect, beforeEach } from "vitest";
-import StorePage from "./StorePage";
-import { MockedProvider } from "@apollo/client/testing/react";
-import { GET_USER_PROFILE } from "@/features/user/graphql/queries";
-
-// Mock the payment hook
-vi.mock("@/features/payment/hooks/usePayment", () => ({
- usePayment: () => ({
- requestPayment: vi.fn(),
- error: null,
- isProcessing: false,
- clearError: vi.fn(),
- }),
-}));
-
-// Mock react-router-dom
-const mockNavigate = vi.fn();
-vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual("react-router-dom");
- return {
- ...actual,
- useNavigate: () => mockNavigate,
- };
-});
-
-// Intentionally skip StorePage legacy suite (broken block removed)
-// describe.skip("StorePage", () => {});
-
-const mocks = [
- {
- request: {
- query: GET_USER_PROFILE,
- },
- result: {
- data: {
- me: {
- id: "1",
- name: "Test User",
- email: "test@example.com",
- wallet: {
- balance: 3817,
- currency: "KRW",
- },
- },
- },
- },
- },
-];
-
-const renderStorePage = () => {
- return render(
-
-
-
-
-
- );
-};
-
-describe("StorePage", () => {
- beforeEach(() => {
- mockNavigate.mockClear();
- });
-
- it("renders store modal with correct title", () => {
- renderStorePage();
-
- expect(screen.getByText("Store")).toBeInTheDocument();
- });
-
- it("renders all purchase options", () => {
- renderStorePage();
-
- expect(screen.getByText("⭐ 1,000 + 10")).toBeInTheDocument();
- expect(screen.getByText("⭐ 3,000 + 89")).toBeInTheDocument();
- expect(screen.getByText("⭐ 5,000 + 567")).toBeInTheDocument();
- expect(screen.getByText("⭐ 10,000 + 1,234")).toBeInTheDocument();
- });
-
- it("renders currency icons", () => {
- renderStorePage();
-
- // Check for dollar sign
- const dollarSign = screen.getByText("$");
- expect(dollarSign).toBeInTheDocument();
-
- // Multiple star icons exist (large display and small badges), check all exist
- const starIcons = screen.getAllByText("★");
- expect(starIcons.length).toBeGreaterThan(0);
- });
-
- it("closes modal when close button is clicked", () => {
- renderStorePage();
-
- const closeButton = screen.getByText("×");
- fireEvent.click(closeButton);
-
- expect(mockNavigate).toHaveBeenCalledWith("/main");
- });
-
- it("has correct modal styling", () => {
- renderStorePage();
-
- const modal = screen.getByTestId("store-modal");
- expect(modal).toHaveStyle({
- backgroundColor: "rgba(75, 0, 130, 0.95)",
- border: "2px solid #00ffff",
- });
- });
-
- it("renders purchase buttons with correct styling", () => {
- renderStorePage();
-
- const purchaseButtons = screen
- .getAllByRole("button")
- .filter((button) => button.textContent?.includes("⭐"));
-
- expect(purchaseButtons).toHaveLength(4);
-
- purchaseButtons.forEach((button) => {
- expect(button).toHaveStyle({
- backgroundColor: "rgba(236, 72, 153, 0.8)",
- color: "#ffffff",
- });
- });
- });
-
- it("handles purchase button hover effects", () => {
- renderStorePage();
-
- const firstButton = screen.getByText("⭐ 1,000 + 10");
-
- fireEvent.mouseEnter(firstButton);
- expect(firstButton).toHaveStyle({
- transform: "scale(1.05)",
- filter: "brightness(1.1)",
- });
-
- fireEvent.mouseLeave(firstButton);
- expect(firstButton).toHaveStyle({
- transform: "scale(1)",
- filter: "brightness(1)",
- });
- });
-
- it("handles purchase button click", () => {
- renderStorePage();
-
- const firstButton = screen.getByText("⭐ 1,000 + 10");
-
- fireEvent.mouseDown(firstButton);
- expect(firstButton).toHaveStyle({
- transform: "scale(0.95)",
- });
-
- fireEvent.mouseUp(firstButton);
- expect(firstButton).toHaveStyle({
- transform: "scale(1.05)",
- });
- });
-});
diff --git a/src/pages/store/StorePage.tsx b/src/pages/store/StorePage.tsx
deleted file mode 100644
index 7892a88..0000000
--- a/src/pages/store/StorePage.tsx
+++ /dev/null
@@ -1,452 +0,0 @@
-import { useState, useEffect, type FC } from "react";
-import { useNavigate } from "react-router-dom";
-import { useQuery } from "@apollo/client/react";
-import { Box } from "../../../styled-system/jsx";
-import { usePayment } from "../../features/payment/hooks/usePayment";
-import { GET_USER_PROFILE } from "../../features/user/graphql/queries";
-import { GET_PRODUCTS } from "@/features/products/graphql/queries";
-import type { GetUserProfileData } from "../../features/user/types/follow-types";
-import multibg from "@/assets/images/multi_background.png";
-import Header from "../../components/common/Header";
-import TutoModal from "../../components/common/TutoModal";
-import SettingModal from "../../components/modal/SettingModal";
-import RankingModal from "../../components/modal/RankingModal";
-
-interface PurchaseOption {
- id: string;
- amount: number;
- bonus: number;
- price: number; // In KRW
- displayText: string;
-}
-
-const StorePage: FC = () => {
- const navigate = useNavigate();
- const { requestPayment, error, isProcessing, clearError } = usePayment();
-
- // Header modal states
- const [showTutoModal, setShowTutoModal] = useState(false);
- const [showSettingModal, setShowSettingModal] = useState(false);
- const [showRankingModal, setShowRankingModal] = useState(false);
-
- // Purchase options configuration
- const purchaseOptions: PurchaseOption[] = [
- {
- id: "1000-stars",
- amount: 1000,
- bonus: 10,
- price: 1000, // 1,000 KRW
- displayText: "⭐ 1,000 + 10",
- },
- {
- id: "3000-stars",
- amount: 3000,
- bonus: 89,
- price: 3000, // 3,000 KRW
- displayText: "⭐ 3,000 + 89",
- },
- {
- id: "5000-stars",
- amount: 5000,
- bonus: 567,
- price: 5000, // 5,000 KRW
- displayText: "⭐ 5,000 + 567",
- },
- {
- id: "10000-stars",
- amount: 10000,
- bonus: 1234,
- price: 10000, // 10,000 KRW
- displayText: "⭐ 10,000 + 1,234",
- },
- ];
-
- // Load products
- //const { data: productsData } = useQuery(GET_PRODUCTS, {
- // variables: { page: 1, limit: 10 },
- //});
- //const purchaseOptions: PurchaseOption[] = (
- // (productsData as any)?.products?.items || []
- //).map((p: any) => ({
- // id: p.id,
- // amount: p.amount,
- // bonus: p.bonus,
- // price: p.price,
- // displayText: `⭐ ${p.amount.toLocaleString()}${p.bonus ? ` + ${p.bonus.toLocaleString()}` : ""}`,
- //}));
-
- // Get user data (including wallet balance)
- const { data: userData, loading } =
- useQuery(GET_USER_PROFILE);
- const currentBalance = userData?.me?.wallet?.balance || 0;
-
- // Payment handlers
- const handlePurchase = async (option: PurchaseOption) => {
- try {
- clearError();
- const result = await requestPayment(option.id);
-
- // Payment success - navigate to success page
- navigate("/store/success", {
- state: {
- purchaseData: {
- amount: option.amount,
- bonus: option.bonus,
- totalStars: option.amount + option.bonus,
- transactionId: result?.transactionId || `txn_${Date.now()}`,
- timestamp: new Date(),
- },
- },
- });
- } catch (err: any) {
- console.error("Payment failed:", err);
-
- // Payment failure - navigate to failure page
- navigate("/store/failure", {
- state: {
- errorData: {
- errorCode: err?.code || "PAYMENT_FAILED",
- errorMessage: err?.message || "Payment processing failed",
- errorType: getErrorType(err?.code),
- transactionId: `txn_${Date.now()}`,
- retryable: isRetryableError(err?.code),
- timestamp: new Date(),
- userMessage: getUserFriendlyMessage(err?.code, err?.message),
- technicalDetails: err?.details
- ? JSON.stringify(err.details)
- : undefined,
- },
- },
- });
- }
- };
-
- // Helper functions for error handling
- const getErrorType = (
- errorCode: string
- ): "payment" | "network" | "system" | "validation" => {
- const networkErrors = ["TIMEOUT", "NETWORK_ERROR"];
- const validationErrors = ["INVALID_CARD", "EXPIRED_CARD"];
- const systemErrors = ["F500", "SERVER_ERROR"];
-
- if (networkErrors.includes(errorCode)) return "network";
- if (validationErrors.includes(errorCode)) return "validation";
- if (systemErrors.includes(errorCode)) return "system";
- return "payment";
- };
-
- const isRetryableError = (errorCode: string): boolean => {
- const nonRetryableErrors = [
- "INSUFFICIENT_FUNDS",
- "CARD_DECLINED",
- "INVALID_CARD",
- "EXPIRED_CARD",
- "USER_CANCEL",
- ];
- return !nonRetryableErrors.includes(errorCode);
- };
-
- const getUserFriendlyMessage = (
- errorCode: string,
- originalMessage: string
- ): string => {
- const messageMap: Record = {
- INSUFFICIENT_FUNDS: "잔액이 부족합니다. 다른 결제 수단을 사용해주세요.",
- CARD_DECLINED:
- "카드가 승인되지 않았습니다. 카드사에 문의하거나 다른 카드를 사용해주세요.",
- EXPIRED_CARD:
- "카드 유효기간이 만료되었습니다. 새로운 카드를 사용해주세요.",
- INVALID_CARD: "유효하지 않은 카드입니다. 카드 정보를 확인해주세요.",
- TIMEOUT: "결제 시간이 초과되었습니다. 다시 시도해주세요.",
- NETWORK_ERROR:
- "네트워크 연결에 문제가 있습니다. 인터넷 연결을 확인해주세요.",
- F500: "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.",
- F400: "결제 정보가 올바르지 않습니다. 다시 시도해주세요.",
- F401: "인증에 실패했습니다. 다시 시도해주세요.",
- PAYMENT_FAILED: "Oops! Something went wrong",
- };
-
- return (
- messageMap[errorCode] || originalMessage || "Oops! Something went wrong"
- );
- };
-
- // Navigation handlers
- const handleClose = () => {
- navigate("/main");
- };
-
- // Keyboard event handlers
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === "Escape") {
- handleClose();
- }
- };
-
- document.addEventListener("keydown", handleKeyDown);
- return () => {
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, [navigate]);
-
- // Purchase button component
- const PurchaseButton: FC<{
- option: PurchaseOption;
- onPurchase: (option: PurchaseOption) => void;
- disabled: boolean;
- }> = ({ option, onPurchase, disabled }) => (
- !disabled && onPurchase(option)}
- style={{
- padding: "0.75rem 1rem",
- backgroundColor: "rgba(236, 72, 153, 0.8)",
- borderRadius: "0.5rem",
- color: "#ffffff",
- fontSize: "1.125rem",
- fontWeight: "600",
- textAlign: "center",
- cursor: disabled ? "not-allowed" : "pointer",
- opacity: disabled ? 0.6 : 1,
- transition: "all 0.15s ease-in-out",
- border: "none",
- outline: "none",
- }}
- onMouseEnter={(e: React.MouseEvent) => {
- if (!disabled) {
- e.currentTarget.style.transform = "scale(1.05)";
- e.currentTarget.style.filter = "brightness(1.1)";
- }
- }}
- onMouseLeave={(e: React.MouseEvent) => {
- if (!disabled) {
- e.currentTarget.style.transform = "scale(1)";
- e.currentTarget.style.filter = "brightness(1)";
- }
- }}
- onMouseDown={(e: React.MouseEvent) => {
- if (!disabled) {
- e.currentTarget.style.transform = "scale(0.95)";
- }
- }}
- onMouseUp={(e: React.MouseEvent) => {
- if (!disabled) {
- e.currentTarget.style.transform = "scale(1.05)";
- }
- }}
- >
- {option.displayText}
-
- );
-
- return (
-
- {/* Header - absolute positioned */}
-
- setShowTutoModal(true)}
- onShowSetting={() => setShowSettingModal(true)}
- onShowRanking={() => setShowRankingModal(true)}
- />
-
- {/* Store Modal */}
-
- {/* Modal Header */}
-
-
- Store
-
-
- navigate("/wallet/history")}
- style={{
- padding: "0.4rem 0.75rem",
- backgroundColor: "#00ffff",
- color: "#000000",
- borderRadius: "0.25rem",
- fontWeight: 700,
- cursor: "pointer",
- border: "none",
- }}
- onMouseEnter={(e) =>
- (e.currentTarget.style.filter = "brightness(0.95)")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.filter = "brightness(1)")
- }
- >
- History
-
- ) => {
- e.currentTarget.style.backgroundColor =
- "rgba(255, 255, 255, 0.1)";
- }}
- onMouseLeave={(e: React.MouseEvent) => {
- e.currentTarget.style.backgroundColor = "transparent";
- }}
- >
- ×
-
-
-
-
- {/* Currency Icon */}
-
-
- $
-
-
- ★
-
-
-
- {/* Purchase Options */}
-
- {purchaseOptions.map((option) => (
-
- ))}
-
-
- {/* Error Display */}
- {error && (
-
- {error.message}
-
- )}
-
- {/* Loading State */}
- {isProcessing && (
-
- 결제 처리 중...
-
- )}
-
-
- {/* Header Modals */}
- {showTutoModal && setShowTutoModal(false)} />}
- {showSettingModal && (
- setShowSettingModal(false)} />
- )}
- {showRankingModal && (
- setShowRankingModal(false)} />
- )}
-
- );
-};
-
-export default StorePage;
diff --git a/src/pages/store/StorePurchaseFailurePage.tsx b/src/pages/store/StorePurchaseFailurePage.tsx
deleted file mode 100644
index 84c673c..0000000
--- a/src/pages/store/StorePurchaseFailurePage.tsx
+++ /dev/null
@@ -1,707 +0,0 @@
-import React, { useState, useEffect, useRef } from "react";
-import { createPortal } from "react-dom";
-import { useNavigate, useLocation } from "react-router-dom";
-import { Box } from "../../../styled-system/jsx";
-import multibg from "@/assets/images/multi_background.png";
-import board1 from "@/assets/images/board1.jpg";
-import Header from "../../components/common/Header";
-
-interface ErrorData {
- errorCode: string;
- errorMessage: string;
- errorType: "payment" | "network" | "system" | "validation";
- transactionId?: string;
- retryable: boolean;
- timestamp: Date;
- userMessage: string;
- technicalDetails?: string;
-}
-
-interface StorePurchaseFailurePageProps {
- errorData?: ErrorData;
-}
-
-const StorePurchaseFailurePage: React.FC = ({
- errorData,
-}) => {
- const navigate = useNavigate();
- const location = useLocation();
- const [isRetrying, setIsRetrying] = useState(false);
- const [retryCount, setRetryCount] = useState(0);
- const retryCounterRef = useRef(null);
- const [animationComplete, setAnimationComplete] = useState(false);
-
- // Get error data from location state, sessionStorage, or props
- const getErrorData = () => {
- if (errorData) return errorData;
- if (location.state?.errorData) return location.state.errorData;
-
- // Check sessionStorage for test data
- if (typeof window !== "undefined") {
- const stored = sessionStorage.getItem("failureErrorData");
- if (stored) {
- try {
- const parsed = JSON.parse(stored);
- sessionStorage.removeItem("failureErrorData"); // Clean up after reading
- return parsed;
- } catch (e) {
- console.error("Failed to parse error data from sessionStorage", e);
- }
- }
- }
-
- // Default error data
- return {
- errorCode: "PAYMENT_FAILED",
- errorMessage: "Payment processing failed",
- errorType: "payment" as const,
- retryable: true,
- userMessage: "Oops! Something went wrong",
- technicalDetails:
- "You may retry the payment or contact support if the problem persists.",
- };
- };
-
- const currentErrorData = getErrorData();
-
- const {
- errorCode,
- errorMessage,
- errorType,
- transactionId,
- retryable,
- userMessage,
- technicalDetails,
- } = currentErrorData;
-
- // Navigation handlers
- const handleTryAgain = async () => {
- if (!retryable || retryCount >= 3) return;
-
- setIsRetrying(true);
- setRetryCount((prev) => prev + 1);
-
- try {
- // Simulate retry logic
- await new Promise((resolve) => setTimeout(resolve, 2000));
-
- // Navigate back to store to retry
- navigate("/store", {
- state: {
- retry: true,
- retryCount: retryCount + 1,
- },
- });
- } catch (error) {
- console.error("Retry failed:", error);
- setIsRetrying(false);
- }
- };
-
- const handleBackToStore = () => {
- navigate("/store");
- };
-
- const handleBackToMain = () => {
- navigate("/main");
- };
-
- // Animation completion handler
- useEffect(() => {
- const timer = setTimeout(() => {
- setAnimationComplete(true);
- }, 300);
- return () => clearTimeout(timer);
- }, []);
-
- // Keyboard event handlers
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === "Escape") {
- navigate("/main");
- }
- };
-
- document.addEventListener("keydown", handleKeyDown);
- return () => {
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, [navigate]);
-
- // Observe external changes to data-retry-count (for e2e test control)
- useEffect(() => {
- const el = retryCounterRef.current;
- const applyFromAttr = () => {
- if (el) {
- const attr = el.getAttribute("data-retry-count");
- if (attr) {
- const parsed = parseInt(attr, 10);
- if (!Number.isNaN(parsed)) {
- setRetryCount(parsed);
- }
- }
- }
- const anyThree = document.querySelector('[data-retry-count="3"]');
- if (anyThree) {
- setRetryCount(3);
- }
- };
-
- applyFromAttr();
-
- const observers: MutationObserver[] = [];
- if (el) {
- const obs = new MutationObserver(() => applyFromAttr());
- obs.observe(el, {
- attributes: true,
- attributeFilter: ["data-retry-count"],
- });
- observers.push(obs);
- }
-
- const bodyObs = new MutationObserver(() => applyFromAttr());
- bodyObs.observe(document.body, {
- attributes: true,
- subtree: true,
- attributeFilter: ["data-retry-count"],
- });
- observers.push(bodyObs);
-
- return () => observers.forEach((o) => o.disconnect());
- }, []);
-
- // Error Icon Component
- const ErrorIcon: React.FC<{ errorType: string }> = ({ errorType }) => {
- const getIconColor = () => {
- switch (errorType) {
- case "payment":
- return "#ef4444";
- case "network":
- return "#f59e0b";
- case "system":
- return "#ef4444";
- case "validation":
- return "#f59e0b";
- default:
- return "#ef4444";
- }
- };
-
- const getIconSVG = () => {
- switch (errorType) {
- case "payment":
- return (
-
-
-
-
-
-
- );
- case "network":
- return (
-
-
-
-
-
- );
- default:
- return (
-
-
-
-
-
- );
- }
- };
-
- return (
-
-
- {getIconSVG()}
-
-
- );
- };
-
- // Action Button Component
- const ActionButton: React.FC<{
- onClick: () => void;
- children: React.ReactNode;
- variant?: "primary" | "secondary";
- disabled?: boolean;
- loading?: boolean;
- }> = ({
- onClick,
- children,
- variant = "primary",
- disabled = false,
- loading = false,
- }) => {
- const getButtonStyle = () => {
- if (variant === "primary") {
- return {
- backgroundColor: disabled ? "rgba(255, 87, 34, 0.5)" : "#ff5722",
- color: "#ffffff",
- border: "none",
- };
- } else {
- return {
- backgroundColor: disabled
- ? "rgba(139, 92, 246, 0.1)"
- : "rgba(139, 92, 246, 0.2)",
- color: "#ffffff",
- border: "1px solid rgba(139, 92, 246, 0.5)",
- };
- }
- };
-
- return (
- !disabled && !loading && onClick()}
- style={{
- width: "100%",
- padding: "0.5rem 1rem",
- borderRadius: "0.5rem",
- fontSize: "1.125rem",
- fontWeight: "600",
- textAlign: "center",
- cursor: disabled || loading ? "not-allowed" : "pointer",
- transition: "all 0.15s ease-in-out",
- opacity: disabled ? 0.6 : 1,
- textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)",
- ...getButtonStyle(),
- }}
- onMouseEnter={(e) => {
- if (!disabled && !loading) {
- e.currentTarget.style.transform = "scale(1.02)";
- if (variant === "primary") {
- e.currentTarget.style.backgroundColor = "#f4511e";
- } else {
- e.currentTarget.style.backgroundColor = "rgba(139, 92, 246, 0.3)";
- }
- }
- }}
- onMouseLeave={(e) => {
- if (!disabled && !loading) {
- e.currentTarget.style.transform = "scale(1)";
- e.currentTarget.style.backgroundColor =
- getButtonStyle().backgroundColor;
- }
- }}
- >
- {loading ? (
-
-
- Processing...
-
- ) : (
- children
- )}
-
- );
- };
-
- const content = (
-
- {/* Header */}
-
-
-
-
- {/* Failure Modal overlay container (tested by e2e) */}
-
- {/* First child: error icon container to satisfy e2e */}
-
-
- {/* Error Message */}
-
- Purchase Failed!
-
-
- {/* User Message */}
-
- {userMessage}
-
-
- {/* Error Details Box */}
- {technicalDetails && (
-
-
- Error Details:
-
-
- {technicalDetails}
-
-
- )}
-
- {/* Hidden retry counter hook into DOM for tests */}
-
-
- {/* Action Buttons */}
-
- {retryable && retryCount < 3 && (
-
- Try Again
-
- )}
-
- {retryCount >= 3 && (
-
- Maximum retry attempts reached. Please contact support.
-
- )}
-
-
- Back to Store
-
-
- Back to Main
-
-
-
-
- {/* Decorative Board background behind overlay */}
-
- {/* Modal Header */}
-
-
- Store
-
- {
- e.currentTarget.style.backgroundColor =
- "rgba(255, 255, 255, 0.2)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "transparent";
- }}
- >
- ×
-
-
-
- {/* Empty body; content moved to overlay */}
-
-
-
- {/* CSS Animations */}
-
-
- );
-
- return typeof document !== "undefined"
- ? createPortal(content, document.body)
- : content;
-};
-
-export default StorePurchaseFailurePage;
diff --git a/src/pages/store/StorePurchaseSuccessPage.test.tsx b/src/pages/store/StorePurchaseSuccessPage.test.tsx
deleted file mode 100644
index 77e7400..0000000
--- a/src/pages/store/StorePurchaseSuccessPage.test.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { MemoryRouter, Route, Routes } from "react-router-dom";
-import { render, screen, fireEvent } from "@testing-library/react";
-import StorePurchaseSuccessPage from "./StorePurchaseSuccessPage";
-
-describe("StorePurchaseSuccessPage", () => {
- const purchaseData = {
- amount: 1000,
- bonus: 10,
- totalStars: 1010,
- transactionId: "txn_test_123",
- timestamp: new Date(),
- };
-
- function renderWithRouter(state?: any) {
- return render(
-
-
- } />
-
-
- );
- }
-
- it("renders purchase success content and reward when location.state.purchaseData provided", () => {
- renderWithRouter({ purchaseData });
- expect(screen.getByText("Purchase Successful!")).toBeInTheDocument();
- expect(
- screen.getByText("Your stars have been added to your account")
- ).toBeInTheDocument();
- expect(screen.getByText("+1,000")).toBeInTheDocument();
- expect(screen.getByText("(+10 bonus)")).toBeInTheDocument();
- expect(
- screen.getByRole("button", { name: /Back to Store/i })
- ).toBeInTheDocument();
- expect(
- screen.getByRole("button", { name: /Back to Main/i })
- ).toBeInTheDocument();
- });
-
- it("falls back to default data if location.state is empty", () => {
- renderWithRouter();
- expect(screen.getByText("Purchase Successful!")).toBeInTheDocument();
- expect(screen.getByText("+1,000")).toBeInTheDocument();
- });
-
- it("navigates to /store when Back to Store is clicked", () => {
- renderWithRouter({ purchaseData });
- const btn = screen.getByRole("button", { name: /Back to Store/i });
- fireEvent.click(btn);
- // DOM should update to store context (simulate navigation)
- // You may add assertions as needed for router context
- });
-
- it("navigates to /main when Back to Main is clicked", () => {
- renderWithRouter({ purchaseData });
- const btn = screen.getByRole("button", { name: /Back to Main/i });
- fireEvent.click(btn);
- // DOM should update to main context (simulate navigation)
- // You may add assertions as needed for router context
- });
-});
diff --git a/src/pages/store/StorePurchaseSuccessPage.tsx b/src/pages/store/StorePurchaseSuccessPage.tsx
deleted file mode 100644
index 599f8f9..0000000
--- a/src/pages/store/StorePurchaseSuccessPage.tsx
+++ /dev/null
@@ -1,415 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { createPortal } from "react-dom";
-import { useNavigate, useLocation } from "react-router-dom";
-import { Box } from "../../../styled-system/jsx";
-import multibg from "@/assets/images/multi_background.png";
-import board1 from "@/assets/images/board1.jpg";
-import Header from "../../components/common/Header";
-
-interface PurchaseData {
- amount: number;
- bonus: number;
- totalStars: number;
- transactionId: string;
- timestamp: Date;
-}
-
-interface StorePurchaseSuccessPageProps {
- purchaseData?: PurchaseData;
-}
-
-const StorePurchaseSuccessPage: React.FC = ({
- purchaseData,
-}) => {
- const navigate = useNavigate();
- const location = useLocation();
- const [animationComplete, setAnimationComplete] = useState(false);
-
- // Get purchase data from location state, sessionStorage, or props
- const getPurchaseData = () => {
- if (purchaseData) return purchaseData;
- if (location.state?.purchaseData) return location.state.purchaseData;
-
- // Check sessionStorage for test data
- if (typeof window !== "undefined") {
- const stored = sessionStorage.getItem("successPurchaseData");
- if (stored) {
- try {
- const parsed = JSON.parse(stored);
- sessionStorage.removeItem("successPurchaseData"); // Clean up after reading
- return parsed;
- } catch (e) {
- console.error("Failed to parse purchase data from sessionStorage", e);
- }
- }
- }
-
- // Default purchase data
- return {
- amount: 1000,
- bonus: 10,
- totalStars: 1010,
- transactionId: "txn_" + Date.now(),
- timestamp: new Date(),
- };
- };
-
- const currentPurchaseData = getPurchaseData();
-
- const { amount, bonus, totalStars, transactionId } = currentPurchaseData;
-
- // Navigation handlers
- const handleBackToStore = () => {
- navigate("/store");
- };
-
- const handleBackToMain = () => {
- navigate("/main");
- };
-
- // Animation completion handler
- useEffect(() => {
- const timer = setTimeout(() => {
- setAnimationComplete(true);
- }, 300);
- return () => clearTimeout(timer);
- }, []);
-
- // Keyboard event handlers
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === "Escape") {
- navigate("/main");
- }
- };
-
- document.addEventListener("keydown", handleKeyDown);
- return () => {
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, [navigate]);
-
- // Success Icon Component
- const SuccessIcon: React.FC = () => (
-
-
-
-
-
-
-
- );
-
- // Currency Display Component
- const CurrencyDisplay: React.FC<{ amount: number; bonus: number }> = ({
- amount,
- bonus,
- }) => (
-
-
- ⭐
-
-
- +{amount.toLocaleString()}
- {bonus > 0 && (
-
- (+{bonus.toLocaleString()} bonus)
-
- )}
-
-
- );
-
- // Action Button Component
- const ActionButton: React.FC<{
- onClick: () => void;
- children: React.ReactNode;
- variant?: "primary" | "secondary";
- }> = ({ onClick, children, variant = "primary" }) => (
- {
- e.currentTarget.style.transform = "scale(1.05)";
- e.currentTarget.style.backgroundColor =
- variant === "primary" ? "#059669" : "rgba(255, 255, 255, 0.2)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.transform = "scale(1)";
- e.currentTarget.style.backgroundColor =
- variant === "primary" ? "#10b981" : "rgba(255, 255, 255, 0.1)";
- }}
- >
- {children}
-
- );
-
- const content = (
-
- {/* Header */}
-
-
-
-
- {/* Success Modal with overlay container (tested by e2e) */}
-
- {/* make success icon container the first child */}
-
-
-
- Purchase Successful!
-
-
- Your stars have been added to your account
-
-
-
- Back to Store
-
- Back to Main
-
-
-
-
- {/* Decorative Board background behind overlay */}
-
- {/* Modal Header */}
-
-
- Store
-
- {
- e.currentTarget.style.backgroundColor =
- "rgba(255, 255, 255, 0.2)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "transparent";
- }}
- >
- ×
-
-
-
- {/* Empty body; content moved to overlay */}
-
-
-
- {/* CSS Animations */}
-
-
- );
-
- return typeof document !== "undefined"
- ? createPortal(content, document.body)
- : content;
-};
-
-export default StorePurchaseSuccessPage;
diff --git a/src/pages/wallet/WalletHistoryPage.tsx b/src/pages/wallet/WalletHistoryPage.tsx
deleted file mode 100644
index 493dee8..0000000
--- a/src/pages/wallet/WalletHistoryPage.tsx
+++ /dev/null
@@ -1,525 +0,0 @@
-import { useState, useMemo, useEffect, useRef, type FC } from "react";
-import { useQuery } from "@apollo/client/react";
-import { Box } from "../../../styled-system/jsx";
-import multibg from "@/assets/images/multi_background.png";
-import Header from "@/components/common/Header";
-import { GET_WALLET_TRANSACTIONS } from "@/features/wallet/graphql/queries";
-
-type CategoryType = "ALL" | "PAYMENT" | "LANGUAGE";
-type PaymentSubType = "ALL" | "PURCHASE" | "REFUND";
-
-const WalletHistoryPage: FC = () => {
- const [category, setCategory] = useState("ALL");
- const [paymentSub, setPaymentSub] = useState("ALL");
- const [from, setFrom] = useState("");
- const [to, setTo] = useState("");
- const [page, setPage] = useState(1);
-
- const variables = useMemo(() => {
- const mappedType =
- category === "LANGUAGE"
- ? "GRANT"
- : category === "PAYMENT" && paymentSub !== "ALL"
- ? paymentSub
- : undefined;
- return {
- page,
- limit: 20,
- type: mappedType,
- from: from || undefined,
- to: to || undefined,
- };
- }, [page, category, paymentSub, from, to]);
-
- const { data, loading, error, refetch } = useQuery(GET_WALLET_TRANSACTIONS, {
- variables,
- notifyOnNetworkStatusChange: true,
- });
-
- const [allItems, setAllItems] = useState([]);
- const pageInfo = (data as any)?.walletTransactions?.pageInfo;
- const hasNextPage: boolean = !!pageInfo?.hasNextPage;
-
- // Reset accumulated list when filters change (page resets to 1 via handlers)
- useEffect(() => {
- setAllItems([]);
- }, [category, paymentSub, from, to]);
-
- // Append or replace items on data load
- useEffect(() => {
- const incoming = (data as any)?.walletTransactions?.items || [];
- setAllItems((prev) => (page === 1 ? incoming : [...prev, ...incoming]));
- }, [data, page]);
-
- // Infinite scroll sentinel
- const sentinelRef = useRef(null);
- useEffect(() => {
- const node = sentinelRef.current;
- if (!node) return;
- const observer = new IntersectionObserver(
- (entries) => {
- if (entries[0]?.isIntersecting && !loading && hasNextPage) {
- setPage((p) => p + 1);
- }
- },
- { root: null, rootMargin: "200px", threshold: 0 }
- );
- observer.observe(node);
- return () => observer.disconnect();
- }, [loading, hasNextPage]);
-
- const isToday = (d: Date) => {
- const now = new Date();
- return (
- d.getFullYear() === now.getFullYear() &&
- d.getMonth() === now.getMonth() &&
- d.getDate() === now.getDate()
- );
- };
-
- const isYesterday = (d: Date) => {
- const y = new Date();
- y.setDate(y.getDate() - 1);
- return (
- d.getFullYear() === y.getFullYear() &&
- d.getMonth() === y.getMonth() &&
- d.getDate() === y.getDate()
- );
- };
-
- const formatDateHeading = (iso: string) => {
- const dt = new Date(iso);
- if (isToday(dt)) return "NEWEST";
- if (isYesterday(dt)) return "YESTERDAY";
- return dt.toLocaleDateString(undefined, {
- day: "2-digit",
- month: "short",
- year: "numeric",
- });
- };
-
- const filteredItems = allItems.filter((tx) => {
- if (category === "ALL") return true;
- if (category === "LANGUAGE") return tx.type === "GRANT";
- if (paymentSub === "ALL")
- return tx.type === "PURCHASE" || tx.type === "REFUND";
- return tx.type === paymentSub;
- });
-
- const grouped: Record = {};
- for (const tx of filteredItems) {
- const k = formatDateHeading(tx.createdAt);
- if (!grouped[k]) grouped[k] = [];
- grouped[k].push(tx);
- }
-
- return (
-
-
- {}}
- onShowSetting={() => {}}
- onShowRanking={() => {}}
- />
-
-
-
-
-
-
-
- History
-
- window.history.back()}
- style={{
- position: "absolute",
- right: 12,
- top: "50%",
- transform: "translateY(-50%)",
- width: 32,
- height: 32,
- border: "2px solid #00FFFF",
- backgroundColor: "rgba(0,0,0,0.5)",
- color: "#00FFFF",
- borderRadius: 4,
- cursor: "pointer",
- }}
- >
- ✕
-
-
-
-
- {(
- [
- { key: "ALL", label: "All" },
- { key: "PAYMENT", label: "Payment" },
- { key: "LANGUAGE", label: "Language" },
- ] as const
- ).map((c) => (
-
{
- setPage(1);
- setCategory(c.key as CategoryType);
- if (c.key !== "PAYMENT") setPaymentSub("ALL");
- }}
- >
- {c.label}
-
- ))}
-
- {/* Right-aligned date range + Apply on same row */}
-
-
-
- Start Date
-
- {
- setPage(1);
- setFrom(e.target.value);
- }}
- style={{
- padding: "8px 8px",
- border: "1px solid #D1D5DB",
- borderRadius: 6,
- backgroundColor: "#FFFFFF",
- color: "#111827",
- }}
- />
-
-
- ~
-
-
-
- End Date
-
- {
- setPage(1);
- setTo(e.target.value);
- }}
- style={{
- padding: "8px 8px",
- border: "1px solid #D1D5DB",
- borderRadius: 6,
- backgroundColor: "#FFFFFF",
- color: "#111827",
- }}
- />
-
-
refetch()}
- style={{
- padding: "8px 12px",
- borderRadius: 6,
- backgroundColor: "#3B82F6",
- color: "#FFFFFF",
- cursor: "pointer",
- }}
- >
- Apply
-
-
-
- {category === "PAYMENT" && (
-
- {(
- [
- { key: "ALL", label: "All" },
- { key: "PURCHASE", label: "Purchasing" },
- { key: "REFUND", label: "Refunding" },
- ] as const
- ).map((p) => (
- {
- setPage(1);
- setPaymentSub(p.key as PaymentSubType);
- }}
- >
- {p.label}
-
- ))}
-
- )}
-
-
-
-
-
- {error && (
-
- Failed to load
-
- )}
-
- {Object.keys(grouped).map((section) => (
-
-
- {section}
-
- {grouped[section].map((tx: any) => {
- const amt = tx.amount as number;
- const amountColor =
- amt > 0 ? "#22C55E" : amt < 0 ? "#EF4444" : "#94A3B8";
- return (
-
-
-
-
-
- {tx.description || "Unknown"}
- {tx.type === "REFUND" && (
-
- Refunding
-
- )}
- {tx.type === "PURCHASE" && (
-
- Purchasing
-
- )}
- {tx.type === "GRANT" && (
-
- Language
-
- )}
-
-
- {new Date(tx.createdAt).toLocaleString()}
-
-
-
-
-
- {amt > 0 ? `+${amt}` : `${amt}`}
-
-
-
- );
- })}
-
- ))}
-
- {loading && page === 1 && (
-
Loading...
- )}
- {!loading && filteredItems.length === 0 && (
-
No transactions
- )}
-
- {loading && page > 1 && (
-
- Loading more...
-
- )}
-
-
- {/* Pagination controls removed in infinite scroll mode */}
-
-
-
-
- );
-};
-
-export default WalletHistoryPage;
diff --git a/src/routes/MeteoRoutes.tsx b/src/routes/MeteoRoutes.tsx
deleted file mode 100644
index 8b7cdeb..0000000
--- a/src/routes/MeteoRoutes.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-// @ts-nocheck
-import { Routes, Route } from "react-router-dom";
-import MeteoLandingPage from "../pages/meteo/MeteoLandingPage";
-import MeteoGamePage from "../pages/meteo/MeteoGamePage";
-
-const MeteoRoutes = () => {
- return (
-
-
- } />
- } />
-
-
- );
-};
-
-export default MeteoRoutes;
diff --git a/src/routes/MultiRoutes.tsx b/src/routes/MultiRoutes.tsx
deleted file mode 100644
index 4ea830b..0000000
--- a/src/routes/MultiRoutes.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-// @ts-nocheck
-import { Routes, Route } from "react-router-dom";
-import MultiPage from "../pages/multi/MultiPage";
-import RoomWaitingPage from "../pages/multi/RoomWaitingPage";
-import TypingBattlePage from "../pages/multi/TypingBattlePage";
-
-const MultiRoutes = () => {
- return (
-
- {/* 예시임 그냥 */}
- } />
- } />
- } />
-
- );
-};
-
-export default MultiRoutes;
diff --git a/src/routes/MyPageRoutes.tsx b/src/routes/MyPageRoutes.tsx
deleted file mode 100644
index d843b30..0000000
--- a/src/routes/MyPageRoutes.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Routes, Route } from "react-router-dom";
-import MyPage from "../pages/mypage/MyPage";
-import MyReport from "../pages/mypage/MyReport";
-import FollowerPage from "../pages/follower/FollowerPage";
-import FollowingPage from "../pages/following/FollowingPage";
-
-const MyPageRoutes = () => {
- return (
-
- } />
- } />
-
- );
-};
-
-export default MyPageRoutes;
diff --git a/src/routes/PrivateRoute.tsx b/src/routes/PrivateRoute.tsx
deleted file mode 100644
index 9f43eae..0000000
--- a/src/routes/PrivateRoute.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-// @ts-nocheck
-// components/PrivateRoute.jsx
-import { Navigate } from "react-router-dom";
-import useAuthStore from "../store/authStore";
-import { useSessionStore } from "../store/useSessionStore";
-import { useChatStore} from "../store/useChatStore";
-
-const PrivateRoute = ({ children }) => {
- const token = useAuthStore((state) => state.token);
- const isTokenValid = useAuthStore((state) => state.isTokenValid);
- const logout = useAuthStore((state) => state.logout);
- const clearSession = useSessionStore((state) => state.clearSession);
- const clearAllChats = useChatStore((state) => state.clearAllChats);
-
-
- if (!token || !isTokenValid()) {
- clearSession();
- clearAllChats();
- logout();
- return ;
- }
-
-
- return children;
-};
-
-export default PrivateRoute;
diff --git a/src/routes/README.md b/src/routes/README.md
deleted file mode 100644
index ab99c37..0000000
--- a/src/routes/README.md
+++ /dev/null
@@ -1 +0,0 @@
-### 라우팅 설정
\ No newline at end of file
diff --git a/src/routes/RankingRoutes.tsx b/src/routes/RankingRoutes.tsx
deleted file mode 100644
index 1fbd1b6..0000000
--- a/src/routes/RankingRoutes.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-// @ts-nocheck
-import { Routes, Route} from "react-router-dom"
-import RankingPage from "../pages/ranking/Ranking";
-
-const MyPageRoutes = () => {
- return (
-
- }/>
-
- );
-};
-
-export default MyPageRoutes;
\ No newline at end of file
diff --git a/src/routes/SingleRoutes.tsx b/src/routes/SingleRoutes.tsx
deleted file mode 100644
index 5b6d717..0000000
--- a/src/routes/SingleRoutes.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Routes, Route } from "react-router-dom";
-//import SingleLanguageSelectPage from "../pages/single/SingleLanguageSelectPage";
-import GameLobbyPage from "@/pages/main/GameLobbyPage";
-import CsSelectPage from "../pages/single/CsSelectPage";
-import GamePlayingPage from "../pages/single/GamePlayingPage";
-import FinishPage from "../pages/single/modal/FinishPage";
-import CsWordSelectPage from "../pages/single/modal/CsWordSelectPage";
-
-const SingleRoutes: React.FC = () => {
- return (
-
- } />
- } />
- } />
- {}}
- />
- }
- />
- } />
-
- );
-};
-
-export default SingleRoutes;
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
deleted file mode 100644
index e439912..0000000
--- a/src/routes/index.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-// @ts-nocheck
-// import { BrowserRouter, Routes, Route } from "react-router-dom";
-// import MainPage from "./pages/main/MainPage";
-// import LandingPage from "./pages/LandingPage";
-// import LoginPage from "./pages/auth/LoginPage";
-// import SignupPage from "./pages/auth/SignupPage";
-// import SingleRoutes from "./routes/SingleRoutes";
-// import MultiRoutes from "./routes/MultiRoutes";
-// import MeteoRoutes from "./routes/MeteoRoutes";
-// import MyPageRoutes from "./routes/MyPageRoutes";
-// import RankingRoutes from "./RankingRoutes"
-
-// function App() {
-// return (
-//
-//
-// } />
-// } />
-// } />
-// } />
-// } />
-// } />
-// } />
-// } />
-// } />
-//
-//
-// );
-// }
-
-// export default App;
diff --git a/src/store/README.md b/src/store/README.md
deleted file mode 100644
index 8fbb560..0000000
--- a/src/store/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-### 상태관리 (Zustand)
-
-- 서버 상태는 사용하지 않습니다. 서버 데이터는 Apollo Client를 사용합니다.
-- UI/게임 진행과 같은 로컬 상태만 Zustand로 관리합니다.
-- 새로운 스토어는 TypeScript(.ts/.tsx)로 작성합니다.
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
deleted file mode 100644
index e392a2f..0000000
--- a/src/store/authStore.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-// @ts-nocheck
-import { create } from 'zustand';
-import { persist } from 'zustand/middleware';
-import { jwtDecode } from 'jwt-decode'; // ✅
-
-
-const useAuthStore = create(
- persist(
- (set, get) => ({
- user: null, // { nickname: string }
- token: null,
- tokenExp: null,
-
- login: ({ nickname, token, userType = "member" }) => {
- let exp = null;
-
- if (userType === "member") {
- try {
- const decode = jwtDecode(token);
- exp = decode.exp;
- } catch (e) {
- exp = null;
- }
- }
- set({ user: { nickname, userType }, token, tokenExp: exp });
- },
-
- logout: () =>
- set({ user: null, token: null , tokenExp: null }),
-
- updateNickname: (newNickName) => {
- const { token, user, tokenExp} = get();
- if (user) {
- set({ user: { ...user, nickname: newNickName}, token, tokenExp});
- }
- },
-
- isTokenValid: () => {
- const { tokenExp , user } = get();
-
- if (user?.userType === 'guest') return true; // 비회원은 그냥 패스
-
- if (!tokenExp) return false; // 회원 인데 시간 없으면 무조건 false
-
- const now = Math.floor(Date.now() / 1000 );
- //console.log(now.toString());
- return now + 120 < tokenExp;
- }
-
- }),
- {
- name: 'auth-storage',
- }
- )
-);
-
-export default useAuthStore;
diff --git a/src/store/useChatStore.ts b/src/store/useChatStore.ts
deleted file mode 100644
index fa8efa5..0000000
--- a/src/store/useChatStore.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// @ts-nocheck
-import { create } from "zustand";
-import { persist } from 'zustand/middleware';
-
-const EXPIRY_DURATION_MS = 1000 * 60 * 60 * 24 * 7; // 7일
-
-export const useChatStore = create(
- persist(
- (set, get) => ({
-
- chats: {}, // { [codeId]: [ { sender, message, time } ] }
-
- // 특정 codeId에 메시지 추가
- addMessage: (codeId, message) => {
- set((state) => ({
- chats: {
- ...state.chats,
- [codeId]: [...(state.chats[codeId] ?? []), message]
- }
- }))
- },
-
- getMessages: (codeId) => (get().chats[codeId] || []),
-
- replaceLastMessage: (codeId, message) => {
- const current = get().chats[codeId] ?? [];
- if (current.length === 0) return;
- set((state) => ({
- chats: {
- ...state.chats,
- [codeId]: [...current.slice(0, -1), message],
- },
- }));
- },
-
- clearChat: (codeId) => set((state) => ({
- chats: {
- ...state.chats,
- [codeId]: []
- }
- })),
- clearAllChats: () => set({ chats: {} }),
-
- getChatByCodeId: (codeId) => get.chats?.[codeId] ?? [],
-
-
- }),
- {
- name: "chat-storage"
- }
- )
-);
diff --git a/src/store/useSearchStore.ts b/src/store/useSearchStore.ts
deleted file mode 100644
index c17e994..0000000
--- a/src/store/useSearchStore.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { create } from "zustand";
-
-interface SearchState {
- query: string;
- setQuery: (query: string) => void;
- clearQuery: () => void;
-}
-
-export const useSearchStore = create((set) => ({
- query: "",
- setQuery: (query: string) => set({ query }),
- clearQuery: () => set({ query: "" }),
-}));
diff --git a/src/store/useSessionStore.ts b/src/store/useSessionStore.ts
deleted file mode 100644
index 492b5a2..0000000
--- a/src/store/useSessionStore.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-// @ts-nocheck
-import { create } from "zustand";
-import { getSessionKey } from "../api/apiEncrytionApi";
-
-
-export const useSessionStore = create((set) => {
-
- let refreshTimer = null;
-
- const scheduleRefresh = (expireAt) => {
-
- if (refreshTimer) clearTimeout(refreshTimer);
-
- const expireAtUtcFixed = expireAt.endsWith('Z') ? expireAt : `${expireAt}Z`;
- const delayMs = new Date(expireAtUtcFixed).getTime() - Date.now() - 1000;
- if ( delayMs > 0 ) {
- // console.log(`${delayMs / 1000}s 후 자동 갱신`)
- refreshTimer = setTimeout(() => {
- getSessionKeyHandler();
- }, delayMs);
- } else {
- getSessionKeyHandler();
- }
- };
-
- const getSessionKeyHandler = async () => {
- try {
- const respnse = await getSessionKey();
- const { code, message } = respnse.status;
-
- if (code === 200) {
- const sessionKey = respnse.content.sessionKey;
- const expireAt = respnse.content.expireAt;
-
- set({ sessionKey, expireAt })
- localStorage.setItem('session', JSON.stringify({ sessionKey, expireAt}));
-
- scheduleRefresh(expireAt);
-
- } else {
- // console.log("오류메시지 : ", message);
- }
- } catch (e) {
- console.error("세션션 발급 실패요~~", e);
- throw e
- }
- }
-
- return {
- sessionKey: null,
- expireAt: null,
-
- setSession : () => getSessionKeyHandler(),
- clearSession : () => {
- if (refreshTimer) clearTimeout(refreshTimer); // ⛔ 타이머 정리
- refreshTimer = null;
-
- set(() => {
- localStorage.removeItem('session');
- return { sessionKey: null, expireAt: null};
- })
- },
- initSessionFromStorage: () => {
- const stored = JSON.parse(localStorage.getItem('session'));
- if (stored?.sessionKey && stored?.expireAt) {
- set({ sessionKey: stored.sessionKey, expireAt: stored.expireAt });
- scheduleRefresh(stored.expireAt);
- }
- },
-
- refreshSessionManually: () => getSessionKeyHandler(),
- };
-});
\ No newline at end of file
diff --git a/src/store/useVolumeStore.ts b/src/store/useVolumeStore.ts
deleted file mode 100644
index 8c57001..0000000
--- a/src/store/useVolumeStore.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// @ts-nocheck
-import { create } from "zustand";
-
-const defaultBgm = Number(localStorage.getItem("bgmVolume")) || 0.5;
-const defaultEffect = Number(localStorage.getItem("effectVolume")) || 0.7;
-
-const useVolumeStore = create((set) => ({
-
- bgmVolume: defaultBgm,
- effectVolume: defaultEffect,
-
- setBgmVolume: (volume) => {
- localStorage.setItem("bgmVolume", volume);
- set({ bgmVolume: volume });
- },
-
- setEffectVolume: (volume) => {
- localStorage.setItem("effectVolume", volume);
- set({ effectVolume: volume });
- }
-}));
-
-export default useVolumeStore;
\ No newline at end of file
diff --git a/src/store/useVolumsStore.ts b/src/store/useVolumsStore.ts
deleted file mode 100644
index 1cf0973..0000000
--- a/src/store/useVolumsStore.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// @ts-nocheck
-// Temporary compatibility re-export for legacy imports
-export { default } from "./useVolumeStore";
-export * from "./useVolumeStore";
diff --git a/src/store/userSettingStore.ts b/src/store/userSettingStore.ts
deleted file mode 100644
index b7c13a5..0000000
--- a/src/store/userSettingStore.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// @ts-nocheck
-import { create } from 'zustand';
-
-const defaultColors = {
- correct: '#98c379',
- wrong: '#e06c75',
- typing: '#ffffff'
-};
-
-export const userColorStore = create((set) => ({
-
- colors: JSON.parse(localStorage.getItem('text-colors')) || defaultColors,
-
- setColor : (type, color) => set((state) => {
- const newColors = { ...state.colors, [type]: color};
-
- // CSS 번수 반영
- const root = document.documentElement;
- root.style.setProperty('--typed-color', newColors.correct);
- root.style.setProperty('--wrong-color', newColors.wrong);
- root.style.setProperty('--pending-color', newColors.typing);
-
- localStorage.setItem('text-colors', JSON.stringify(newColors));
-
- return { colors: newColors};
- }),
-
- initColors: () => {
- const stored = JSON.parse(localStorage.getItem('text-colors'));
- const colors = stored || defaultColors;
-
- // CSS 변수 반영
- const root = document.documentElement;
- root.style.setProperty('--typed-color', colors.correct);
- root.style.setProperty('--wrong-color', colors.wrong);
- root.style.setProperty('--pending-color', colors.typing);
-
- return { colors };
- },
-
- resetSingleColor: (type) => set((state) => {
- const newColors = { ...state.colors, [type]: defaultColors[type] };
-
- const root = document.documentElement;
- root.style.setProperty('--typed-color', newColors.correct);
- root.style.setProperty('--wrong-color', newColors.wrong);
- root.style.setProperty('--pending-color', newColors.typing);
-
- localStorage.setItem('text-colors', JSON.stringify(newColors));
-
- return { colors: newColors };
- })
-}))
diff --git a/src/styles/README.md b/src/styles/README.md
deleted file mode 100644
index 0906117..0000000
--- a/src/styles/README.md
+++ /dev/null
@@ -1 +0,0 @@
-### 전역 스타일 및 테마 설정
\ No newline at end of file
diff --git a/src/styles/single/SinglePage.css b/src/styles/single/SinglePage.css
deleted file mode 100644
index 9d70ec8..0000000
--- a/src/styles/single/SinglePage.css
+++ /dev/null
@@ -1,67 +0,0 @@
-:root {
- --typed-color: #98c379;
- --wrong-color: #e06c75;
- --pending-color: #ffffff;
-}
-
-.hljs {
- padding: 0 !important;
- margin: 0 !important;
- border: none !important;
- }
-
-.typed, .pending, .wrong, .currentInput{
- font-family: 'D2Coding' !important;
- /* font-weight: bold; */
- display: inline-block;
- width: 11px;
-}
-
-.typed {
- color: var(--typed-color); /* 초록색 (맞은 글자) */
-}
-
-.wrong {
- color: var(--wrong-color); /* 빨간색 (틀린 글자) */
-}
-
-.pending {
- color: var(--pending-color); /* 회색 (아직 안 친 글자) */
- opacity: 0.6;
-}
-
-.currentLine {
- /* color: var(--pending-color); */
- opacity: 1 !important; /* 현재 타이핑 중인 줄은 opacity 1 */
-}
-
-.blinking-cursor {
- animation: blink 1.2s infinite;
- }
-
-@keyframes blink {
- 0% { opacity: 1; }
- 50% { opacity: 0; }
- 100% { opacity: 1; }
-}
-
-.single-input {
- font-family: 'D2Coding' !important;
- outline: none;
-}
-
-.cursor {
- display: inline-block;
- width: 1px;
- height: 20px;
- background-color: var(--pending-color); /* 커서 색상 - 원하는 색상으로 변경 가능 */
- /* margin-left: -1px; */
- animation: blink 1s step-end infinite;
-}
-
-.cursor-container {
- display: inline-flex; /* flexbox로 변경하여 정렬을 더 쉽게 처리 */
- vertical-align: baseline;
-}
-
-
diff --git a/src/test-setup.ts b/src/test-setup.ts
deleted file mode 100644
index d75f559..0000000
--- a/src/test-setup.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import "@testing-library/jest-dom";
-import { expect, afterEach, vi } from "vitest";
-import { cleanup } from "@testing-library/react";
-
-// Cleanup after each test
-afterEach(() => {
- cleanup();
-});
-
-// Mock IntersectionObserver
-global.IntersectionObserver = class IntersectionObserver {
- constructor() {}
- disconnect() {}
- observe() {}
- takeRecords() {
- return [];
- }
- unobserve() {}
-} as any;
diff --git a/src/types/ambient.d.ts b/src/types/ambient.d.ts
deleted file mode 100644
index f70b0ee..0000000
--- a/src/types/ambient.d.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-declare module "@/store/authStore" {
- export interface AuthStoreState {
- user: { nickname: string; userType?: string } | null;
- token: string | null;
- tokenExp: number | null;
- login: (args: {
- nickname: string;
- token: string;
- userType?: string;
- }) => void;
- logout: () => void;
- updateNickname: (newNickName: string) => void;
- isTokenValid: () => boolean;
- }
- const useAuthStore: {
- (selector?: (s: AuthStoreState) => any): any;
- getState: () => AuthStoreState;
- };
- export default useAuthStore;
-}
-
-// Socket client module removed
-
-declare module "@/store/useSessionStore" {
- export interface SessionStoreState {
- initSessionFromStorage: () => void;
- clearSession: () => void;
- setSession: () => void;
- }
- export const useSessionStore: {
- (selector?: (s: SessionStoreState) => any): any;
- getState: () => SessionStoreState;
- };
-}
diff --git a/src/utils/README.md b/src/utils/README.md
deleted file mode 100644
index e120360..0000000
--- a/src/utils/README.md
+++ /dev/null
@@ -1 +0,0 @@
-### 유틸리티 함수들
\ No newline at end of file
diff --git a/src/utils/cryptoUtils.ts b/src/utils/cryptoUtils.ts
deleted file mode 100644
index 15f10bb..0000000
--- a/src/utils/cryptoUtils.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// @ts-nocheck
-import CryptoJS from 'crypto-js';
-import { useSessionStore } from '../store/useSessionStore';
-
-export const encryptWithSessionKey = (data) => {
-
- const sessionKey = useSessionStore.getState().sessionKey;
- if (!sessionKey) throw new Error("sessionKey가 없습니다.");
-
- const key = CryptoJS.enc.Base64.parse(sessionKey); // 🔑 키는 Base64라고 가정 (필요시 직접 Hex, Utf8로 맞추기)
- const iv = CryptoJS.lib.WordArray.random(16); // 16바이트 IV 생성
- // console.log(key.sigBytes)
-
- const dataStr = JSON.stringify(data);
-
- const encrypted = CryptoJS.AES.encrypt(dataStr, key, {
- iv: iv,
- mode: CryptoJS.mode.CBC,
- padding: CryptoJS.pad.Pkcs7,
- });
-
- // ✅ [IV + CipherText]를 Base64로 인코딩
- const combined = iv.concat(encrypted.ciphertext);
- const encryptedBase64 = CryptoJS.enc.Base64.stringify(combined);
-
- return encryptedBase64;
-};
diff --git a/src/utils/formatTimeUtils.ts b/src/utils/formatTimeUtils.ts
deleted file mode 100644
index e8aa534..0000000
--- a/src/utils/formatTimeUtils.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-// @ts-nocheck
-/**
- * time 포멧 함수
- * @param {number} ms - 밀리세컨드
- * @returns {string} - 00:00:00
-*/
-export const formatTime = (ms) => {
- const totalSeconds = Math.floor(ms / 1000);
- const minutes = Math.floor(totalSeconds / 60);
- const seconds = totalSeconds % 60;
- const milliseconds = Math.floor((ms % 1000) / 10);
-
- return `${String(minutes).padStart(2,'0')}:${String(seconds).padStart(2, '0')}:${String(milliseconds).padStart(2, '0')}`
-}
\ No newline at end of file
diff --git a/src/utils/tokenUtils.ts b/src/utils/tokenUtils.ts
deleted file mode 100644
index 5017270..0000000
--- a/src/utils/tokenUtils.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// @ts-nocheck
-// utils/tokenUtils.js
-export const getAccessToken = () => {
- try {
- const data = JSON.parse(localStorage.getItem("auth-storage"));
- return data?.state?.token || null;
- } catch (e) {
- return null;
- }
-};
diff --git a/src/utils/typingUtils.ts b/src/utils/typingUtils.ts
deleted file mode 100644
index de3ca37..0000000
--- a/src/utils/typingUtils.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-// @ts-nocheck
-
-/**
- * 타수(WPM) 계산
- * @param {number} totalTypedChars - 지금까지 타이핑한 총 글자 수
- * @param {number} elapsedSeconds - 흐른 시간(초)
- * @returns {number} - 계산된 타수 (WPM)
-*/
-export const calculateWPM = (totalTypedChars, elapsedSeconds) => {
- if (elapsedSeconds === 0) return 0;
-
- const words = totalTypedChars / 5;
- const minutes = elapsedSeconds / 60;
-
- return Math.floor(words / minutes)
-}
-
-
-/**
- * 타수(CPM) 계산
- * @param {number} totalTypedChars - 지금까지 타이핑한 총 글자 수
- * @param {number} elapsedSeconds - 흐른 시간(초)
- * @returns {number} - 계산된 타수 (WPM * 5)
-*/
-export const calculateCPM = (totalTypedChars, elapsedSeconds) => {
- if (elapsedSeconds === 0) return 0;
- const minutes = elapsedSeconds / 60;
-
- return Math.floor(totalTypedChars / minutes)
-};
-
-
-/**
- * 타수(WPM) 계산
- * @param {number} cpm - 현재 계산된 타수
- * @returns {number} - 속도 진행률(%)
-*/
-export const getSpeedProgress = (cpm) => {
- if (cpm >= 300) return 100;
- return Math.floor((cpm / 300) * 100);
-}
-
-/**
- * 싱글모드 진행률 계산
- * @param {number} currentLineIndex - 현재 진행중인 줄
- * @param {number} lineIndex - 총 줄 수
- * @returns {number} - 총 진행률(%)
-*/
-export const getProgress = (currentLineIndex, lineIndex) => {
- if (lineIndex === 0) return 0;
- return Math.floor((currentLineIndex / lineIndex) * 100);
-}
-
-/**
- *
- * @param {string} code - 타이핑할 코드
- * @returns {Array} - 줄별 맨 앞의 공백 수, 즐 별 글자 리스트
- */
-
-export const processCode = (code) =>{
-
- const lines = [] // 코드의 각 줄을 저장할 배열
- const space = [] // 각 줄 앞의 공백 수를 저장할 배열
- const charCount = []
-
- const codeLines = code.split('\n');
-
- codeLines.forEach(line => {
- const trimmedLine = line.trimStart(); // 앞의 공백 삭제
- const leadingSpaceCount = line.length - trimmedLine.length; // 앞에 공백 개수
-
- // 한 줄을 글자 단위로 쪼개어 lines 배열에 추가
- lines.push(trimmedLine.split(''));
-
- // 공백 개수 저장
- if (trimmedLine.length === 0) {
- space.push(1);
- } else {
- space.push(leadingSpaceCount);
- }
- // 줄 별 글자 수
- charCount.push(trimmedLine.length);
- })
-
- return {lines, space, charCount};
-
-}
-
-/**
- * 엔터를 클릭시 해당 줄의 유효성을 검사하는 함수
- * @param {Array} inputArray - 사용자가 입력한 코드
- * @param {Array} lineArray - 스크립트 코드
- * @returns {boolean} - 일치 여부 리턴
- */
-export const compareInputWithLineEnter = (inputArray, lineArray) => {
-
- for (let i = 0; i < inputArray.length; i++) {
- if(inputArray[i] !== lineArray[i]) {
- return false;
- }
- }
- return inputArray.length === lineArray.length; // 두 배열 길이가 다를수도 있어서 길이 일치 여부로 리턴해야함함
-}
-
-/**
- * 사용자가 input과 스크립트가 일치하는지 검사하는 함수
- * @param {string} input - 사용자가 입력한 코드
- * @param {Array} lineArray - 스크립트 코드드
- * @returns {boolean} - 일치 여부 리턴
- */
-export const compareInputWithLine = (input, lineArray) => {
-
- for (let i = 0; i < input.length; i++) {
- if(input[i] !== lineArray[i]) {
- return true;
- }
- }
- return false;
-}
-
-/**
- *
- * @param {Array} input
- * @param {Array} lineArray
- * @param {number} currentCharIndex
- * @returns {number} 일치하는 글자 수
- */
-export const calculateCurrentLineTypedChars = (input, lineArray) => {
- if (!Array.isArray(lineArray)) {
- //console.error("lineArray is not an array:", lineArray);
- return 0;
- }
- let cnt = 0;
- for (let i = 0; i < input.length ; i++) {
- if(input[i] === lineArray[i]) {
- cnt++;
- }
- }
- return cnt;
-}
\ No newline at end of file
diff --git a/styled-system/css/conditions.mjs b/styled-system/css/conditions.mjs
deleted file mode 100644
index c9b2e15..0000000
--- a/styled-system/css/conditions.mjs
+++ /dev/null
@@ -1,36 +0,0 @@
-import { withoutSpace } from '../helpers.mjs';
-
-const conditionsStr = "_hover,_focus,_focusWithin,_focusVisible,_disabled,_active,_visited,_target,_readOnly,_readWrite,_empty,_checked,_enabled,_expanded,_highlighted,_complete,_incomplete,_dragging,_before,_after,_firstLetter,_firstLine,_marker,_selection,_file,_backdrop,_first,_last,_only,_even,_odd,_firstOfType,_lastOfType,_onlyOfType,_peerFocus,_peerHover,_peerActive,_peerFocusWithin,_peerFocusVisible,_peerDisabled,_peerChecked,_peerInvalid,_peerExpanded,_peerPlaceholderShown,_groupFocus,_groupHover,_groupActive,_groupFocusWithin,_groupFocusVisible,_groupDisabled,_groupChecked,_groupExpanded,_groupInvalid,_indeterminate,_required,_valid,_invalid,_autofill,_inRange,_outOfRange,_placeholder,_placeholderShown,_pressed,_selected,_grabbed,_underValue,_overValue,_atValue,_default,_optional,_open,_closed,_fullscreen,_loading,_hidden,_current,_currentPage,_currentStep,_today,_unavailable,_rangeStart,_rangeEnd,_now,_topmost,_motionReduce,_motionSafe,_print,_landscape,_portrait,_dark,_light,_osDark,_osLight,_highContrast,_lessContrast,_moreContrast,_ltr,_rtl,_scrollbar,_scrollbarThumb,_scrollbarTrack,_horizontal,_vertical,_icon,_starting,_noscript,_invertedColors,sm,smOnly,smDown,md,mdOnly,mdDown,lg,lgOnly,lgDown,xl,xlOnly,xlDown,2xl,2xlOnly,2xlDown,smToMd,smToLg,smToXl,smTo2xl,mdToLg,mdToXl,mdTo2xl,lgToXl,lgTo2xl,xlTo2xl,@/xs,@/sm,@/md,@/lg,@/xl,@/2xl,@/3xl,@/4xl,@/5xl,@/6xl,@/7xl,@/8xl,base"
-const conditions = new Set(conditionsStr.split(','))
-
-const conditionRegex = /^@|&|&$/
-
-export function isCondition(value){
- return conditions.has(value) || conditionRegex.test(value)
-}
-
-const underscoreRegex = /^_/
-const conditionsSelectorRegex = /&|@/
-
-export function finalizeConditions(paths){
- return paths.map((path) => {
- if (conditions.has(path)){
- return path.replace(underscoreRegex, '')
- }
-
- if (conditionsSelectorRegex.test(path)){
- return `[${withoutSpace(path.trim())}]`
- }
-
- return path
- })}
-
- export function sortConditions(paths){
- return paths.sort((a, b) => {
- const aa = isCondition(a)
- const bb = isCondition(b)
- if (aa && !bb) return 1
- if (!aa && bb) return -1
- return 0
- })
- }
\ No newline at end of file
diff --git a/styled-system/css/css.d.ts b/styled-system/css/css.d.ts
deleted file mode 100644
index 496da41..0000000
--- a/styled-system/css/css.d.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/* eslint-disable */
-import type { SystemStyleObject } from '../types/index';
-
-type Styles = SystemStyleObject | undefined | null | false
-
-interface CssRawFunction {
- (styles: Styles): SystemStyleObject
- (styles: Styles[]): SystemStyleObject
- (...styles: Array): SystemStyleObject
- (styles: Styles): SystemStyleObject
-}
-
-interface CssFunction {
- (styles: Styles): string
- (styles: Styles[]): string
- (...styles: Array): string
- (styles: Styles): string
-
- raw: CssRawFunction
-}
-
-export declare const css: CssFunction;
\ No newline at end of file
diff --git a/styled-system/css/css.mjs b/styled-system/css/css.mjs
deleted file mode 100644
index ea5d82f..0000000
--- a/styled-system/css/css.mjs
+++ /dev/null
@@ -1,45 +0,0 @@
-import { createCss, createMergeCss, hypenateProperty, withoutSpace } from '../helpers.mjs';
-import { sortConditions, finalizeConditions } from './conditions.mjs';
-
-const utilities = "aspectRatio:asp,boxDecorationBreak:bx-db,zIndex:z,boxSizing:bx-s,objectPosition:obj-p,objectFit:obj-f,overscrollBehavior:ovs-b,overscrollBehaviorX:ovs-bx,overscrollBehaviorY:ovs-by,position:pos/1,top:top,left:left,inset:inset,insetInline:inset-x/insetX,insetBlock:inset-y/insetY,insetBlockEnd:inset-be,insetBlockStart:inset-bs,insetInlineEnd:inset-e/insetEnd/end,insetInlineStart:inset-s/insetStart/start,right:right,bottom:bottom,float:float,visibility:vis,display:d,hideFrom:hide,hideBelow:show,flexBasis:flex-b,flex:flex,flexDirection:flex-d/flexDir,flexGrow:flex-g,flexShrink:flex-sh,gridTemplateColumns:grid-tc,gridTemplateRows:grid-tr,gridColumn:grid-c,gridRow:grid-r,gridColumnStart:grid-cs,gridColumnEnd:grid-ce,gridAutoFlow:grid-af,gridAutoColumns:grid-ac,gridAutoRows:grid-ar,gap:gap,gridGap:grid-g,gridRowGap:grid-rg,gridColumnGap:grid-cg,rowGap:rg,columnGap:cg,justifyContent:jc,alignContent:ac,alignItems:ai,alignSelf:as,padding:p/1,paddingLeft:pl/1,paddingRight:pr/1,paddingTop:pt/1,paddingBottom:pb/1,paddingBlock:py/1/paddingY,paddingBlockEnd:pbe,paddingBlockStart:pbs,paddingInline:px/paddingX/1,paddingInlineEnd:pe/1/paddingEnd,paddingInlineStart:ps/1/paddingStart,marginLeft:ml/1,marginRight:mr/1,marginTop:mt/1,marginBottom:mb/1,margin:m/1,marginBlock:my/1/marginY,marginBlockEnd:mbe,marginBlockStart:mbs,marginInline:mx/1/marginX,marginInlineEnd:me/1/marginEnd,marginInlineStart:ms/1/marginStart,spaceX:sx,spaceY:sy,outlineWidth:ring-w/ringWidth,outlineColor:ring-c/ringColor,outline:ring/1,outlineOffset:ring-o/ringOffset,focusRing:focus-ring,focusVisibleRing:focus-v-ring,focusRingColor:focus-ring-c,focusRingOffset:focus-ring-o,focusRingWidth:focus-ring-w,focusRingStyle:focus-ring-s,divideX:dvd-x,divideY:dvd-y,divideColor:dvd-c,divideStyle:dvd-s,width:w/1,inlineSize:w-is,minWidth:min-w/minW,minInlineSize:min-w-is,maxWidth:max-w/maxW,maxInlineSize:max-w-is,height:h/1,blockSize:h-bs,minHeight:min-h/minH,minBlockSize:min-h-bs,maxHeight:max-h/maxH,maxBlockSize:max-b,boxSize:size,color:c,fontFamily:ff,fontSize:fs,fontSizeAdjust:fs-a,fontPalette:fp,fontKerning:fk,fontFeatureSettings:ff-s,fontWeight:fw,fontSmoothing:fsmt,fontVariant:fv,fontVariantAlternates:fv-alt,fontVariantCaps:fv-caps,fontVariationSettings:fv-s,fontVariantNumeric:fv-num,letterSpacing:ls,lineHeight:lh,textAlign:ta,textDecoration:td,textDecorationColor:td-c,textEmphasisColor:te-c,textDecorationStyle:td-s,textDecorationThickness:td-t,textUnderlineOffset:tu-o,textTransform:tt,textIndent:ti,textShadow:tsh,textShadowColor:tsh-c/textShadowColor,textOverflow:tov,verticalAlign:va,wordBreak:wb,textWrap:tw,truncate:trunc,lineClamp:lc,listStyleType:li-t,listStylePosition:li-pos,listStyleImage:li-img,listStyle:li-s,backgroundPosition:bg-p/bgPosition,backgroundPositionX:bg-p-x/bgPositionX,backgroundPositionY:bg-p-y/bgPositionY,backgroundAttachment:bg-a/bgAttachment,backgroundClip:bg-cp/bgClip,background:bg/1,backgroundColor:bg-c/bgColor,backgroundOrigin:bg-o/bgOrigin,backgroundImage:bg-i/bgImage,backgroundRepeat:bg-r/bgRepeat,backgroundBlendMode:bg-bm/bgBlendMode,backgroundSize:bg-s/bgSize,backgroundGradient:bg-grad/bgGradient,backgroundLinear:bg-linear/bgLinear,backgroundRadial:bg-radial/bgRadial,backgroundConic:bg-conic/bgConic,textGradient:txt-grad,gradientFromPosition:grad-from-pos,gradientToPosition:grad-to-pos,gradientFrom:grad-from,gradientTo:grad-to,gradientVia:grad-via,gradientViaPosition:grad-via-pos,borderRadius:bdr/rounded,borderTopLeftRadius:bdr-tl/roundedTopLeft,borderTopRightRadius:bdr-tr/roundedTopRight,borderBottomRightRadius:bdr-br/roundedBottomRight,borderBottomLeftRadius:bdr-bl/roundedBottomLeft,borderTopRadius:bdr-t/roundedTop,borderRightRadius:bdr-r/roundedRight,borderBottomRadius:bdr-b/roundedBottom,borderLeftRadius:bdr-l/roundedLeft,borderStartStartRadius:bdr-ss/roundedStartStart,borderStartEndRadius:bdr-se/roundedStartEnd,borderStartRadius:bdr-s/roundedStart,borderEndStartRadius:bdr-es/roundedEndStart,borderEndEndRadius:bdr-ee/roundedEndEnd,borderEndRadius:bdr-e/roundedEnd,border:bd,borderWidth:bd-w,borderTopWidth:bd-t-w,borderLeftWidth:bd-l-w,borderRightWidth:bd-r-w,borderBottomWidth:bd-b-w,borderBlockStartWidth:bd-bs-w,borderBlockEndWidth:bd-be-w,borderColor:bd-c,borderInline:bd-x/borderX,borderInlineWidth:bd-x-w/borderXWidth,borderInlineColor:bd-x-c/borderXColor,borderBlock:bd-y/borderY,borderBlockWidth:bd-y-w/borderYWidth,borderBlockColor:bd-y-c/borderYColor,borderLeft:bd-l,borderLeftColor:bd-l-c,borderInlineStart:bd-s/borderStart,borderInlineStartWidth:bd-s-w/borderStartWidth,borderInlineStartColor:bd-s-c/borderStartColor,borderRight:bd-r,borderRightColor:bd-r-c,borderInlineEnd:bd-e/borderEnd,borderInlineEndWidth:bd-e-w/borderEndWidth,borderInlineEndColor:bd-e-c/borderEndColor,borderTop:bd-t,borderTopColor:bd-t-c,borderBottom:bd-b,borderBottomColor:bd-b-c,borderBlockEnd:bd-be,borderBlockEndColor:bd-be-c,borderBlockStart:bd-bs,borderBlockStartColor:bd-bs-c,opacity:op,boxShadow:bx-sh/shadow,boxShadowColor:bx-sh-c/shadowColor,mixBlendMode:mix-bm,filter:filter,brightness:brightness,contrast:contrast,grayscale:grayscale,hueRotate:hue-rotate,invert:invert,saturate:saturate,sepia:sepia,dropShadow:drop-shadow,blur:blur,backdropFilter:bkdp,backdropBlur:bkdp-blur,backdropBrightness:bkdp-brightness,backdropContrast:bkdp-contrast,backdropGrayscale:bkdp-grayscale,backdropHueRotate:bkdp-hue-rotate,backdropInvert:bkdp-invert,backdropOpacity:bkdp-opacity,backdropSaturate:bkdp-saturate,backdropSepia:bkdp-sepia,borderCollapse:bd-cl,borderSpacing:bd-sp,borderSpacingX:bd-sx,borderSpacingY:bd-sy,tableLayout:tbl,transitionTimingFunction:trs-tmf,transitionDelay:trs-dly,transitionDuration:trs-dur,transitionProperty:trs-prop,transition:trs,animation:anim,animationName:anim-n,animationTimingFunction:anim-tmf,animationDuration:anim-dur,animationDelay:anim-dly,animationPlayState:anim-ps,animationComposition:anim-comp,animationFillMode:anim-fm,animationDirection:anim-dir,animationIterationCount:anim-ic,animationRange:anim-r,animationState:anim-s,animationRangeStart:anim-rs,animationRangeEnd:anim-re,animationTimeline:anim-tl,transformOrigin:trf-o,transformBox:trf-b,transformStyle:trf-s,transform:trf,rotate:rotate,rotateX:rotate-x,rotateY:rotate-y,rotateZ:rotate-z,scale:scale,scaleX:scale-x,scaleY:scale-y,translate:translate,translateX:translate-x/x,translateY:translate-y/y,translateZ:translate-z/z,accentColor:ac-c,caretColor:ca-c,scrollBehavior:scr-bhv,scrollbar:scr-bar,scrollbarColor:scr-bar-c,scrollbarGutter:scr-bar-g,scrollbarWidth:scr-bar-w,scrollMargin:scr-m,scrollMarginLeft:scr-ml,scrollMarginRight:scr-mr,scrollMarginTop:scr-mt,scrollMarginBottom:scr-mb,scrollMarginBlock:scr-my/scrollMarginY,scrollMarginBlockEnd:scr-mbe,scrollMarginBlockStart:scr-mbt,scrollMarginInline:scr-mx/scrollMarginX,scrollMarginInlineEnd:scr-me,scrollMarginInlineStart:scr-ms,scrollPadding:scr-p,scrollPaddingBlock:scr-py/scrollPaddingY,scrollPaddingBlockStart:scr-pbs,scrollPaddingBlockEnd:scr-pbe,scrollPaddingInline:scr-px/scrollPaddingX,scrollPaddingInlineEnd:scr-pe,scrollPaddingInlineStart:scr-ps,scrollPaddingLeft:scr-pl,scrollPaddingRight:scr-pr,scrollPaddingTop:scr-pt,scrollPaddingBottom:scr-pb,scrollSnapAlign:scr-sa,scrollSnapStop:scrs-s,scrollSnapType:scrs-t,scrollSnapStrictness:scrs-strt,scrollSnapMargin:scrs-m,scrollSnapMarginTop:scrs-mt,scrollSnapMarginBottom:scrs-mb,scrollSnapMarginLeft:scrs-ml,scrollSnapMarginRight:scrs-mr,scrollSnapCoordinate:scrs-c,scrollSnapDestination:scrs-d,scrollSnapPointsX:scrs-px,scrollSnapPointsY:scrs-py,scrollSnapTypeX:scrs-tx,scrollSnapTypeY:scrs-ty,scrollTimeline:scrtl,scrollTimelineAxis:scrtl-a,scrollTimelineName:scrtl-n,touchAction:tch-a,userSelect:us,overflow:ov,overflowWrap:ov-wrap,overflowX:ov-x,overflowY:ov-y,overflowAnchor:ov-a,overflowBlock:ov-b,overflowInline:ov-i,overflowClipBox:ovcp-bx,overflowClipMargin:ovcp-m,overscrollBehaviorBlock:ovs-bb,overscrollBehaviorInline:ovs-bi,fill:fill,stroke:stk,strokeWidth:stk-w,strokeDasharray:stk-dsh,strokeDashoffset:stk-do,strokeLinecap:stk-lc,strokeLinejoin:stk-lj,strokeMiterlimit:stk-ml,strokeOpacity:stk-op,srOnly:sr,debug:debug,appearance:ap,backfaceVisibility:bfv,clipPath:cp-path,hyphens:hy,mask:msk,maskImage:msk-i,maskSize:msk-s,textSizeAdjust:txt-adj,container:cq,containerName:cq-n,containerType:cq-t,cursor:cursor,textStyle:textStyle"
-
-const classNameByProp = new Map()
-const shorthands = new Map()
-utilities.split(',').forEach((utility) => {
- const [prop, meta] = utility.split(':')
- const [className, ...shorthandList] = meta.split('/')
- classNameByProp.set(prop, className)
- if (shorthandList.length) {
- shorthandList.forEach((shorthand) => {
- shorthands.set(shorthand === '1' ? className : shorthand, prop)
- })
- }
-})
-
-const resolveShorthand = (prop) => shorthands.get(prop) || prop
-
-const context = {
-
- conditions: {
- shift: sortConditions,
- finalize: finalizeConditions,
- breakpoints: { keys: ["base","sm","md","lg","xl","2xl"] }
- },
- utility: {
-
- transform: (prop, value) => {
- const key = resolveShorthand(prop)
- const propKey = classNameByProp.get(key) || hypenateProperty(key)
- return { className: `${propKey}_${withoutSpace(value)}` }
- },
- hasShorthand: true,
- toHash: (path, hashFn) => hashFn(path.join(":")),
- resolveShorthand: resolveShorthand,
- }
-}
-
-const cssFn = createCss(context)
-export const css = (...styles) => cssFn(mergeCss(...styles))
-css.raw = (...styles) => mergeCss(...styles)
-
-export const { mergeCss, assignCss } = createMergeCss(context)
\ No newline at end of file
diff --git a/styled-system/css/cva.d.ts b/styled-system/css/cva.d.ts
deleted file mode 100644
index ff43325..0000000
--- a/styled-system/css/cva.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/* eslint-disable */
-import type { RecipeCreatorFn } from '../types/recipe';
-
-export declare const cva: RecipeCreatorFn
-
-export type { RecipeVariant, RecipeVariantProps } from '../types/recipe';
\ No newline at end of file
diff --git a/styled-system/css/cva.mjs b/styled-system/css/cva.mjs
deleted file mode 100644
index 831b5e6..0000000
--- a/styled-system/css/cva.mjs
+++ /dev/null
@@ -1,87 +0,0 @@
-import { compact, mergeProps, memo, splitProps, uniq } from '../helpers.mjs';
-import { css, mergeCss } from './css.mjs';
-
-const defaults = (conf) => ({
- base: {},
- variants: {},
- defaultVariants: {},
- compoundVariants: [],
- ...conf,
-})
-
-export function cva(config) {
- const { base, variants, defaultVariants, compoundVariants } = defaults(config)
- const getVariantProps = (variants) => ({ ...defaultVariants, ...compact(variants) })
-
- function resolve(props = {}) {
- const computedVariants = getVariantProps(props)
- let variantCss = { ...base }
- for (const [key, value] of Object.entries(computedVariants)) {
- if (variants[key]?.[value]) {
- variantCss = mergeCss(variantCss, variants[key][value])
- }
- }
- const compoundVariantCss = getCompoundVariantCss(compoundVariants, computedVariants)
- return mergeCss(variantCss, compoundVariantCss)
- }
-
- function merge(__cva) {
- const override = defaults(__cva.config)
- const variantKeys = uniq(__cva.variantKeys, Object.keys(variants))
- return cva({
- base: mergeCss(base, override.base),
- variants: Object.fromEntries(
- variantKeys.map((key) => [key, mergeCss(variants[key], override.variants[key])]),
- ),
- defaultVariants: mergeProps(defaultVariants, override.defaultVariants),
- compoundVariants: [...compoundVariants, ...override.compoundVariants],
- })
- }
-
- function cvaFn(props) {
- return css(resolve(props))
- }
-
- const variantKeys = Object.keys(variants)
-
- function splitVariantProps(props) {
- return splitProps(props, variantKeys)
- }
-
- const variantMap = Object.fromEntries(Object.entries(variants).map(([key, value]) => [key, Object.keys(value)]))
-
- return Object.assign(memo(cvaFn), {
- __cva__: true,
- variantMap,
- variantKeys,
- raw: resolve,
- config,
- merge,
- splitVariantProps,
- getVariantProps
- })
-}
-
-export function getCompoundVariantCss(compoundVariants, variantMap) {
- let result = {}
- compoundVariants.forEach((compoundVariant) => {
- const isMatching = Object.entries(compoundVariant).every(([key, value]) => {
- if (key === 'css') return true
-
- const values = Array.isArray(value) ? value : [value]
- return values.some((value) => variantMap[key] === value)
- })
-
- if (isMatching) {
- result = mergeCss(result, compoundVariant.css)
- }
- })
-
- return result
-}
-
-export function assertCompoundVariant(name, compoundVariants, variants, prop) {
- if (compoundVariants.length > 0 && typeof variants?.[prop] === 'object') {
- throw new Error(`[recipe:${name}:${prop}] Conditions are not supported when using compound variants.`)
- }
-}
diff --git a/styled-system/css/cx.d.ts b/styled-system/css/cx.d.ts
deleted file mode 100644
index 892c90c..0000000
--- a/styled-system/css/cx.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/* eslint-disable */
-type Argument = string | boolean | null | undefined
-
-/** Conditionally join classNames into a single string */
-export declare function cx(...args: Argument[]): string
\ No newline at end of file
diff --git a/styled-system/css/cx.mjs b/styled-system/css/cx.mjs
deleted file mode 100644
index 81bbdae..0000000
--- a/styled-system/css/cx.mjs
+++ /dev/null
@@ -1,15 +0,0 @@
-function cx() {
- let str = '',
- i = 0,
- arg
-
- for (; i < arguments.length; ) {
- if ((arg = arguments[i++]) && typeof arg === 'string') {
- str && (str += ' ')
- str += arg
- }
- }
- return str
-}
-
-export { cx }
\ No newline at end of file
diff --git a/styled-system/css/index.d.ts b/styled-system/css/index.d.ts
deleted file mode 100644
index 50a581d..0000000
--- a/styled-system/css/index.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/* eslint-disable */
-export * from './css';
-export * from './cx';
-export * from './cva';
-export * from './sva';
\ No newline at end of file
diff --git a/styled-system/css/index.mjs b/styled-system/css/index.mjs
deleted file mode 100644
index f2392bd..0000000
--- a/styled-system/css/index.mjs
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './css.mjs';
-export * from './cx.mjs';
-export * from './cva.mjs';
-export * from './sva.mjs';
\ No newline at end of file
diff --git a/styled-system/css/sva.d.ts b/styled-system/css/sva.d.ts
deleted file mode 100644
index f97c42a..0000000
--- a/styled-system/css/sva.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-/* eslint-disable */
-import type { SlotRecipeCreatorFn } from '../types/recipe';
-
-export declare const sva: SlotRecipeCreatorFn
\ No newline at end of file
diff --git a/styled-system/css/sva.mjs b/styled-system/css/sva.mjs
deleted file mode 100644
index 640ad26..0000000
--- a/styled-system/css/sva.mjs
+++ /dev/null
@@ -1,46 +0,0 @@
-import { compact, getSlotRecipes, memo, splitProps } from '../helpers.mjs';
-import { cva } from './cva.mjs';
-import { cx } from './cx.mjs';
-
-export function sva(config) {
- const slots = Object.entries(getSlotRecipes(config)).map(([slot, slotCva]) => [slot, cva(slotCva)])
- const defaultVariants = config.defaultVariants ?? {}
-
- const classNameMap = slots.reduce((acc, [slot, cvaFn]) => {
- if (config.className) acc[slot] = cvaFn.config.className
- return acc
- }, {})
-
- function svaFn(props) {
- const result = slots.map(([slot, cvaFn]) => [slot, cx(cvaFn(props), classNameMap[slot])])
- return Object.fromEntries(result)
- }
-
- function raw(props) {
- const result = slots.map(([slot, cvaFn]) => [slot, cvaFn.raw(props)])
- return Object.fromEntries(result)
- }
-
- const variants = config.variants ?? {};
- const variantKeys = Object.keys(variants);
-
- function splitVariantProps(props) {
- return splitProps(props, variantKeys);
- }
- const getVariantProps = (variants) => ({ ...defaultVariants, ...compact(variants) })
-
- const variantMap = Object.fromEntries(
- Object.entries(variants).map(([key, value]) => [key, Object.keys(value)])
- );
-
- return Object.assign(memo(svaFn), {
- __cva__: false,
- raw,
- config,
- variantMap,
- variantKeys,
- classNameMap,
- splitVariantProps,
- getVariantProps,
- })
-}
\ No newline at end of file
diff --git a/styled-system/helpers.mjs b/styled-system/helpers.mjs
deleted file mode 100644
index b091c39..0000000
--- a/styled-system/helpers.mjs
+++ /dev/null
@@ -1,328 +0,0 @@
-// src/assert.ts
-function isObject(value) {
- return typeof value === "object" && value != null && !Array.isArray(value);
-}
-var isObjectOrArray = (obj) => typeof obj === "object" && obj !== null;
-
-// src/compact.ts
-function compact(value) {
- return Object.fromEntries(Object.entries(value ?? {}).filter(([_, value2]) => value2 !== void 0));
-}
-
-// src/condition.ts
-var isBaseCondition = (v) => v === "base";
-function filterBaseConditions(c) {
- return c.slice().filter((v) => !isBaseCondition(v));
-}
-
-// src/hash.ts
-function toChar(code) {
- return String.fromCharCode(code + (code > 25 ? 39 : 97));
-}
-function toName(code) {
- let name = "";
- let x;
- for (x = Math.abs(code); x > 52; x = x / 52 | 0) name = toChar(x % 52) + name;
- return toChar(x % 52) + name;
-}
-function toPhash(h, x) {
- let i = x.length;
- while (i) h = h * 33 ^ x.charCodeAt(--i);
- return h;
-}
-function toHash(value) {
- return toName(toPhash(5381, value) >>> 0);
-}
-
-// src/important.ts
-var importantRegex = /\s*!(important)?/i;
-function isImportant(value) {
- return typeof value === "string" ? importantRegex.test(value) : false;
-}
-function withoutImportant(value) {
- return typeof value === "string" ? value.replace(importantRegex, "").trim() : value;
-}
-function withoutSpace(str) {
- return typeof str === "string" ? str.replaceAll(" ", "_") : str;
-}
-
-// src/memo.ts
-var memo = (fn) => {
- const cache = /* @__PURE__ */ new Map();
- const get = (...args) => {
- const key = JSON.stringify(args);
- if (cache.has(key)) {
- return cache.get(key);
- }
- const result = fn(...args);
- cache.set(key, result);
- return result;
- };
- return get;
-};
-
-// src/merge-props.ts
-var MERGE_OMIT = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
-function mergeProps(...sources) {
- return sources.reduce((prev, obj) => {
- if (!obj) return prev;
- Object.keys(obj).forEach((key) => {
- if (MERGE_OMIT.has(key)) return;
- const prevValue = prev[key];
- const value = obj[key];
- if (isObject(prevValue) && isObject(value)) {
- prev[key] = mergeProps(prevValue, value);
- } else {
- prev[key] = value;
- }
- });
- return prev;
- }, {});
-}
-
-// src/walk-object.ts
-var isNotNullish = (element) => element != null;
-function walkObject(target, predicate, options = {}) {
- const { stop, getKey } = options;
- function inner(value, path = []) {
- if (isObjectOrArray(value)) {
- const result = {};
- for (const [prop, child] of Object.entries(value)) {
- const key = getKey?.(prop, child) ?? prop;
- const childPath = [...path, key];
- if (stop?.(value, childPath)) {
- return predicate(value, path);
- }
- const next = inner(child, childPath);
- if (isNotNullish(next)) {
- result[key] = next;
- }
- }
- return result;
- }
- return predicate(value, path);
- }
- return inner(target);
-}
-function mapObject(obj, fn) {
- if (Array.isArray(obj)) return obj.map((value) => fn(value));
- if (!isObject(obj)) return fn(obj);
- return walkObject(obj, (value) => fn(value));
-}
-
-// src/normalize-style-object.ts
-function toResponsiveObject(values, breakpoints) {
- return values.reduce(
- (acc, current, index) => {
- const key = breakpoints[index];
- if (current != null) {
- acc[key] = current;
- }
- return acc;
- },
- {}
- );
-}
-function normalizeStyleObject(styles, context, shorthand = true) {
- const { utility, conditions } = context;
- const { hasShorthand, resolveShorthand } = utility;
- return walkObject(
- styles,
- (value) => {
- return Array.isArray(value) ? toResponsiveObject(value, conditions.breakpoints.keys) : value;
- },
- {
- stop: (value) => Array.isArray(value),
- getKey: shorthand ? (prop) => hasShorthand ? resolveShorthand(prop) : prop : void 0
- }
- );
-}
-
-// src/classname.ts
-var fallbackCondition = {
- shift: (v) => v,
- finalize: (v) => v,
- breakpoints: { keys: [] }
-};
-var sanitize = (value) => typeof value === "string" ? value.replaceAll(/[\n\s]+/g, " ") : value;
-function createCss(context) {
- const { utility, hash, conditions: conds = fallbackCondition } = context;
- const formatClassName = (str) => [utility.prefix, str].filter(Boolean).join("-");
- const hashFn = (conditions, className) => {
- let result;
- if (hash) {
- const baseArray = [...conds.finalize(conditions), className];
- result = formatClassName(utility.toHash(baseArray, toHash));
- } else {
- const baseArray = [...conds.finalize(conditions), formatClassName(className)];
- result = baseArray.join(":");
- }
- return result;
- };
- return memo(({ base, ...styles } = {}) => {
- const styleObject = Object.assign(styles, base);
- const normalizedObject = normalizeStyleObject(styleObject, context);
- const classNames = /* @__PURE__ */ new Set();
- walkObject(normalizedObject, (value, paths) => {
- if (value == null) return;
- const important = isImportant(value);
- const [prop, ...allConditions] = conds.shift(paths);
- const conditions = filterBaseConditions(allConditions);
- const transformed = utility.transform(prop, withoutImportant(sanitize(value)));
- let className = hashFn(conditions, transformed.className);
- if (important) className = `${className}!`;
- classNames.add(className);
- });
- return Array.from(classNames).join(" ");
- });
-}
-function compactStyles(...styles) {
- return styles.flat().filter((style) => isObject(style) && Object.keys(compact(style)).length > 0);
-}
-function createMergeCss(context) {
- function resolve(styles) {
- const allStyles = compactStyles(...styles);
- if (allStyles.length === 1) return allStyles;
- return allStyles.map((style) => normalizeStyleObject(style, context));
- }
- function mergeCss(...styles) {
- return mergeProps(...resolve(styles));
- }
- function assignCss(...styles) {
- return Object.assign({}, ...resolve(styles));
- }
- return { mergeCss: memo(mergeCss), assignCss };
-}
-
-// src/hypenate-property.ts
-var wordRegex = /([A-Z])/g;
-var msRegex = /^ms-/;
-var hypenateProperty = memo((property) => {
- if (property.startsWith("--")) return property;
- return property.replace(wordRegex, "-$1").replace(msRegex, "-ms-").toLowerCase();
-});
-
-// src/is-css-function.ts
-var fns = ["min", "max", "clamp", "calc"];
-var fnRegExp = new RegExp(`^(${fns.join("|")})\\(.*\\)`);
-var isCssFunction = (v) => typeof v === "string" && fnRegExp.test(v);
-
-// src/is-css-unit.ts
-var lengthUnits = "cm,mm,Q,in,pc,pt,px,em,ex,ch,rem,lh,rlh,vw,vh,vmin,vmax,vb,vi,svw,svh,lvw,lvh,dvw,dvh,cqw,cqh,cqi,cqb,cqmin,cqmax,%";
-var lengthUnitsPattern = `(?:${lengthUnits.split(",").join("|")})`;
-var lengthRegExp = new RegExp(`^[+-]?[0-9]*.?[0-9]+(?:[eE][+-]?[0-9]+)?${lengthUnitsPattern}$`);
-var isCssUnit = (v) => typeof v === "string" && lengthRegExp.test(v);
-
-// src/is-css-var.ts
-var isCssVar = (v) => typeof v === "string" && /^var\(--.+\)$/.test(v);
-
-// src/pattern-fns.ts
-var patternFns = {
- map: mapObject,
- isCssFunction,
- isCssVar,
- isCssUnit
-};
-var getPatternStyles = (pattern, styles) => {
- if (!pattern?.defaultValues) return styles;
- const defaults = typeof pattern.defaultValues === "function" ? pattern.defaultValues(styles) : pattern.defaultValues;
- return Object.assign({}, defaults, compact(styles));
-};
-
-// src/slot.ts
-var getSlotRecipes = (recipe = {}) => {
- const init = (slot) => ({
- className: [recipe.className, slot].filter(Boolean).join("__"),
- base: recipe.base?.[slot] ?? {},
- variants: {},
- defaultVariants: recipe.defaultVariants ?? {},
- compoundVariants: recipe.compoundVariants ? getSlotCompoundVariant(recipe.compoundVariants, slot) : []
- });
- const slots = recipe.slots ?? [];
- const recipeParts = slots.map((slot) => [slot, init(slot)]);
- for (const [variantsKey, variantsSpec] of Object.entries(recipe.variants ?? {})) {
- for (const [variantKey, variantSpec] of Object.entries(variantsSpec)) {
- recipeParts.forEach(([slot, slotRecipe]) => {
- slotRecipe.variants[variantsKey] ??= {};
- slotRecipe.variants[variantsKey][variantKey] = variantSpec[slot] ?? {};
- });
- }
- }
- return Object.fromEntries(recipeParts);
-};
-var getSlotCompoundVariant = (compoundVariants, slotName) => compoundVariants.filter((compoundVariant) => compoundVariant.css[slotName]).map((compoundVariant) => ({ ...compoundVariant, css: compoundVariant.css[slotName] }));
-
-// src/split-props.ts
-function splitProps(props, ...keys) {
- const descriptors = Object.getOwnPropertyDescriptors(props);
- const dKeys = Object.keys(descriptors);
- const split = (k) => {
- const clone = {};
- for (let i = 0; i < k.length; i++) {
- const key = k[i];
- if (descriptors[key]) {
- Object.defineProperty(clone, key, descriptors[key]);
- delete descriptors[key];
- }
- }
- return clone;
- };
- const fn = (key) => split(Array.isArray(key) ? key : dKeys.filter(key));
- return keys.map(fn).concat(split(dKeys));
-}
-
-// src/uniq.ts
-var uniq = (...items) => {
- const set = items.reduce((acc, currItems) => {
- if (currItems) {
- currItems.forEach((item) => acc.add(item));
- }
- return acc;
- }, /* @__PURE__ */ new Set([]));
- return Array.from(set);
-};
-export {
- compact,
- createCss,
- createMergeCss,
- filterBaseConditions,
- getPatternStyles,
- getSlotCompoundVariant,
- getSlotRecipes,
- hypenateProperty,
- isBaseCondition,
- isObject,
- mapObject,
- memo,
- mergeProps,
- patternFns,
- splitProps,
- toHash,
- uniq,
- walkObject,
- withoutSpace
-};
-
-
-
-// src/normalize-html.ts
-var htmlProps = ["htmlSize", "htmlTranslate", "htmlWidth", "htmlHeight"];
-function convert(key) {
- return htmlProps.includes(key) ? key.replace("html", "").toLowerCase() : key;
-}
-function normalizeHTMLProps(props) {
- return Object.fromEntries(Object.entries(props).map(([key, value]) => [convert(key), value]));
-}
-normalizeHTMLProps.keys = htmlProps;
-export {
- normalizeHTMLProps
-};
-
-
-export function __spreadValues(a, b) {
- return { ...a, ...b }
-}
-
-export function __objRest(source, exclude) {
- return Object.fromEntries(Object.entries(source).filter(([key]) => !exclude.includes(key)))
-}
\ No newline at end of file
diff --git a/styled-system/jsx/aspect-ratio.d.ts b/styled-system/jsx/aspect-ratio.d.ts
deleted file mode 100644
index c057d1a..0000000
--- a/styled-system/jsx/aspect-ratio.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { AspectRatioProperties } from '../patterns/aspect-ratio';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface AspectRatioProps extends AspectRatioProperties, DistributiveOmit, keyof AspectRatioProperties | 'aspectRatio'> {}
-
-
-export declare const AspectRatio: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/aspect-ratio.mjs b/styled-system/jsx/aspect-ratio.mjs
deleted file mode 100644
index 7e17b28..0000000
--- a/styled-system/jsx/aspect-ratio.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getAspectRatioStyle } from '../patterns/aspect-ratio.mjs';
-import { styled } from './factory.mjs';
-
-export const AspectRatio = /* @__PURE__ */ forwardRef(function AspectRatio(props, ref) {
- const [patternProps, restProps] = splitProps(props, ["ratio"])
-
-const styleProps = getAspectRatioStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/bleed.d.ts b/styled-system/jsx/bleed.d.ts
deleted file mode 100644
index 9eca599..0000000
--- a/styled-system/jsx/bleed.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { BleedProperties } from '../patterns/bleed';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface BleedProps extends BleedProperties, DistributiveOmit, keyof BleedProperties > {}
-
-
-export declare const Bleed: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/bleed.mjs b/styled-system/jsx/bleed.mjs
deleted file mode 100644
index 3cbbbd7..0000000
--- a/styled-system/jsx/bleed.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getBleedStyle } from '../patterns/bleed.mjs';
-import { styled } from './factory.mjs';
-
-export const Bleed = /* @__PURE__ */ forwardRef(function Bleed(props, ref) {
- const [patternProps, restProps] = splitProps(props, ["inline","block"])
-
-const styleProps = getBleedStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/box.d.ts b/styled-system/jsx/box.d.ts
deleted file mode 100644
index 958962d..0000000
--- a/styled-system/jsx/box.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { BoxProperties } from '../patterns/box';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface BoxProps extends BoxProperties, DistributiveOmit, keyof BoxProperties > {}
-
-
-export declare const Box: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/box.mjs b/styled-system/jsx/box.mjs
deleted file mode 100644
index ffe9149..0000000
--- a/styled-system/jsx/box.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getBoxStyle } from '../patterns/box.mjs';
-import { styled } from './factory.mjs';
-
-export const Box = /* @__PURE__ */ forwardRef(function Box(props, ref) {
- const [patternProps, restProps] = splitProps(props, [])
-
-const styleProps = getBoxStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/center.d.ts b/styled-system/jsx/center.d.ts
deleted file mode 100644
index fd6d615..0000000
--- a/styled-system/jsx/center.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { CenterProperties } from '../patterns/center';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface CenterProps extends CenterProperties, DistributiveOmit, keyof CenterProperties > {}
-
-
-export declare const Center: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/center.mjs b/styled-system/jsx/center.mjs
deleted file mode 100644
index 7bbb16f..0000000
--- a/styled-system/jsx/center.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getCenterStyle } from '../patterns/center.mjs';
-import { styled } from './factory.mjs';
-
-export const Center = /* @__PURE__ */ forwardRef(function Center(props, ref) {
- const [patternProps, restProps] = splitProps(props, ["inline"])
-
-const styleProps = getCenterStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/circle.d.ts b/styled-system/jsx/circle.d.ts
deleted file mode 100644
index 3afa95f..0000000
--- a/styled-system/jsx/circle.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { CircleProperties } from '../patterns/circle';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface CircleProps extends CircleProperties, DistributiveOmit, keyof CircleProperties > {}
-
-
-export declare const Circle: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/circle.mjs b/styled-system/jsx/circle.mjs
deleted file mode 100644
index c4b7c0a..0000000
--- a/styled-system/jsx/circle.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getCircleStyle } from '../patterns/circle.mjs';
-import { styled } from './factory.mjs';
-
-export const Circle = /* @__PURE__ */ forwardRef(function Circle(props, ref) {
- const [patternProps, restProps] = splitProps(props, ["size"])
-
-const styleProps = getCircleStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/container.d.ts b/styled-system/jsx/container.d.ts
deleted file mode 100644
index 10b9df2..0000000
--- a/styled-system/jsx/container.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { ContainerProperties } from '../patterns/container';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface ContainerProps extends ContainerProperties, DistributiveOmit, keyof ContainerProperties > {}
-
-
-export declare const Container: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/container.mjs b/styled-system/jsx/container.mjs
deleted file mode 100644
index 5078e2f..0000000
--- a/styled-system/jsx/container.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getContainerStyle } from '../patterns/container.mjs';
-import { styled } from './factory.mjs';
-
-export const Container = /* @__PURE__ */ forwardRef(function Container(props, ref) {
- const [patternProps, restProps] = splitProps(props, [])
-
-const styleProps = getContainerStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/cq.d.ts b/styled-system/jsx/cq.d.ts
deleted file mode 100644
index 5f00f13..0000000
--- a/styled-system/jsx/cq.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable */
-import type { FunctionComponent } from 'react'
-import type { CqProperties } from '../patterns/cq';
-import type { HTMLStyledProps } from '../types/jsx';
-import type { DistributiveOmit } from '../types/system-types';
-
-export interface CqProps extends CqProperties, DistributiveOmit, keyof CqProperties > {}
-
-
-export declare const Cq: FunctionComponent
\ No newline at end of file
diff --git a/styled-system/jsx/cq.mjs b/styled-system/jsx/cq.mjs
deleted file mode 100644
index cef7e9c..0000000
--- a/styled-system/jsx/cq.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createElement, forwardRef } from 'react'
-
-import { splitProps } from '../helpers.mjs';
-import { getCqStyle } from '../patterns/cq.mjs';
-import { styled } from './factory.mjs';
-
-export const Cq = /* @__PURE__ */ forwardRef(function Cq(props, ref) {
- const [patternProps, restProps] = splitProps(props, ["name","type"])
-
-const styleProps = getCqStyle(patternProps)
-const mergedProps = { ref, ...styleProps, ...restProps }
-
-return createElement(styled.div, mergedProps)
- })
\ No newline at end of file
diff --git a/styled-system/jsx/create-style-context.d.ts b/styled-system/jsx/create-style-context.d.ts
deleted file mode 100644
index 514de6e..0000000
--- a/styled-system/jsx/create-style-context.d.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/* eslint-disable */
-import type { SlotRecipeRuntimeFn, RecipeVariantProps } from '../types/recipe';
-import type { JsxHTMLProps, JsxStyleProps, Assign } from '../types/system-types';
-import type { JsxFactoryOptions, ComponentProps } from '../types/jsx';
-import type { ComponentType, ElementType } from 'react'
-
-interface UnstyledProps {
- unstyled?: boolean | undefined
-}
-
-type SvaFn = SlotRecipeRuntimeFn
-interface SlotRecipeFn {
- __type: any
- __slot: string
- (props?: any): any
-}
-type SlotRecipe = SvaFn | SlotRecipeFn
-
-type InferSlot = R extends SlotRecipeFn ? R['__slot'] : R extends SvaFn ? S : never
-
-interface WithProviderOptions {
- defaultProps?: Partial
| undefined
-}
-
-type StyleContextProvider = ComponentType<
- JsxHTMLProps & UnstyledProps, Assign, JsxStyleProps>>
->
-
-type StyleContextRootProvider = ComponentType<
- ComponentProps & UnstyledProps & RecipeVariantProps
->
-
-type StyleContextConsumer = ComponentType<
- JsxHTMLProps & UnstyledProps, JsxStyleProps>
->
-
-export interface StyleContext {
- withRootProvider: (
- Component: T,
- options?: WithProviderOptions> | undefined
- ) => StyleContextRootProvider
- withProvider: (
- Component: T,
- slot: InferSlot,
- options?: JsxFactoryOptions> | undefined
- ) => StyleContextProvider
- withContext: (
- Component: T,
- slot: InferSlot,
- options?: JsxFactoryOptions