diff --git a/public/default_profile.png b/public/default_profile.png new file mode 100644 index 0000000..c15ae5f Binary files /dev/null and b/public/default_profile.png differ diff --git a/src/App.tsx b/src/App.tsx index 75b1419..669ff0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,8 @@ import MobileRegisterPage from "./pages/mobile/MobileRegisterPage"; import ProtectedRoute from "./components/ProtectedRoute"; import MobileProtectedRoute from "./components/MobileProtectedRoute"; import HomePage from "./pages/HomePage"; +import TokenInfoPage from "./pages/TokenInfoPage"; +import MyPage from "./pages/MyPage"; const App = () => { return ( @@ -39,6 +41,8 @@ const App = () => { } /> } /> } /> + } /> + } /> diff --git a/src/components/home/HomeLayout.tsx b/src/components/home/HomeLayout.tsx index a0675ab..496df7d 100644 --- a/src/components/home/HomeLayout.tsx +++ b/src/components/home/HomeLayout.tsx @@ -3,11 +3,13 @@ import Sidebar from "./Sidebar"; interface LayoutProps { children: ReactNode; + username: string; + email: string; } -const Layout: React.FC = ({ children }) => ( +const Layout: React.FC = ({ children , username , email} ) => (
- +
{children}
); diff --git a/src/components/home/Sidebar.tsx b/src/components/home/Sidebar.tsx index b1d0fff..736c11f 100644 --- a/src/components/home/Sidebar.tsx +++ b/src/components/home/Sidebar.tsx @@ -1,41 +1,54 @@ import React from "react"; +import { useLocation } from "react-router-dom"; import SidebarLink from "./SidebarLink"; interface SidebarProps { name: string; email: string; + profileUrl?: string; } -const Sidebar: React.FC = ({ name, email }) => ( - + ); +}; export default Sidebar; diff --git a/src/components/mypage/PasswordChangeDialog.tsx b/src/components/mypage/PasswordChangeDialog.tsx new file mode 100644 index 0000000..82b3f25 --- /dev/null +++ b/src/components/mypage/PasswordChangeDialog.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { Lock } from 'lucide-react'; + +const PasswordChangeDialog: React.FC = () => { + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isOpen, setIsOpen] = useState(false); + + const handlePasswordChange = () => { + // Validation and password change logic + console.log('Password change initiated'); + setIsOpen(false); // Close modal after submission + }; + + return ( +
+ {/* Trigger button */} + + + {/* Modal */} + {isOpen && ( +
+
+

비밀번호 변경

+
+ setCurrentPassword(e.target.value)} + className="w-full border rounded-lg p-2" + /> + setNewPassword(e.target.value)} + className="w-full border rounded-lg p-2" + /> + setConfirmPassword(e.target.value)} + className="w-full border rounded-lg p-2" + /> + + +
+
+
+ )} +
+ ); +}; + +export default PasswordChangeDialog; diff --git a/src/components/mypage/PinChangeDialog.tsx b/src/components/mypage/PinChangeDialog.tsx new file mode 100644 index 0000000..6fe6149 --- /dev/null +++ b/src/components/mypage/PinChangeDialog.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { Shield } from 'lucide-react'; + +const PinChangeDialog: React.FC = () => { + const [pin, setPin] = useState(''); + const [isOpen, setIsOpen] = useState(false); + + const handlePinChange = () => { + // PIN change logic + console.log('PIN change initiated'); + setIsOpen(false); // Close modal after submission + }; + + return ( +
+ {/* Trigger button */} + + + {/* Modal */} + {isOpen && ( +
+
+

PIN 변경

+
+ setPin(e.target.value)} + className="w-full border rounded-lg p-2" + /> + + +
+
+
+ )} +
+ ); +}; + +export default PinChangeDialog; diff --git a/src/components/mypage/ProfileImageUpload.tsx b/src/components/mypage/ProfileImageUpload.tsx new file mode 100644 index 0000000..f7611d5 --- /dev/null +++ b/src/components/mypage/ProfileImageUpload.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import { Upload } from 'lucide-react'; + +interface ProfileImageUploadProps { + profileImage: string; + userName: string; +} +const ProfileImageUpload: React.FC = ({ profileImage, userName }) => { + const [selectedFile, setSelectedFile] = useState(null); + + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files) { + setSelectedFile(event.target.files[0]); + } + }; + + return ( +
+ {/* Avatar 부분을 간단한 img 태그로 대체 */} +
+ Profile +
+
+ + +
+
+ ); +}; + +export default ProfileImageUpload; diff --git a/src/components/token/TokenHistoryItem.tsx b/src/components/token/TokenHistoryItem.tsx new file mode 100644 index 0000000..e9513ea --- /dev/null +++ b/src/components/token/TokenHistoryItem.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { Shield, Trash2 } from 'lucide-react'; + +interface TokenHistoryItemProps { + token: { + id: string; + service: string; + date: string; + device: string; + ip: string; + location: string; + }; + onDelete: (id: string) => void; + onBlockIP: (ip: string) => void; +} + +const TokenHistoryItem: React.FC = ({ token, onDelete, onBlockIP }) => { + const [showModal, setShowModal] = useState(false); + const [actionType, setActionType] = useState<'delete' | 'block'>('delete'); + + const handleAction = () => { + if (actionType === 'delete') { + onDelete(token.id); + } else if (actionType === 'block') { + onBlockIP(token.ip); + } + setShowModal(false); + }; + + return ( +
+
+
+
+ {token.service} + + {token.device} + +
+
+ +
+
+

날짜

+

{token.date}

+
+
+

IP 주소

+

{token.ip}

+
+
+

위치

+

{token.location}

+
+
+ + {/* 모바일 최적화된 액션 버튼 */} +
+ + +
+
+ + {/* 경고 모달 */} + {showModal && ( +
+
+

+ {actionType === 'delete' ? '정말로 이 토큰을 삭제하시겠습니까?' : 'IP를 차단하고 토큰을 삭제하시겠습니까?'} +

+

+ {actionType === 'delete' ? `서비스: ${token.service}, ${token.device} 에서 발급된 토큰을 더이상 사용할 수 없습니다!` : + `기존 토큰을 삭제하며 ${token.ip} / ${token.location} 에서 더이상 로그인 할 수 없습니다!`} +

+
+ + +
+
+
+ )} +
+ ); +}; + +export default TokenHistoryItem; \ No newline at end of file diff --git a/src/components/token/TokenHistoryList.tsx b/src/components/token/TokenHistoryList.tsx new file mode 100644 index 0000000..02fb5f9 --- /dev/null +++ b/src/components/token/TokenHistoryList.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import TokenHistoryItem from './TokenHistoryItem'; +import Token from '../../types/Token'; + +interface TokenHistoryListProps { + tokens: Token[]; + onDelete: (id: string) => void; + onBlockIP: (ip: string) => void; +} + +const TokenHistoryList: React.FC = ({ tokens, onDelete, onBlockIP }) => { + return ( +
+ {tokens.map((token) => ( + + ))} +
+ ); +}; + +export default TokenHistoryList; \ No newline at end of file diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 04ad2cb..d5e08a4 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -4,13 +4,14 @@ import LoginHistoryItem from "../components/home/LoginHistoryItem"; import Layout from "../components/home/HomeLayout"; const HomePage: React.FC = () => ( - +
-

반가워요

-

이동현 님

+

반갑습니다

+

Lee Donghyun 님의 나누아이디 사용을 환영합니다

+

추천 설정

( subtitle="이메일이 인증되지 않음" />
+

최근 활동

로그인 기록

diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 6352a5e..2258b4c 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -18,7 +18,7 @@ const LoginPage = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [captchaToken, setCaptchaToken] = useState(null); // 리캡챠 토큰 상태 추가 + const [captchaToken, setCaptchaToken] = useState(null); const [isPinModalOpen, setIsPinModalOpen] = useState(false); const navigate = useNavigate(); diff --git a/src/pages/MyPage.tsx b/src/pages/MyPage.tsx new file mode 100644 index 0000000..44833d6 --- /dev/null +++ b/src/pages/MyPage.tsx @@ -0,0 +1,107 @@ +import React, { useState } from "react"; +import { Edit, Phone, Mail, User } from "lucide-react"; +import PasswordChangeDialog from "../components/mypage/PasswordChangeDialog"; +import PinChangeDialog from "../components/mypage/PinChangeDialog"; +import ProfileImageUpload from "../components/mypage/ProfileImageUpload"; +import Layout from "../components/home/HomeLayout"; + +const mockUserData = { + name: "Lee Donghyun", + email: "m*****n@gmail.com", + phone: "010-****-7890", + profileImage: "/default_profile.png", + connectedDevices: [ + { + loginDate: "2024.01.23 09:00", + deviceName: "SM-P580 Android" + }, + ], +}; + +const MyPage: React.FC = () => { + const [userName, setUserName] = useState(mockUserData.name); + const [isEditingName, setIsEditingName] = useState(false); + + return ( + +
+
+

마이 페이지

+

+ 개인 정보를 확인하고 수정할 수 있습니다 +

+
+ +
+
+
+ +
+ {isEditingName ? ( +
+ setUserName(e.target.value)} + className="border rounded-lg px-2 py-1" + /> + +
+ ) : ( +
+

{userName}

+ +
+ )} +

{mockUserData.email}

+
+
+
+ +
+

보안 설정

+
+ + +
+
+
+ +
+
+

로그인된 모바일 기기

+
+ + {mockUserData.connectedDevices[0]?.deviceName === "NONE" ? ( +
+

로그인된 기기가 없습니다.

+
+ ) : ( +
+
+

+ {mockUserData.connectedDevices[0]?.deviceName} +

+
+

+ {mockUserData.connectedDevices[0]?.loginDate} +

+
+ )} +
+
+
+ ); +}; + +export default MyPage; diff --git a/src/pages/TokenInfoPage.tsx b/src/pages/TokenInfoPage.tsx new file mode 100644 index 0000000..42a3e24 --- /dev/null +++ b/src/pages/TokenInfoPage.tsx @@ -0,0 +1,68 @@ +import { useState, useEffect } from "react"; +import TokenHistoryList from "../components/token/TokenHistoryList"; +import Layout from "../components/home/HomeLayout"; +import { getLocationByIP } from "../services/IpLocationService"; +const mockTokens = [ + { + id: '1', + date: '2024.1.23 AM 9:00 KST', + service: 'DASHBOARD(NANUID)', + device: 'Android Web', + ip: '103.21.244.31', + location: '' // 초기값은 빈 문자열 + }, + { + id: '2', + date: '2024.1.21 AM 2:00 KST', + service: 'VocaVault Service', + device: 'Windows Web', + ip: '103.21.244.91', + location: '' // 초기값은 빈 문자열 + } +]; + +const TokenInfoPage: React.FC = () => { + const [tokens, setTokens] = useState(mockTokens); + + const fetchLocationForTokens = async () => { + const updatedTokens = await Promise.all( + tokens.map(async (token) => { + const location = await getLocationByIP(token.ip); + return { ...token, location }; + }) + ); + setTokens(updatedTokens); // 위치 정보 업데이트 + }; + + useEffect(() => { + fetchLocationForTokens(); // 컴포넌트가 마운트될 때 위치 정보 가져오기 + }, []); // 최초 렌더링 시 한 번만 실행 + + const handleDeleteToken = (id: string) => { + setTokens(tokens.filter(token => token.id !== id)); + console.log(`Token ${id} deleted`); + }; + + const handleBlockIP = (ip: string) => { + console.log(`IP ${ip} blocked`); + }; + + return ( + +
+
+

토큰 내역

+

로그인된 토큰 기록을 관리합니다

+
+ + +
+
+ ); +}; + +export default TokenInfoPage; diff --git a/src/services/IpLocationService.ts b/src/services/IpLocationService.ts new file mode 100644 index 0000000..1459a3a --- /dev/null +++ b/src/services/IpLocationService.ts @@ -0,0 +1,14 @@ +export const getLocationByIP = async (ip: string): Promise => { + try { + const response = await fetch(`http://ip-api.com/json/${ip}`); + const data = await response.json(); + if (data.status === "success") { + return `${data.city}, ${data.regionName}, ${data.country}`; + } else { + return "Unknown"; + } + } catch (error) { + console.error("Error fetching location:", error); + return "Unknown"; + } +}; diff --git a/src/types/Token.ts b/src/types/Token.ts new file mode 100644 index 0000000..b9ceee9 --- /dev/null +++ b/src/types/Token.ts @@ -0,0 +1,8 @@ +export default interface Token { + id: string; + service: string; + date: string; + device: string; + ip: string; + location: string; +}