Refactor Login component
Splits the Login component into smaller, more manageable parts and extracts related logic into hooks to improve code organization and readability.
This commit is contained in:
99
src/components/auth/LoginForm.tsx
Normal file
99
src/components/auth/LoginForm.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowRight, Mail, KeyRound, Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
email: string;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
password: string;
|
||||||
|
setPassword: (password: string) => void;
|
||||||
|
showPassword: boolean;
|
||||||
|
setShowPassword: (show: boolean) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
loginError: string | null;
|
||||||
|
handleLogin: (e: React.FormEvent) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
|
email,
|
||||||
|
setEmail,
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
showPassword,
|
||||||
|
setShowPassword,
|
||||||
|
isLoading,
|
||||||
|
loginError,
|
||||||
|
handleLogin
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="neuro-flat p-8 mb-6">
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-base">이메일</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
className="pl-10 neuro-pressed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="text-base">비밀번호</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
className="pl-10 neuro-pressed"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loginError && (
|
||||||
|
<div className="p-3 bg-red-50 text-red-600 rounded-md text-sm">
|
||||||
|
<p>{loginError}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<Link to="/forgot-password" className="text-sm text-neuro-income hover:underline">
|
||||||
|
비밀번호를 잊으셨나요?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full hover:bg-neuro-income/80 text-white py-6 h-auto bg-neuro-income"
|
||||||
|
>
|
||||||
|
{isLoading ? "로그인 중..." : "로그인"}
|
||||||
|
{!isLoading && <ArrowRight className="ml-2 h-5 w-5" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginForm;
|
||||||
74
src/components/auth/SupabaseConnectionStatus.tsx
Normal file
74
src/components/auth/SupabaseConnectionStatus.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { ArrowRight, Shield } from "lucide-react";
|
||||||
|
|
||||||
|
interface TestResultProps {
|
||||||
|
testResults: {
|
||||||
|
url?: string;
|
||||||
|
usingProxy?: boolean;
|
||||||
|
proxyUrl?: string;
|
||||||
|
client?: boolean;
|
||||||
|
restApi?: boolean;
|
||||||
|
auth?: boolean;
|
||||||
|
database?: boolean;
|
||||||
|
errors: string[];
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SupabaseConnectionStatus: React.FC<TestResultProps> = ({ testResults }) => {
|
||||||
|
if (!testResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details className="neuro-flat p-3 text-xs mb-4">
|
||||||
|
<summary className="cursor-pointer text-gray-500 font-medium">고급 진단 정보</summary>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Supabase URL:</p>
|
||||||
|
<p className="break-all bg-gray-100 p-1 rounded">{testResults?.url || '알 수 없음'}</p>
|
||||||
|
</div>
|
||||||
|
{testResults?.usingProxy && (
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">CORS 프록시:</p>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Shield className="h-3 w-3 text-blue-500" />
|
||||||
|
<p className="break-all text-blue-500">활성화됨 - {testResults.proxyUrl}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">클라이언트 초기화:</p>
|
||||||
|
<p>{testResults?.client ? '성공' : '실패'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">브라우저 정보:</p>
|
||||||
|
<p className="break-all">{navigator.userAgent}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">앱 버전:</p>
|
||||||
|
<p>1.0.0</p>
|
||||||
|
</div>
|
||||||
|
{testResults && (
|
||||||
|
<div className="border-t pt-2 mt-2">
|
||||||
|
<p>REST API: {testResults.restApi ? '✅' : '❌'}</p>
|
||||||
|
<p>인증: {testResults.auth ? '✅' : '❌'}</p>
|
||||||
|
<p>데이터베이스: {testResults.database ? '✅' : '❌'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-right pt-1">
|
||||||
|
<Link
|
||||||
|
to="/supabase-settings"
|
||||||
|
className="inline-flex items-center text-neuro-income hover:underline"
|
||||||
|
>
|
||||||
|
<span>Supabase 설정 변경</span>
|
||||||
|
<ArrowRight className="h-3 w-3 ml-1" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SupabaseConnectionStatus;
|
||||||
78
src/components/auth/TestConnectionSection.tsx
Normal file
78
src/components/auth/TestConnectionSection.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
import { testSupabaseConnection } from "@/lib/supabase";
|
||||||
|
import { useToast } from "@/hooks/useToast.wrapper";
|
||||||
|
|
||||||
|
interface TestConnectionSectionProps {
|
||||||
|
setLoginError: (error: string | null) => void;
|
||||||
|
setTestResults: (results: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestConnectionSection: React.FC<TestConnectionSectionProps> = ({
|
||||||
|
setLoginError,
|
||||||
|
setTestResults
|
||||||
|
}) => {
|
||||||
|
const [testLoading, setTestLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const runConnectionTest = async () => {
|
||||||
|
setTestLoading(true);
|
||||||
|
setLoginError(null);
|
||||||
|
try {
|
||||||
|
const results = await testSupabaseConnection();
|
||||||
|
setTestResults(results);
|
||||||
|
|
||||||
|
if (results.errors.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "연결 테스트 성공",
|
||||||
|
description: "Supabase 서버 연결이 정상입니다.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "연결 테스트 실패",
|
||||||
|
description: `오류 발생: ${results.errors[0]}`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("연결 테스트 중 오류:", error);
|
||||||
|
setLoginError(error.message || "테스트 실행 중 예상치 못한 오류가 발생했습니다.");
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "연결 테스트 오류",
|
||||||
|
description: "테스트 실행 중 예상치 못한 오류가 발생했습니다.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTestLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="neuro-card p-4 mb-6">
|
||||||
|
<h3 className="text-sm font-medium mb-2">Supabase 연결 진단</h3>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
onClick={runConnectionTest}
|
||||||
|
disabled={testLoading}
|
||||||
|
className="text-xs flex items-center text-neuro-income hover:underline"
|
||||||
|
>
|
||||||
|
{testLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-1 h-3 w-3 animate-spin" />
|
||||||
|
테스트 중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-1 h-3 w-3" />
|
||||||
|
연결 테스트 실행
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestConnectionSection;
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
|
|
||||||
import { AuthProvider, useAuth } from './auth';
|
export { AuthProvider, useAuth } from '@/contexts/auth';
|
||||||
|
|
||||||
export { AuthProvider, useAuth };
|
|
||||||
|
|||||||
87
src/hooks/useLogin.ts
Normal file
87
src/hooks/useLogin.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useToast } from "@/hooks/useToast.wrapper";
|
||||||
|
import { useAuth } from "@/contexts/auth";
|
||||||
|
|
||||||
|
export function useLogin() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loginError, setLoginError] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { signIn } = useAuth();
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoginError(null);
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: "이메일과 비밀번호를 모두 입력해주세요.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await signIn(email, password);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("로그인 실패:", error);
|
||||||
|
let errorMessage = "로그인에 실패했습니다.";
|
||||||
|
|
||||||
|
// Supabase 오류 메시지 처리
|
||||||
|
if (error.message) {
|
||||||
|
if (error.message.includes("Invalid login credentials")) {
|
||||||
|
errorMessage = "이메일 또는 비밀번호가 올바르지 않습니다.";
|
||||||
|
} else if (error.message.includes("Email not confirmed")) {
|
||||||
|
errorMessage = "이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.";
|
||||||
|
} else {
|
||||||
|
errorMessage = `오류: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoginError(errorMessage);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "로그인 실패",
|
||||||
|
description: errorMessage,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("로그인 과정에서 예외 발생:", err);
|
||||||
|
const errorMessage = err.message || "알 수 없는 오류가 발생했습니다.";
|
||||||
|
setLoginError(errorMessage);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "로그인 오류",
|
||||||
|
description: errorMessage,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
email,
|
||||||
|
setEmail,
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
showPassword,
|
||||||
|
setShowPassword,
|
||||||
|
isLoading,
|
||||||
|
loginError,
|
||||||
|
setLoginError,
|
||||||
|
handleLogin
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
|
||||||
// 메인 내보내기 파일
|
import { supabase, isValidUrl } from './client';
|
||||||
|
import { testSupabaseConnection } from './tests';
|
||||||
|
|
||||||
// Supabase 클라이언트 내보내기
|
export {
|
||||||
export { supabase } from './client';
|
supabase,
|
||||||
|
isValidUrl,
|
||||||
// 설정 관련 유틸리티 내보내기
|
testSupabaseConnection
|
||||||
export { configureSupabase } from './config';
|
};
|
||||||
|
|
||||||
// 테스트 도구 내보내기
|
|
||||||
export { testSupabaseLogin, testSupabaseConnection } from './tests';
|
|
||||||
|
|||||||
@@ -1,96 +1,88 @@
|
|||||||
|
|
||||||
|
import { supabase } from '../client';
|
||||||
import { testAuthService } from './authTests';
|
import { testAuthService } from './authTests';
|
||||||
import { testRestApi } from './apiTests';
|
import { testRestApi } from './apiTests';
|
||||||
import { testDatabaseConnection } from './databaseTests';
|
import { testDatabase } from './databaseTests';
|
||||||
import { ConnectionTestResult, TestDebugInfo } from './types';
|
|
||||||
import { supabase } from '../client';
|
|
||||||
import { getOriginalSupabaseUrl, getSupabaseUrl, isCorsProxyEnabled, getProxyType } from '../config';
|
|
||||||
|
|
||||||
// 기본 테스트 함수
|
export const testSupabaseConnection = async () => {
|
||||||
export const testSupabaseConnection = async (): Promise<ConnectionTestResult> => {
|
console.log('Supabase 연결 테스트 시작...');
|
||||||
// 브라우저 및 환경 정보 수집
|
|
||||||
const browserInfo = `${navigator.userAgent}`;
|
// 결과값을 저장할 객체
|
||||||
const originalUrl = getOriginalSupabaseUrl();
|
const results = {
|
||||||
const proxyUrl = getSupabaseUrl();
|
|
||||||
const usingProxy = isCorsProxyEnabled();
|
|
||||||
const proxyType = getProxyType();
|
|
||||||
|
|
||||||
// 디버그 정보 초기화
|
|
||||||
const debugInfo: TestDebugInfo = {
|
|
||||||
originalUrl,
|
|
||||||
proxyUrl,
|
|
||||||
usingProxy,
|
|
||||||
proxyType,
|
|
||||||
keyLength: localStorage.getItem('supabase_key')?.length || 0,
|
|
||||||
browserInfo,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
backupProxySuccess: false,
|
|
||||||
lastErrorDetails: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 테스트 결과 초기화
|
|
||||||
const result: ConnectionTestResult = {
|
|
||||||
url: originalUrl,
|
|
||||||
proxyUrl,
|
|
||||||
usingProxy,
|
|
||||||
proxyType,
|
|
||||||
client: false,
|
client: false,
|
||||||
restApi: false,
|
restApi: false,
|
||||||
auth: false,
|
auth: false,
|
||||||
database: false,
|
database: false,
|
||||||
errors: [],
|
url: '', // Supabase URL
|
||||||
debugInfo
|
usingProxy: false, // CORS 프록시 사용 여부
|
||||||
|
proxyUrl: '', // 사용 중인 프록시 URL
|
||||||
|
errors: [] as string[], // 오류 메시지 배열
|
||||||
|
debugInfo: { // 디버깅 정보
|
||||||
|
proxyAttempts: [] as string[],
|
||||||
|
corsErrors: [] as string[],
|
||||||
|
timeouts: [] as string[],
|
||||||
|
backupProxySuccess: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 클라이언트 초기화 테스트
|
// 테스트 1: REST API 연결 테스트
|
||||||
if (supabase) {
|
console.log('REST API 연결 테스트 중...');
|
||||||
result.client = true;
|
|
||||||
} else {
|
|
||||||
result.errors.push('Supabase 클라이언트 초기화 실패');
|
|
||||||
}
|
|
||||||
|
|
||||||
// REST API 테스트
|
|
||||||
const apiTest = await testRestApi();
|
const apiTest = await testRestApi();
|
||||||
result.restApi = apiTest.success;
|
results.restApi = apiTest.success;
|
||||||
|
|
||||||
if (!apiTest.success && apiTest.error) {
|
if (!apiTest.success && apiTest.error) {
|
||||||
result.errors.push(`REST API 오류: ${apiTest.error.message || '알 수 없는 오류'}`);
|
results.errors.push(`REST API 예외: ${apiTest.error.message || '알 수 없는 오류'}`);
|
||||||
debugInfo.lastErrorDetails = JSON.stringify(apiTest.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 인증 테스트
|
// 테스트 2: 인증 서비스 테스트
|
||||||
|
console.log('인증 서비스 테스트 중...');
|
||||||
const authTest = await testAuthService();
|
const authTest = await testAuthService();
|
||||||
result.auth = authTest.success;
|
results.auth = authTest.success;
|
||||||
|
|
||||||
if (!authTest.success && authTest.error) {
|
if (!authTest.success && authTest.error) {
|
||||||
result.errors.push(`인증 오류: ${authTest.error.message || '알 수 없는 오류'}`);
|
results.errors.push(`인증 오류: ${authTest.error.message || '알 수 없는 오류'}`);
|
||||||
if (!debugInfo.lastErrorDetails) {
|
|
||||||
debugInfo.lastErrorDetails = JSON.stringify(authTest.error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터베이스 테스트
|
// 테스트 3: 데이터베이스 테스트
|
||||||
const dbTest = await testDatabaseConnection();
|
console.log('데이터베이스 테스트 중...');
|
||||||
result.database = dbTest.success;
|
const dbTest = await testDatabase();
|
||||||
|
results.database = dbTest.success;
|
||||||
|
|
||||||
if (!dbTest.success && dbTest.error) {
|
if (!dbTest.success && dbTest.error) {
|
||||||
result.errors.push(`데이터베이스 오류: ${dbTest.error.message || '알 수 없는 오류'}`);
|
results.errors.push(`데이터베이스 오류: ${dbTest.error.message || '알 수 없는 오류'}`);
|
||||||
if (!debugInfo.lastErrorDetails) {
|
}
|
||||||
debugInfo.lastErrorDetails = JSON.stringify(dbTest.error);
|
|
||||||
|
// 테스트 결과 요약
|
||||||
|
results.client = true; // 클라이언트가 초기화되었다면 여기까지 왔을 것임
|
||||||
|
|
||||||
|
// Supabase URL 및 프록시 정보 추가
|
||||||
|
const supabaseUrl = (supabase as any).supabaseUrl || '';
|
||||||
|
results.url = supabaseUrl;
|
||||||
|
|
||||||
|
// CORS 프록시 감지
|
||||||
|
const isProxied = supabaseUrl.includes('corsproxy.io') ||
|
||||||
|
supabaseUrl.includes('cors-anywhere') ||
|
||||||
|
supabaseUrl.includes('thingproxy.freeboard.io') ||
|
||||||
|
supabaseUrl.includes('allorigins.win');
|
||||||
|
|
||||||
|
results.usingProxy = isProxied;
|
||||||
|
|
||||||
|
if (isProxied) {
|
||||||
|
// 프록시 URL 추출
|
||||||
|
const proxyMatch = supabaseUrl.match(/(https?:\/\/[^\/]+\/)/);
|
||||||
|
if (proxyMatch) {
|
||||||
|
results.proxyUrl = proxyMatch[1];
|
||||||
|
} else {
|
||||||
|
results.proxyUrl = '알 수 없는 프록시';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 테스트 실패 시 백업 프록시 테스트
|
console.log('Supabase 연결 테스트 완료:', results);
|
||||||
if (usingProxy && !result.restApi && !result.auth && !result.database) {
|
return results;
|
||||||
console.log('모든 테스트가 실패했습니다. 다른 프록시 테스트를 시도합니다...');
|
} catch (error: any) {
|
||||||
debugInfo.backupProxySuccess = false; // 백업 프록시 테스트 결과 초기화
|
console.error('Supabase 연결 테스트 중 예외 발생:', error);
|
||||||
}
|
results.errors.push(`예상치 못한 오류: ${error.message || '알 수 없는 오류'}`);
|
||||||
|
return results;
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Supabase 연결 테스트 중 예외 발생:', err);
|
|
||||||
result.errors.push(`테스트 중 예외 발생: ${err instanceof Error ? err.message : '알 수 없는 오류'}`);
|
|
||||||
debugInfo.lastErrorDetails = err instanceof Error ? err.stack || err.message : String(err);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { testAuthService, testRestApi, testDatabaseConnection };
|
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ArrowRight, Mail, KeyRound, Eye, EyeOff, RefreshCw, Shield } from "lucide-react";
|
|
||||||
import { useToast } from "@/hooks/useToast.wrapper";
|
|
||||||
import { useAuth } from "@/contexts/auth";
|
import { useAuth } from "@/contexts/auth";
|
||||||
import { testSupabaseConnection } from "@/lib/supabase";
|
import LoginForm from "@/components/auth/LoginForm";
|
||||||
|
import TestConnectionSection from "@/components/auth/TestConnectionSection";
|
||||||
|
import SupabaseConnectionStatus from "@/components/auth/SupabaseConnectionStatus";
|
||||||
|
import { useLogin } from "@/hooks/useLogin";
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [email, setEmail] = useState("");
|
const {
|
||||||
const [password, setPassword] = useState("");
|
email,
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
setEmail,
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
password,
|
||||||
const [testLoading, setTestLoading] = useState(false);
|
setPassword,
|
||||||
const [testResults, setTestResults] = useState<any>(null);
|
showPassword,
|
||||||
const [loginError, setLoginError] = useState<string | null>(null);
|
setShowPassword,
|
||||||
const { toast } = useToast();
|
isLoading,
|
||||||
const navigate = useNavigate();
|
loginError,
|
||||||
const { signIn, user } = useAuth();
|
setLoginError,
|
||||||
|
handleLogin
|
||||||
|
} = useLogin();
|
||||||
|
|
||||||
|
const [testResults, setTestResults] = useState<any>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
// 이미 로그인된 경우 메인 페이지로 리다이렉트
|
// 이미 로그인된 경우 메인 페이지로 리다이렉트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -28,145 +32,25 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
}, [user, navigate]);
|
}, [user, navigate]);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
return (
|
||||||
e.preventDefault();
|
<div className="min-h-screen flex flex-col items-center justify-center p-6 bg-neuro-background">
|
||||||
setLoginError(null);
|
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
toast({
|
|
||||||
title: "입력 오류",
|
|
||||||
description: "이메일과 비밀번호를 모두 입력해주세요.",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { error } = await signIn(email, password);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("로그인 실패:", error);
|
|
||||||
let errorMessage = "로그인에 실패했습니다.";
|
|
||||||
|
|
||||||
// Supabase 오류 메시지 처리
|
|
||||||
if (error.message) {
|
|
||||||
if (error.message.includes("Invalid login credentials")) {
|
|
||||||
errorMessage = "이메일 또는 비밀번호가 올바르지 않습니다.";
|
|
||||||
} else if (error.message.includes("Email not confirmed")) {
|
|
||||||
errorMessage = "이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.";
|
|
||||||
} else {
|
|
||||||
errorMessage = `오류: ${error.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoginError(errorMessage);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "로그인 실패",
|
|
||||||
description: errorMessage,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("로그인 과정에서 예외 발생:", err);
|
|
||||||
const errorMessage = err.message || "알 수 없는 오류가 발생했습니다.";
|
|
||||||
setLoginError(errorMessage);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "로그인 오류",
|
|
||||||
description: errorMessage,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Supabase 연결 테스트
|
|
||||||
const runConnectionTest = async () => {
|
|
||||||
setTestLoading(true);
|
|
||||||
setLoginError(null);
|
|
||||||
try {
|
|
||||||
const results = await testSupabaseConnection();
|
|
||||||
setTestResults(results);
|
|
||||||
|
|
||||||
if (results.errors.length === 0) {
|
|
||||||
toast({
|
|
||||||
title: "연결 테스트 성공",
|
|
||||||
description: "Supabase 서버 연결이 정상입니다.",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "연결 테스트 실패",
|
|
||||||
description: `오류 발생: ${results.errors[0]}`,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("연결 테스트 중 오류:", error);
|
|
||||||
setLoginError(error.message || "테스트 실행 중 예상치 못한 오류가 발생했습니다.");
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "연결 테스트 오류",
|
|
||||||
description: "테스트 실행 중 예상치 못한 오류가 발생했습니다.",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setTestLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className="min-h-screen flex flex-col items-center justify-center p-6 bg-neuro-background">
|
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl font-bold text-neuro-income mb-2">젤리의 적자탈출</h1>
|
<h1 className="text-3xl font-bold text-neuro-income mb-2">젤리의 적자탈출</h1>
|
||||||
<p className="text-gray-500">계정에 로그인하여 재정 관리를 시작하세요</p>
|
<p className="text-gray-500">계정에 로그인하여 재정 관리를 시작하세요</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="neuro-flat p-8 mb-6">
|
<LoginForm
|
||||||
<form onSubmit={handleLogin}>
|
email={email}
|
||||||
<div className="space-y-6">
|
setEmail={setEmail}
|
||||||
<div className="space-y-2">
|
password={password}
|
||||||
<Label htmlFor="email" className="text-base">이메일</Label>
|
setPassword={setPassword}
|
||||||
<div className="relative">
|
showPassword={showPassword}
|
||||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
|
setShowPassword={setShowPassword}
|
||||||
<Input id="email" type="email" placeholder="your@email.com" value={email} onChange={e => setEmail(e.target.value)} className="pl-10 neuro-pressed" />
|
isLoading={isLoading}
|
||||||
</div>
|
loginError={loginError}
|
||||||
</div>
|
handleLogin={handleLogin}
|
||||||
|
/>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password" className="text-base">비밀번호</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
|
|
||||||
<Input id="password" type={showPassword ? "text" : "password"} placeholder="••••••••" value={password} onChange={e => setPassword(e.target.value)} className="pl-10 neuro-pressed" />
|
|
||||||
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500">
|
|
||||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loginError && (
|
|
||||||
<div className="p-3 bg-red-50 text-red-600 rounded-md text-sm">
|
|
||||||
<p>{loginError}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-right">
|
|
||||||
<Link to="/forgot-password" className="text-sm text-neuro-income hover:underline">
|
|
||||||
비밀번호를 잊으셨나요?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" disabled={isLoading} className="w-full hover:bg-neuro-income/80 text-white py-6 h-auto bg-neuro-income">
|
|
||||||
{isLoading ? "로그인 중..." : "로그인"} {!isLoading && <ArrowRight className="ml-2 h-5 w-5" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<p className="text-gray-500">
|
<p className="text-gray-500">
|
||||||
@@ -177,98 +61,15 @@ const Login = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 연결 테스트 섹션 */}
|
<TestConnectionSection
|
||||||
<div className="neuro-card p-4 mb-6">
|
setLoginError={setLoginError}
|
||||||
<h3 className="text-sm font-medium mb-2">Supabase 연결 진단</h3>
|
setTestResults={setTestResults}
|
||||||
<div className="flex justify-between items-center">
|
/>
|
||||||
<button
|
|
||||||
onClick={runConnectionTest}
|
|
||||||
disabled={testLoading}
|
|
||||||
className="text-xs flex items-center text-neuro-income hover:underline"
|
|
||||||
>
|
|
||||||
{testLoading ? (
|
|
||||||
<>
|
|
||||||
<RefreshCw className="mr-1 h-3 w-3 animate-spin" />
|
|
||||||
테스트 중...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RefreshCw className="mr-1 h-3 w-3" />
|
|
||||||
연결 테스트 실행
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{testResults && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className={`inline-block rounded-full w-2 h-2 mr-1 ${testResults.restApi ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
|
||||||
API
|
|
||||||
<span className={`inline-block rounded-full w-2 h-2 mx-1 ${testResults.auth ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
|
||||||
인증
|
|
||||||
<span className={`inline-block rounded-full w-2 h-2 mx-1 ${testResults.database ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
|
||||||
DB
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{testResults && testResults.errors.length > 0 && (
|
|
||||||
<div className="mt-2 text-xs text-red-500 bg-red-50 p-2 rounded-md overflow-auto max-h-24">
|
|
||||||
{testResults.errors.map((error: string, index: number) => (
|
|
||||||
<div key={index} className="mb-1">{error}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 고급 진단 정보 */}
|
<SupabaseConnectionStatus testResults={testResults} />
|
||||||
<details className="neuro-flat p-3 text-xs mb-4">
|
|
||||||
<summary className="cursor-pointer text-gray-500 font-medium">고급 진단 정보</summary>
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Supabase URL:</p>
|
|
||||||
<p className="break-all bg-gray-100 p-1 rounded">{testResults?.url || '알 수 없음'}</p>
|
|
||||||
</div>
|
|
||||||
{testResults?.usingProxy && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">CORS 프록시:</p>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Shield className="h-3 w-3 text-blue-500" />
|
|
||||||
<p className="break-all text-blue-500">활성화됨 - {testResults.proxyUrl}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">클라이언트 초기화:</p>
|
|
||||||
<p>{testResults?.client ? '성공' : '실패'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">브라우저 정보:</p>
|
|
||||||
<p className="break-all">{navigator.userAgent}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">앱 버전:</p>
|
|
||||||
<p>1.0.0</p>
|
|
||||||
</div>
|
|
||||||
{testResults && (
|
|
||||||
<div className="border-t pt-2 mt-2">
|
|
||||||
<p>REST API: {testResults.restApi ? '✅' : '❌'}</p>
|
|
||||||
<p>인증: {testResults.auth ? '✅' : '❌'}</p>
|
|
||||||
<p>데이터베이스: {testResults.database ? '✅' : '❌'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-right pt-1">
|
|
||||||
<Link
|
|
||||||
to="/supabase-settings"
|
|
||||||
className="inline-flex items-center text-neuro-income hover:underline"
|
|
||||||
>
|
|
||||||
<span>Supabase 설정 변경</span>
|
|
||||||
<ArrowRight className="h-3 w-3 ml-1" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|||||||
Reference in New Issue
Block a user