Files
hanarang-dashboard/frontend/app/login/page.tsx
나랑이 176cb51f8a feat(sprint-010): API Key 완전 제거 + JWT 전용 정리 + 죽은 코드 청소
TASK-036: admin/layout.tsx API Key UI 완전 제거
TASK-037: adminFetch JWT 전용 단순화 (api key fallback 제거)
TASK-038: ApiKeyStrategy, ApiKeyGuard, CompositeGuard 제거
  - api-key.strategy.ts, api-key.guard.ts 파일 삭제
  - 모든 컨트롤러 @UseGuards JwtGuard로 전환
  - AuthModule에서 ApiKey 관련 providers 제거
TASK-039: PUBLIC_PATHS에서 /register 제거, login FooterLink dead code 제거

테스트 23/23 pass, FE 16 routes build 성공
2026-04-04 17:24:32 +09:00

176 lines
4.1 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import styled from 'styled-components';
import { useAuth } from '@/lib/AuthContext';
import { useRouter } from 'next/navigation';
const Page = styled.div`
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg-main);
`;
const Box = styled.div`
width: 100%;
max-width: 360px;
padding: var(--space-xl);
`;
const Logo = styled.div`
display: flex;
align-items: center;
gap: 4px;
margin-bottom: var(--space-xxl);
`;
const CircleFull = styled.div`
width: 20px; height: 20px;
background: var(--text-primary);
border-radius: 50%;
`;
const CircleHalf = styled.div`
width: 10px; height: 20px;
background: var(--text-primary);
border-radius: 0 20px 20px 0;
`;
const Title = styled.h1`
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-xs);
letter-spacing: -0.02em;
`;
const Subtitle = styled.p`
font-size: 12px;
color: var(--text-secondary);
font-family: var(--font-mono);
margin-bottom: var(--space-xl);
`;
const Form = styled.form`
display: flex;
flex-direction: column;
gap: var(--space-md);
`;
const FieldLabel = styled.label`
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
margin-bottom: var(--space-xs);
display: block;
`;
const Input = styled.input`
width: 100%;
background: var(--bg-input);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 13px;
outline: none;
transition: border-color 0.15s;
&:focus { border-color: var(--border-hover); }
&::placeholder { color: var(--text-secondary); opacity: 0.5; }
`;
const SubmitBtn = styled.button`
background: var(--text-primary);
border: 1px solid var(--text-primary);
color: #000;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 12px;
cursor: pointer;
transition: all 0.15s;
margin-top: var(--space-sm);
&:hover { background: transparent; color: var(--text-primary); }
&:disabled { opacity: 0.5; cursor: not-allowed; }
`;
const ErrorMsg = styled.div`
font-family: var(--font-mono);
font-size: 11px;
color: #ff5f5f;
padding: var(--space-sm);
border: 1px solid #ff5f5f44;
background: rgba(255,95,95,0.05);
`;
export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(username, password);
router.push('/');
} catch (err) {
setError((err as Error).message ?? '로그인 실패');
} finally {
setLoading(false);
}
};
return (
<Page>
<Box>
<Logo><CircleFull /><CircleHalf /></Logo>
<Title> </Title>
<Subtitle>SYSTEM_ACCESS // AUTHENTICATE</Subtitle>
<Form onSubmit={handleSubmit}>
<div>
<FieldLabel>USERNAME</FieldLabel>
<Input
type="text"
placeholder="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
required
/>
</div>
<div>
<FieldLabel>PASSWORD</FieldLabel>
<Input
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
required
/>
</div>
{error && <ErrorMsg>ERR: {error}</ErrorMsg>}
<SubmitBtn type="submit" disabled={loading}>
{loading ? 'AUTHENTICATING...' : 'LOGIN'}
</SubmitBtn>
</Form>
</Box>
</Page>
);
}