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 성공
176 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|