Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/web/app/(auth)/find-id/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import { useFindId } from '@/shared/hooks/useFindId';
import { FindIdComponent } from '@/widgets/authentication/FindIdComponent';

export default function FindIdPage() {
const findIdProps = useFindId();

return (
<div className="bg-background-light flex min-h-screen w-full flex-col items-center pt-20">
<FindIdComponent {...findIdProps} />
</div>
);
}
20 changes: 4 additions & 16 deletions apps/web/app/(auth)/signup/complete/page.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
'use client';

import { useAuthStore } from '@/shared/stores/auth';
import { LoadingSpinner } from '@/widgets/loading-spiner';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

function SignupCompletePage() {
const router = useRouter();

useEffect(() => {
const syncAndRedirect = async () => {
// 서버의 세션 상태와 클라이언트 스토어를 동기화합니다.
await useAuthStore.getState().restore();

// 동기화가 완료된 후 메인 페이지로 자동 이동합니다.
router.push('/main');
};

syncAndRedirect();
}, [router]);
setTimeout(() => {
router.push('/login');
}, 1000);

return (
<div style={{ maxWidth: '400px', margin: 'auto', textAlign: 'center', padding: '40px 0' }}>
<h2>환영합니다!</h2>
<p className="mb-12 mt-4">가입 절차가 완료되었습니다.</p>
<p>잠시 후 메인 페이지로 이동합니다...</p>
<LoadingSpinner />
<p>잠시 후 로그인 페이지로 이동합니다...</p>
</div>
);
}
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/api/auth/verify-otp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export async function POST(request: NextRequest) {
{ data },
{
status: 200,
headers: response.headers,
}
);
}
5 changes: 3 additions & 2 deletions apps/web/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function middleware(request: NextRequest) {

// 대시보드, 프로필 등 보호된 페이지 경로 판별
const isProtectedRoute =
request.nextUrl.pathname.startsWith('/main') ||
request.nextUrl.pathname.startsWith('/signup/complete') ||
request.nextUrl.pathname.startsWith('/profile') ||
request.nextUrl.pathname.startsWith('/admin') ||
request.nextUrl.pathname.startsWith('/chat') ||
Expand All @@ -63,7 +63,8 @@ export async function middleware(request: NextRequest) {
}

// 로그인/회원가입 페이지에 접근하려는데 이미 로그인된 경우 대시보드로 리디렉션
if (isAuthRoute && session) {
// 단, /signup/complete 페이지는 예외로 둠
if (isAuthRoute && session && request.nextUrl.pathname !== '/signup/complete') {
return NextResponse.redirect(new URL('/main', request.url));
}

Expand Down
65 changes: 65 additions & 0 deletions apps/web/shared/hooks/useFindId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// apps/web/shared/hooks/useFindId.ts

'use client';

import { AuthService } from '@/shared/api/server/authentication/authService';
import { useCallback, useState } from 'react';
import { z } from 'zod'; // Import zod for client-side validation

type CheckStatus = '' | 'checking' | 'success' | 'error';

const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return '알 수 없는 오류가 발생했습니다.';
};

// Zod schema for email format validation
const emailSchema = z.string().email({ message: '유효한 이메일 주소를 입력해주세요.' });

export const useFindId = () => {
const [email, setEmail] = useState('');
const [isChecking, setIsChecking] = useState(false);
const [status, setStatus] = useState<CheckStatus>('');
const [message, setMessage] = useState('');

const onChangeEmail = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
setStatus('');
setMessage('');
}, []);

const onCheckEmail = useCallback(async () => {
// 1. Client-side email format validation
const validation = emailSchema.safeParse(email);
if (!validation.success) {
setMessage(validation.error.errors[0]!.message.toString());
setStatus('error');
return;
}

setIsChecking(true);
setStatus('checking');
setMessage('');

try {
await AuthService.checkEmailDuplicate(email);
setStatus('error');
setMessage('가입되지 않은 이메일입니다.');
} catch (error: any) {
setStatus('success');
setMessage('가입된 이메일입니다.');
} finally {
setIsChecking(false);
}
}, [email]);

return {
email,
isChecking,
status,
message,
onChangeEmail,
onCheckEmail,
};
};
81 changes: 81 additions & 0 deletions apps/web/widgets/authentication/FindIdComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// apps/web/widgets/authentication/FindIdComponent.tsx

'use client';

import { Button } from '@repo/ui/design-system/base-components/Button/index';
import { Input } from '@repo/ui/design-system/base-components/Input/index';
import React from 'react';
import { LoginLink } from './LoginLink';

interface FindIdComponentProps {
email: string;
isChecking: boolean;
status: '' | 'checking' | 'success' | 'error';
message: string;
onChangeEmail: (e: React.ChangeEvent<HTMLInputElement>) => void;
onCheckEmail: () => Promise<void>;
}

export const FindIdComponent: React.FC<FindIdComponentProps> = ({
email,
isChecking,
status,
message,
onChangeEmail,
onCheckEmail,
}) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onCheckEmail();
};

const getMessageColor = () => {
switch (status) {
case 'success':
return 'text-green-600';
case 'error':
return 'text-red-600';
case 'checking':
return 'text-gray-500'; // 또는 다른 색상
default:
return 'text-gray-500';
}
};

return (
<div className="w-full max-w-sm">
<h2 className="mb-8 text-center text-xl font-bold text-gray-800">아이디 찾기</h2>
<p className="mb-6 text-center text-sm text-gray-600">
가입 시 사용한 이메일 주소를 입력하시면
<br />
가입 여부를 확인해 드립니다.
</p>
<form onSubmit={handleSubmit} noValidate>
<div className="mb-4">
<Input
label="이메일"
type="email"
value={email}
onChange={onChangeEmail}
required
disabled={isChecking}
placeholder="이메일을 입력하세요"
/>
</div>

{message && <p className={`mb-4 text-center text-sm ${getMessageColor()}`}>{message}</p>}

<Button
type="submit"
className="w-full font-bold"
size="lg"
variants="primary"
disabled={!email || isChecking}
>
{isChecking ? '확인 중...' : '이메일로 아이디 찾기'}
</Button>
</form>
<LoginLink />
</div>
);
};
19 changes: 10 additions & 9 deletions apps/web/widgets/authentication/LoginFormComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,22 @@ export const LoginFormComponent: React.FC<LoginFormComponentProps> = ({
</div>
{error && <p className="mb-4 text-center text-sm text-red-600">{error}</p>}

<div className="relative mb-8 flex w-full items-center justify-center text-sm text-gray-500">
<Link
href="/password-reset"
className="hover:text-primary-900 absolute left-9 transition-colors"
>
<div className="mb-8 flex justify-center space-x-4 text-sm text-gray-500">
{' '}
{/* 폰트 크기 조정 */}
<Link href="/find-id" className="hover:text-primary-900 transition-colors">
계정 찾기
</Link>
<span className="text-gray-300">|</span>
<Link href="/password-reset" className="hover:text-primary-900 transition-colors">
비밀번호 찾기
</Link>
<span className="text-gray-300">|</span>
<Link
href="/signup"
className="hover:text-primary-900 absolute right-14 transition-colors"
>
<Link href="/signup" className="hover:text-primary-900 transition-colors">
회원가입
</Link>
</div>

<Button
type="submit"
className="bg-primary-500 hover:bg-primary-600 focus:ring-primary-500 w-full rounded-lg font-bold text-white transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"
Expand Down