Skip to content

Commit ac1551c

Browse files
authored
Merge pull request #84 from DMU-DebugVisual/dongjun
mockData 동적 로딩 및 토큰 만료 시 자동 로그아웃 기능 추가
2 parents a290703 + 07386c5 commit ac1551c

File tree

8 files changed

+138
-95
lines changed

8 files changed

+138
-95
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@testing-library/user-event": "^13.5.0",
1212
"axios": "^0.27.2",
1313
"d3": "^7.9.0",
14+
"jwt-decode": "^4.0.0",
1415
"react": "^19.1.0",
1516
"react-dom": "^19.1.0",
1617
"react-icons": "^5.5.0",
@@ -20,6 +21,8 @@
2021
"web-vitals": "^2.1.4"
2122
},
2223
"scripts": {
24+
"prestart": "node scripts/generate-examples.js",
25+
"prebuild": "node scripts/generate-examples.js",
2326
"start": "react-scripts start",
2427
"build": "react-scripts build",
2528
"test": "react-scripts test",

scripts/generate-examples.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// scripts/generate-examples.js
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
const mockDataPath = path.resolve(__dirname, '../src/components/ide/mockData');
7+
const outputPath = path.resolve(__dirname, '../src/generated-examples.js');
8+
9+
const files = fs.readdirSync(mockDataPath);
10+
11+
const jsonExamples = [];
12+
13+
files.forEach(file => {
14+
if (path.extname(file) === '.json') {
15+
const filePath = path.join(mockDataPath, file);
16+
const content = fs.readFileSync(filePath, 'utf-8');
17+
jsonExamples.push({
18+
name: file,
19+
type: 'json',
20+
code: content,
21+
});
22+
}
23+
});
24+
25+
const outputContent = `// 이 파일은 스크립트에 의해 자동으로 생성되었습니다. 직접 수정하지 마세요.\nexport const jsonExamples = ${JSON.stringify(jsonExamples, null, 2)};`;
26+
27+
fs.writeFileSync(outputPath, outputContent);
28+
29+
console.log('✅ JSON 예제 파일이 성공적으로 생성되었습니다.');

src/App.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { HashRouter, Routes, Route, useLocation } from "react-router-dom";
22
import { useEffect, useState } from "react";
3+
// ✅ 1. jwt-decode 라이브러리를 import 합니다.
4+
import { jwtDecode } from "jwt-decode";
35

46
import Header from "./components/header/Header";
57
import Footer from "./components/footer/Footer";
@@ -19,7 +21,7 @@ import MyCommunity from "./components/mypage/MyCommunity";
1921
import ScrollToTop from "./components/common/ScrollToTop";
2022
import CommunityWrite from "./components/community/CommunityWrite";
2123
import VisualizationModal from "./components/ide/VisualizationModal";
22-
import PostDetail from "./components/community/PostDetail"; // ✅ 게시글 상세 컴포넌트
24+
import PostDetail from "./components/community/PostDetail";
2325
import CodecastLive from "./components/codecast/codecastlive/CodecastLive";
2426

2527
function AppContent() {
@@ -30,19 +32,43 @@ function AppContent() {
3032
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
3133

3234
const isSignupPage = location.pathname === "/signup";
33-
const isIdePage = location.pathname.startsWith("/ide"); // ✅ IDE 페이지 판별
35+
const isIdePage = location.pathname.startsWith("/ide");
3436

37+
// ✅ 2. 토큰 만료를 확인하는 로직으로 교체된 useEffect
3538
useEffect(() => {
3639
const token = localStorage.getItem('token');
3740
const storedUsername = localStorage.getItem('username');
41+
3842
if (token && storedUsername) {
39-
setIsLoggedIn(true);
40-
setNickname(storedUsername);
43+
try {
44+
const decodedToken = jwtDecode(token);
45+
// 토큰의 만료 시간(exp)이 현재 시간보다 이전이면 만료된 것
46+
if (decodedToken.exp * 1000 < Date.now()) {
47+
// 토큰이 만료된 경우, 로그아웃 처리
48+
localStorage.removeItem('token');
49+
localStorage.removeItem('username');
50+
setIsLoggedIn(false);
51+
setNickname('');
52+
console.log('만료된 토큰이 감지되어 자동 로그아웃되었습니다.');
53+
} else {
54+
// 토큰이 유효한 경우, 로그인 상태로 설정
55+
setIsLoggedIn(true);
56+
setNickname(storedUsername);
57+
}
58+
} catch (error) {
59+
// 토큰 형식이 잘못된 경우에도 로그아웃 처리
60+
localStorage.removeItem('token');
61+
localStorage.removeItem('username');
62+
setIsLoggedIn(false);
63+
setNickname('');
64+
console.error('잘못된 토큰 형식으로 인해 로그아웃 처리:', error);
65+
}
4166
} else {
67+
// 토큰이 없는 경우, 기본적으로 로그아웃 상태
4268
setIsLoggedIn(false);
4369
setNickname('');
4470
}
45-
}, []);
71+
}, []); // 앱이 처음 로드될 때 한 번만 실행됩니다.
4672

4773
useEffect(() => {
4874
const savedTheme = localStorage.getItem("theme");
@@ -76,7 +102,7 @@ function AppContent() {
76102
<Route path="/ide/:language/:filename" element={<IDE />} />
77103
<Route path="/community" element={<Community />} />
78104
<Route path="/community/write" element={<CommunityWrite />} />
79-
<Route path="/community/post/:id" element={<PostDetail />} /> {/* ✅ 상세 페이지 */}
105+
<Route path="/community/post/:id" element={<PostDetail />} />
80106
<Route path="/broadcast" element={<Codecast />} />
81107
<Route path="/startbroadcast" element={<StartCodecast />} />
82108
<Route path="/broadcast/live" element={<CodecastLive isDark={isDark} />} />
@@ -89,7 +115,6 @@ function AppContent() {
89115
</Route>
90116
</Routes>
91117

92-
{/* ✅ 푸터는 회원가입/IDE 페이지에서는 숨김 */}
93118
{(!isSignupPage && !isIdePage) && <Footer />}
94119

95120
{isLoginModalOpen && (
@@ -111,4 +136,4 @@ export default function App() {
111136
<AppContent />
112137
</HashRouter>
113138
);
114-
}
139+
}

src/api/globalFetch.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// src/api/globalFetch.js
2+
3+
// 1. 기존의 window.fetch 함수를 백업해둡니다.
4+
const originalFetch = window.fetch;
5+
6+
// 2. window.fetch 함수를 우리의 감시 기능이 추가된 새 함수로 덮어씁니다.
7+
window.fetch = async (...args) => {
8+
// 3. 백업해둔 원래 fetch 함수를 호출하여 실제 API 요청을 보냅니다.
9+
const response = await originalFetch(...args);
10+
11+
// 4. 응답을 받은 후, 만약 401 에러(토큰 만료)가 발생했다면
12+
if (response.status === 401) {
13+
// 이전에 localStorage에 저장된 토큰이 있을 때만 로그아웃 처리
14+
if (localStorage.getItem('token')) {
15+
localStorage.removeItem('token');
16+
localStorage.removeItem('username');
17+
18+
alert('세션이 만료되었습니다. 다시 로그인해 주세요.');
19+
// 현재 페이지를 새로고침하여 로그인 상태를 갱신합니다.
20+
// 로그인 페이지로 강제 이동시키는 것보다 사용자 경험이 더 나을 수 있습니다.
21+
window.location.reload();
22+
}
23+
}
24+
25+
// 5. 원래 API를 호출했던 곳에 응답을 그대로 돌려줍니다.
26+
return response;
27+
};

src/components/ide/IDE.jsx

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Editor from '@monaco-editor/react';
66
import VisualizationModal from './VisualizationModal';
77
import './IDE.css';
88
import config from '../../config';
9+
import { jsonExamples } from '../../generated-examples.js';
910

1011
// 🎨 Feather Icons CDN 로드
1112
if (!document.querySelector('script[src*="feather"]')) {
@@ -326,93 +327,7 @@ int main() {
326327
}`
327328
},
328329
// JSON 예제 파일들
329-
{
330-
name: "bubble_sort.json",
331-
type: "json",
332-
code: `{
333-
"algorithm": "bubble_sort",
334-
"steps": [
335-
{
336-
"step": 1,
337-
"description": "배열 초기화",
338-
"array": [64, 34, 25, 12, 22, 11, 90],
339-
"comparison": null,
340-
"swap": null
341-
},
342-
{
343-
"step": 2,
344-
"description": "첫 번째 패스 시작",
345-
"array": [64, 34, 25, 12, 22, 11, 90],
346-
"comparison": [0, 1],
347-
"swap": [0, 1]
348-
},
349-
{
350-
"step": 3,
351-
"description": "64와 34 비교 후 교환",
352-
"array": [34, 64, 25, 12, 22, 11, 90],
353-
"comparison": [1, 2],
354-
"swap": [1, 2]
355-
},
356-
{
357-
"step": 4,
358-
"description": "64와 25 비교 후 교환",
359-
"array": [34, 25, 64, 12, 22, 11, 90],
360-
"comparison": [2, 3],
361-
"swap": [2, 3]
362-
},
363-
{
364-
"step": 5,
365-
"description": "최종 정렬된 배열",
366-
"array": [11, 12, 22, 25, 34, 64, 90],
367-
"comparison": null,
368-
"swap": null
369-
}
370-
],
371-
"complexity": {
372-
"time": "O(n²)",
373-
"space": "O(1)"
374-
}
375-
}`
376-
},
377-
{
378-
name: "binary_tree.json",
379-
type: "json",
380-
code: `{
381-
"tree": {
382-
"root": {
383-
"value": 1,
384-
"left": {
385-
"value": 2,
386-
"left": {
387-
"value": 4,
388-
"left": null,
389-
"right": null
390-
},
391-
"right": {
392-
"value": 5,
393-
"left": null,
394-
"right": null
395-
}
396-
},
397-
"right": {
398-
"value": 3,
399-
"left": null,
400-
"right": null
401-
}
402-
}
403-
},
404-
"traversals": {
405-
"inorder": [4, 2, 5, 1, 3],
406-
"preorder": [1, 2, 4, 5, 3],
407-
"postorder": [4, 5, 2, 3, 1]
408-
},
409-
"properties": {
410-
"height": 3,
411-
"nodes": 5,
412-
"leaves": 3
413-
}
414-
}`
415-
}
330+
...jsonExamples
416331
]);
417332

418333
// 🆕 사이드바 섹션 상태 관리

src/generated-examples.js

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
33
import './index.css';
44
import App from './App';
55
import reportWebVitals from './reportWebVitals';
6+
import './api/globalFetch.js';
67

78
const root = ReactDOM.createRoot(document.getElementById('root'));
89
root.render(

0 commit comments

Comments
 (0)