Skip to content

Commit 2e1fbec

Browse files
committed
[FEAT]: 로컬 개인 기록 조회 및 통계 기능 구현
1 parent 1140fc4 commit 2e1fbec

File tree

45 files changed

+1316
-26
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1316
-26
lines changed

client/.pnp.cjs

+421
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

client/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
"dependencies": {
77
"@reduxjs/toolkit": "^1.8.6",
88
"lodash": "^4.17.21",
9+
"lucide-react": "^0.474.0",
910
"react": "18.2.0",
1011
"react-cookie": "^7.1.4",
1112
"react-dom": "18.2.0",
1213
"react-draggable": "4.4.5",
1314
"react-intersection-observer": "9.4.0",
1415
"react-redux": "8.0.2",
1516
"react-scripts": "^5.0.1",
17+
"recharts": "^2.15.1",
1618
"redux": "4.2.0",
1719
"tailwindcss": "^3.4.14"
1820
},
File renamed without changes.

client/src/App.css

+199
Original file line numberDiff line numberDiff line change
@@ -786,3 +786,202 @@ ul {
786786
display: inline-block;
787787
margin: 0 5px 0 0;
788788
}
789+
/* App.css 수정 */
790+
791+
.stats-container {
792+
transition: all 0.3s;
793+
padding: 0.4rem 1.4rem;
794+
height: 96%;
795+
display: flex;
796+
flex-direction: column;
797+
gap: 1.5rem;
798+
}
799+
800+
.stats-summary {
801+
transition: all 0.3s;
802+
display: grid;
803+
grid-template-columns: repeat(3, 1fr);
804+
gap: 1.25rem;
805+
}
806+
807+
.stats-card {
808+
transition: all 0.3s;
809+
background-color: var(--sidebar-background-color);
810+
padding: 1rem 1.25rem;
811+
border-radius: 12px;
812+
display: flex;
813+
align-items: flex-start;
814+
justify-content: space-between;
815+
gap: 1rem;
816+
}
817+
818+
.card-icon-wrapper {
819+
display: flex;
820+
align-items: center;
821+
justify-content: center;
822+
color: var(--section-header-color);
823+
}
824+
825+
.card-info {
826+
display: flex;
827+
flex-direction: column-reverse;
828+
height: 42px;
829+
gap: 0.25rem;
830+
}
831+
832+
.card-label {
833+
font-size: 01rem;
834+
margin-left: 8px;
835+
color: var(--section-header-color);
836+
}
837+
838+
.card-value {
839+
font-size: 1.5rem;
840+
color: var(--section-header-color);
841+
font-weight: 500;
842+
line-height: 1;
843+
}
844+
845+
.card-unit {
846+
font-size: 0.75rem;
847+
opacity: 0.6;
848+
margin-left: 0.25rem;
849+
}
850+
851+
.trend-header {
852+
transition: all 0.3s;
853+
display: flex;
854+
justify-content: space-between;
855+
align-items: flex-start;
856+
margin-bottom: 0.75rem;
857+
}
858+
859+
.chart-scroll-container {
860+
overflow-x: auto;
861+
margin: 0 -1.25rem;
862+
padding: 0 1.25rem;
863+
scrollbar-width: none;
864+
}
865+
866+
.records-section {
867+
flex: 1;
868+
display: flex;
869+
flex-direction: column;
870+
min-height: 0; /* 중요: 스크롤을 위해 필요 */
871+
}
872+
873+
.records-header {
874+
padding: 0.75rem 0;
875+
margin-bottom: 0.5rem;
876+
}
877+
878+
.records-table-container {
879+
flex: 1;
880+
overflow-y: auto;
881+
border-radius: 8px;
882+
}
883+
.records-table-container::-webkit-scrollbar {
884+
width: 7px;
885+
}
886+
887+
.records-table-container::-webkit-scrollbar-thumb {
888+
background-color: hsla(0, 0%, 42%, 0.169);
889+
border-radius: 100px;
890+
}
891+
.data-table {
892+
width: 100%;
893+
border-collapse: collapse;
894+
}
895+
896+
.data-table th {
897+
position: sticky;
898+
top: 0;
899+
background-color: var(--sidebar-background-color);
900+
padding: 1rem 1.25rem;
901+
font-weight: normal;
902+
color: var(--section-header-color);
903+
text-align: left;
904+
font-size: 0.875rem;
905+
border-bottom: 1px solid var(--form-background-color);
906+
}
907+
908+
.data-table td {
909+
padding: 0.875rem 1.25rem;
910+
color: var(--section-header-color);
911+
border-bottom: 1px solid var(--form-background-color);
912+
font-size: 0.875rem;
913+
text-align: left;
914+
}
915+
916+
.data-table tr:hover td {
917+
background-color: var(--editor-background-color);
918+
}
919+
920+
.emphasis {
921+
color: var(--cursor-color);
922+
font-weight: 500;
923+
text-align: left;
924+
}
925+
926+
.speed-normal {
927+
color: var(--section-header-color);
928+
}
929+
930+
@keyframes gradient {
931+
0% {
932+
background-position: 0% 50%;
933+
}
934+
50% {
935+
background-position: 100% 50%;
936+
}
937+
100% {
938+
background-position: 0% 50%;
939+
}
940+
}
941+
942+
.stats-card {
943+
transition: all 0.2s cubic-bezier(0.42, 0, 0.58, 1);
944+
position: relative;
945+
overflow: hidden;
946+
}
947+
948+
.stats-card:hover {
949+
transform: translateY(-2px) scale(1.01);
950+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
951+
}
952+
953+
/* Epic 카드 스타일 */
954+
.speed-card-epic {
955+
background: linear-gradient(-45deg, #ff6b6b, #ffd93d, #ff6b6b);
956+
background-size: 300% 300%;
957+
animation: gradient 20s ease infinite;
958+
transition: transform 0.2s ease-out;
959+
}
960+
961+
/* Legendary 카드 스타일 */
962+
.speed-card-legendary {
963+
background: linear-gradient(135deg, #ff0099, #00ffcc);
964+
background-size: 300% 300%;
965+
animation: gradient 20s ease infinite;
966+
transition: transform 0.2s ease-out;
967+
}
968+
969+
@keyframes gradient {
970+
0% {
971+
background-position: 0% 50%;
972+
}
973+
50% {
974+
background-position: 100% 50%;
975+
}
976+
100% {
977+
background-position: 0% 50%;
978+
}
979+
}
980+
.white-text {
981+
color: white !important;
982+
}
983+
984+
.white-text .card-unit {
985+
color: rgba(255, 255, 255, 0.8) !important;
986+
opacity: 1;
987+
}

client/src/components/Editor.jsx

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Text from "./Text";
22
import { useEffect, useState } from "react";
33
import getFilecontents from "../utils/filecontents";
4+
import TypingStats from "./TypingStats";
45

56
export default function Editor({
67
file,
@@ -23,11 +24,10 @@ export default function Editor({
2324
// 이벤트 모드 체크를 위한 useEffect 추가
2425
useEffect(() => {
2526
const queryParams = new URLSearchParams(window.location.search);
26-
const eventParam = queryParams.get('event');
27-
setIsEventMode(eventParam?.toLowerCase() === 'bisc');
27+
const eventParam = queryParams.get("event");
28+
setIsEventMode(eventParam?.toLowerCase() === "bisc");
2829
}, []);
2930

30-
3131
useEffect(() => {
3232
setTextSplit(getFilecontents(file).content);
3333
if (fileForCompare !== file) {
@@ -54,7 +54,7 @@ export default function Editor({
5454
}, [userInput]);
5555

5656
const userInputTabHandler = (event) => {
57-
if (isEventMode && (event.key === 'Backspace' || event.key === 'Delete')) {
57+
if (isEventMode && (event.key === "Backspace" || event.key === "Delete")) {
5858
event.preventDefault();
5959
return;
6060
}
@@ -144,7 +144,7 @@ export default function Editor({
144144
//textbox로 focus이동
145145
document.querySelector(".textbox").focus();
146146
};
147-
return file !== "Ranking" ? (
147+
return file !== "Statistics" ? (
148148
<div className="editor" onClick={focus}>
149149
<div className="numbering">
150150
{Array.from({ length: 26 }, (_, i) => (
@@ -174,12 +174,8 @@ export default function Editor({
174174
/>
175175
</div>
176176
) : (
177-
<div className="editor" style={{ fontSize: "60px", color: "gray" }}>
178-
<br />
179-
<br />
180-
<br />
181-
<br />
182-
Press Explorer to play.
177+
<div className="editor">
178+
<TypingStats />
183179
</div>
184180
);
185181
}

client/src/components/PopupInnerInput.jsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useRef, useState } from "react";
22
import fetcher from "../utils/fetcher";
33
import { connect } from "react-redux";
4+
import { saveTypingRecord } from "../utils/typingHistory";
45

56
function PopupInnerInput({
67
file,
@@ -21,7 +22,12 @@ function PopupInnerInput({
2122
const sendResult = () => {
2223
if (loading) return;
2324
setLoading(true);
24-
25+
saveTypingRecord({
26+
file,
27+
cpm: finishTrigger,
28+
correctChars: Correctchr,
29+
wrongChars: Wrongchr,
30+
});
2531
fetcher.save(
2632
file,
2733
finishTrigger,

client/src/components/Sidebar.jsx

+13-12
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ export default function Sidebar(props) {
1616
setIsEventMode(event?.toLowerCase() === "bisc");
1717
}, []);
1818

19-
const filename = isEventMode
20-
? ["ex.py", "print.c", "sha256.java"]
21-
: [
22-
"hello.py",
23-
"test.java",
24-
"server.js",
25-
"RectangleArea.java",
26-
"say_hello.py",
27-
"Example.java",
28-
"Fibonacci.java",
29-
];
19+
const filename = [
20+
"hello.py",
21+
"ex.py",
22+
"print.c",
23+
"sha256.java",
24+
"test.java",
25+
"server.js",
26+
"RectangleArea.java",
27+
"say_hello.py",
28+
"Example.java",
29+
"Fibonacci.java",
30+
];
3031

3132
useEffect(() => {
3233
if (props.section === "1" && isEventMode) {
@@ -40,7 +41,7 @@ export default function Sidebar(props) {
4041
setFilestate(filestate);
4142
props.setFile(filestate);
4243
} else if (props.section === "2") {
43-
props.setFile("Ranking");
44+
props.setFile("Statistics");
4445
}
4546
}, [props.section]);
4647
if (props.section === "1") {

0 commit comments

Comments
 (0)