-
-
+ return (
+
-);
+
+
+ );
+};
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 && (
+
+ )}
+
+ );
+};
+
+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 태그로 대체 */}
+
+

+
+
+
+
+
+
+ );
+};
+
+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.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 = () => (
-
+
+
추천 설정
(
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;
+}