TASK-020 버그 수정:
- BUG-1: adminFetch Authorization 헤더 우선순위 수정 (JWT > API Key)
하네스 저장 실패 에러 핸들링 개선 (401/non-ok 명시적 처리)
- BUG-2: /admin/logs useEffect 초기 로드 추가 (마운트 시 자동 조회)
- BUG-3: costs/record python3 → node/python3/jq fallback 체인
TASK-021 JWT BE:
- User 모델 추가 (Prisma schema)
- AuthService: register(초대코드 검증+bcrypt) / login / refresh / getMe
- JwtStrategy (passport-jwt), JwtGuard, CompositeGuard (JWT+API Key)
- AuthController: /api/auth/{register,login,refresh,me}
- ThrottlerModule: 로그인 5회/분 Rate limiting
- CompositeGuard로 AdminController, CostsController Guard 전환
- EventsGateway: Socket.IO handshake JWT 검증 추가
- 테스트 26/26 pass
TASK-022 FE:
- AuthContext (AuthProvider + useAuth)
- AppShell (AuthProvider + AuthGate 라우트 보호)
- /login 페이지 (터미널 UI)
- /register 페이지 (초대 코드 필드)
- Sidebar: 로그인 사용자명 + 로그아웃 버튼
- FE 16 routes build 성공
116 lines
2.9 KiB
TypeScript
116 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
import styled from 'styled-components';
|
|
import LogTerminal from '@/components/admin/LogTerminal';
|
|
import { adminFetch } from '@/lib/adminFetch';
|
|
|
|
const SISTERS = [
|
|
{ value: 'harang', label: '🦊 하랑이' },
|
|
{ value: 'narang', label: '🦊 나랑이' },
|
|
{ value: 'darang', label: '🐱 다랑이' },
|
|
{ value: 'erang', label: '🐺 이랑이' },
|
|
];
|
|
|
|
const Controls = styled.div`
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
`;
|
|
|
|
const Select = styled.select`
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 7px;
|
|
color: var(--text-primary);
|
|
padding: 8px 12px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
outline: none;
|
|
&:focus { border-color: #58A6FF; }
|
|
option { background: #1a1f2a; }
|
|
`;
|
|
|
|
const LinesInput = styled.input`
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 7px;
|
|
color: var(--text-primary);
|
|
padding: 8px 12px;
|
|
font-size: 13px;
|
|
width: 80px;
|
|
outline: none;
|
|
&:focus { border-color: #58A6FF; }
|
|
`;
|
|
|
|
const FetchBtn = styled.button`
|
|
padding: 8px 18px;
|
|
border-radius: 7px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
background: rgba(88,166,255,0.1);
|
|
border: 1px solid rgba(88,166,255,0.35);
|
|
color: #58A6FF;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
&:hover { background: rgba(88,166,255,0.2); }
|
|
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
`;
|
|
|
|
const Label = styled.span`
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
`;
|
|
|
|
export default function LogsPage() {
|
|
const [sister, setSister] = useState('narang');
|
|
const [lines, setLines] = useState(100);
|
|
const [logLines, setLogLines] = useState<string[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const fetchLogs = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await adminFetch(`/api/admin/logs/${sister}?lines=${lines}`);
|
|
const d = await res.json();
|
|
setLogLines(d.lines ?? []);
|
|
} catch {
|
|
setLogLines(['(로그 로드 실패)']);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [sister, lines]);
|
|
|
|
// 마운트 시 자동 로드
|
|
useEffect(() => {
|
|
fetchLogs();
|
|
}, [fetchLogs]);
|
|
|
|
return (
|
|
<>
|
|
<Controls>
|
|
<Select value={sister} onChange={(e) => setSister(e.target.value)}>
|
|
{SISTERS.map((s) => (
|
|
<option key={s.value} value={s.value}>{s.label}</option>
|
|
))}
|
|
</Select>
|
|
<Label>최근</Label>
|
|
<LinesInput
|
|
type="number"
|
|
value={lines}
|
|
onChange={(e) => setLines(parseInt(e.target.value, 10) || 100)}
|
|
min={10}
|
|
max={500}
|
|
/>
|
|
<Label>줄</Label>
|
|
<FetchBtn onClick={fetchLogs} disabled={loading}>
|
|
{loading ? '로딩 중...' : '📋 로그 가져오기'}
|
|
</FetchBtn>
|
|
</Controls>
|
|
|
|
<LogTerminal lines={logLines} />
|
|
</>
|
|
);
|
|
}
|