Skip to content

Commit 65ca760

Browse files
SOIVclaude
andcommitted
feat(web): 관리자/사용자 홈 분리 및 PIN 세션 30분 만료 UX 구현
- HomeView에 isAdmin prop 추가 — 관리자 로그인 시 Admin Overview 배너 표시 (활성 사용자·설치 모듈·시스템 상태 카드, Admin 패널 바로가기 버튼) - 관리자 PIN 세션 30분 자동 만료 구현 (pinVerifiedAt 타임스탬프 기반 setTimeout) 만료 시 isPinVerified 리셋 + notice 표시 → AdminView 재인증 게이트 자동 복원 - home.css에 어드민 배너/카드 스타일 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3e153d5 commit 65ca760

4 files changed

Lines changed: 108 additions & 4 deletions

File tree

apps/web/src/main.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ function App({ installMode }: { installMode: InstallMode }) {
104104
const [isPinVerified, setIsPinVerified] = useState(
105105
() => sessionStorage.getItem(SS.pinVerified) === "true",
106106
);
107+
const [pinVerifiedAt, setPinVerifiedAt] = useState<number | null>(null);
107108
// OTP 인증 대기 중인 이메일 (로그인 완료 전 임시 상태 — sessionStorage 미저장)
108109
const [pendingOtpEmail, setPendingOtpEmail] = useState<string | null>(null);
109110

@@ -141,6 +142,26 @@ function App({ installMode }: { installMode: InstallMode }) {
141142
return () => window.removeEventListener("hashchange", handleHashChange);
142143
}, []);
143144

145+
// 관리자 PIN 세션 30분 만료
146+
useEffect(() => {
147+
if (!isPinVerified || pinVerifiedAt === null) return;
148+
const remaining = pinVerifiedAt + 30 * 60 * 1000 - Date.now();
149+
if (remaining <= 0) {
150+
setIsPinVerified(false);
151+
setPinVerifiedAt(null);
152+
sessionStorage.removeItem(SS.pinVerified);
153+
setNotice("관리자 세션이 만료되었습니다. 다시 인증해 주세요.");
154+
return;
155+
}
156+
const timer = setTimeout(() => {
157+
setIsPinVerified(false);
158+
setPinVerifiedAt(null);
159+
sessionStorage.removeItem(SS.pinVerified);
160+
setNotice("관리자 세션이 만료되었습니다. 다시 인증해 주세요.");
161+
}, remaining);
162+
return () => clearTimeout(timer);
163+
}, [isPinVerified, pinVerifiedAt]);
164+
144165
const effectiveRoute = useMemo<RouteKey>(() => {
145166
// OTP 대기 중: login 화면 유지 (LoginView 내부에서 step 전환)
146167
if (pendingOtpEmail) return "login";
@@ -266,6 +287,7 @@ function App({ installMode }: { installMode: InstallMode }) {
266287
const onPinVerified = () => {
267288
setIsAdmin(true);
268289
setIsPinVerified(true);
290+
setPinVerifiedAt(Date.now());
269291
setIsPinModalOpen(false);
270292
sessionStorage.setItem(SS.admin, "true");
271293
sessionStorage.setItem(SS.pinVerified, "true");
@@ -277,6 +299,7 @@ function App({ installMode }: { installMode: InstallMode }) {
277299
setIsAuthenticated(false);
278300
setIsAdmin(false);
279301
setIsPinVerified(false);
302+
setPinVerifiedAt(null);
280303
setCurrentUser(null);
281304
sessionStorage.removeItem(SS.auth);
282305
sessionStorage.removeItem(SS.admin);
@@ -354,7 +377,13 @@ function App({ installMode }: { installMode: InstallMode }) {
354377
onLogout={onLogout}
355378
onOpenSettings={() => setIsSettingsOpen(true)}
356379
>
357-
{effectiveRoute === "home" && <HomeView onOpenSettings={() => setIsSettingsOpen(true)} />}
380+
{effectiveRoute === "home" && (
381+
<HomeView
382+
isAdmin={isAdmin}
383+
onOpenSettings={() => setIsSettingsOpen(true)}
384+
onNavigateAdmin={() => navigate("admin")}
385+
/>
386+
)}
358387
{effectiveRoute === "marketplace" && <MarketplaceView />}
359388
{effectiveRoute === "admin" && (
360389
<AdminView isPinVerified={isPinVerified} onRequestPin={() => setIsPinModalOpen(true)} />
@@ -371,6 +400,7 @@ function App({ installMode }: { installMode: InstallMode }) {
371400
if (!next) {
372401
// 관리자 역할 해제 시 PIN 인증도 초기화
373402
setIsPinVerified(false);
403+
setPinVerifiedAt(null);
374404
sessionStorage.removeItem(SS.pinVerified);
375405
}
376406
setNotice(next ? "Admin authority enabled (mock)." : "Admin authority disabled.");

apps/web/src/styles/home.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,49 @@
289289
color: var(--text-faint);
290290
}
291291

292+
/* ── Admin Banner ─────────────────────────────────────────── */
293+
.home-admin-banner {
294+
border-color: var(--accent);
295+
background: var(--accent-subtle);
296+
}
297+
298+
.home-admin-badge {
299+
margin-right: 5px;
300+
}
301+
302+
.home-admin-grid {
303+
display: grid;
304+
gap: 8px;
305+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
306+
}
307+
308+
.home-admin-card {
309+
background: var(--bg-elevated);
310+
border: 1px solid var(--border);
311+
border-radius: 10px;
312+
padding: 10px 12px;
313+
}
314+
315+
.home-admin-card-label {
316+
margin: 0;
317+
font-size: 11px;
318+
font-weight: 700;
319+
text-transform: uppercase;
320+
letter-spacing: 0.07em;
321+
color: var(--text-muted);
322+
}
323+
324+
.home-admin-card-value {
325+
margin: 6px 0 0;
326+
font-size: 22px;
327+
font-weight: 800;
328+
color: var(--text);
329+
}
330+
331+
.home-admin-card-ok {
332+
color: var(--ok);
333+
}
334+
292335
@media (max-width: 740px) {
293336
.home-title {
294337
font-size: 24px;

apps/web/src/views/HomeView.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ const MOCK_RECENT_ACTIVITY = [
2020
];
2121

2222
interface HomeViewProps {
23+
isAdmin: boolean;
2324
onOpenSettings: () => void;
25+
onNavigateAdmin: () => void;
2426
}
2527

26-
export function HomeView({ onOpenSettings }: HomeViewProps) {
28+
export function HomeView({ isAdmin, onOpenSettings, onNavigateAdmin }: HomeViewProps) {
2729
const hasModules = MOCK_INSTALLED_MODULES.length > 0;
2830

2931
return (
@@ -46,6 +48,34 @@ export function HomeView({ onOpenSettings }: HomeViewProps) {
4648
</div>
4749
</div>
4850

51+
{isAdmin && (
52+
<section className="home-section home-admin-banner" aria-labelledby="home-admin-title">
53+
<div className="home-section-head">
54+
<h2 className="home-section-title" id="home-admin-title">
55+
<span className="home-admin-badge" aria-hidden="true"></span>
56+
Admin Overview
57+
</h2>
58+
<Button size="sm" type="button" onClick={onNavigateAdmin}>
59+
Admin 패널 →
60+
</Button>
61+
</div>
62+
<div className="home-admin-grid">
63+
<div className="home-admin-card">
64+
<p className="home-admin-card-label">활성 사용자</p>
65+
<p className="home-admin-card-value">1</p>
66+
</div>
67+
<div className="home-admin-card">
68+
<p className="home-admin-card-label">설치된 모듈</p>
69+
<p className="home-admin-card-value">{MOCK_INSTALLED_MODULES.length}</p>
70+
</div>
71+
<div className="home-admin-card">
72+
<p className="home-admin-card-label">시스템 상태</p>
73+
<p className="home-admin-card-value home-admin-card-ok">정상</p>
74+
</div>
75+
</div>
76+
</section>
77+
)}
78+
4979
{hasModules ? (
5080
<div className="home-stat-grid">
5181
<article className="home-stat-card">

docs/v2_FINANCIAL-LEDGER/roadmap/01-development-plan.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ Control 전체 목록과 상태 관리는 별도 문서에서 관리:
190190

191191
- [x] Home 화면 정보 구조 확정 (요약 영역, 빠른 액션, 최근 활동) - shell
192192
- [x] 모듈 0개 상태 Empty UX 구현 (안내 + 다음 행동 CTA)
193-
- [ ] 관리자/일반 사용자 홈 표시 정책 분리
193+
- [x] 관리자/일반 사용자 홈 표시 정책 분리
194194
- [x] 글로벌 네비게이션 진입점 확정 (설정, 모듈 관리, 로그아웃) - shell
195195
- [x] 글로벌 네비게이션 Marketplace 진입점 추가 (사이드바 Workspace 섹션)
196196
- [ ] 글로벌 네비게이션 상세 규격 확정 (메뉴 순서, 아이콘, 모바일 Drawer, 키보드 탐색)
@@ -208,7 +208,7 @@ Control 전체 목록과 상태 관리는 별도 문서에서 관리:
208208
- [x] 관리자 PIN Step-up 모달 흐름 구현 (isAdmin/isPinVerified 분리, 비관리자 진입점 숨김)
209209
- [x] Protected Route 정책 구현 (권한 부족 시 리다이렉트) - shell
210210
- [ ] 관리자 PIN 관리 UI 구현 (최초 설정/변경/오류 처리)
211-
- [ ] 관리자 세션 만료 UX 구현 (30분 만료 시 재인증 모달)
211+
- [x] 관리자 세션 만료 UX 구현 (30분 만료 시 재인증 모달)
212212
- [x] 일반 설정 저장 UX 보강 (저장 성공/실패, 미저장 변경 경고)
213213

214214
> 변경 감지(dirty state) 구현 — 변경 없으면 저장 버튼 비활성화. 저장 클릭 시 로딩(500ms) 후 모달 닫힘 + notice 표시. 미저장 상태에서 닫기 시 브라우저 기본 confirm 창으로 경고.
@@ -324,6 +324,7 @@ Control 전체 목록과 상태 관리는 별도 문서에서 관리:
324324
| 2026-04-12 | 1차 UI/UX 전면 개편. 다크 모드 디자인 토큰 시스템 구축 및 고정 220px 좌측 사이드바 레이아웃으로 재설계. AppShell A/B/C/D 변형 폐기 후 단일 Shell로 통합. 로그인/홈/설정/관리자 CSS 전체를 다크 토큰 기반으로 전환 |
325325
| 2026-04-12 | 임시 비밀번호 첫 로그인 강제 변경 화면(ChangePasswordView) 구현. 관리자 역할(isAdmin)과 PIN 인증(isPinVerified) 상태 분리 — 역할 보유자도 Admin 페이지 진입 시 PIN 재인증 필요. 비관리자 Admin 진입점 사이드바에서 숨김. Marketplace 사이드바 진입점 추가(Phase 3 플레이스홀더). @fieldstack/core ESM 빌드 전환 |
326326
| 2026-04-13 | P0/P0.5 Control 전 항목 `packages/controls`에 React 컴포넌트 구현 완료 (`ready: true`). `controls.css` 작성(fs- 접두사). `global.css` 토큰을 라이트 기본값 + 다크 오버라이드(`[data-theme]`/`prefers-color-scheme`) 구조로 재설계. Settings 테마 셀렉터 실제 동작 연결 (localStorage + data-theme 적용). |
327+
| 2026-04-15 | 관리자/사용자 홈 분리 완료. HomeView에 `isAdmin` prop 추가 — 관리자 로그인 시 Admin Overview 배너(활성 사용자·설치 모듈·시스템 상태 카드) 표시, Admin 패널 바로가기 버튼 제공. 관리자 PIN 세션 30분 자동 만료 구현 — `pinVerifiedAt` 타임스탬프 기반 setTimeout, 만료 시 notice 표시 및 AdminView 재인증 게이트 자동 복원. |
327328
| 2026-04-15 | Phase 1.5.5 일반 설정 저장 UX 완료. SettingsView dirty state 감지, 저장 로딩 피드백, 미저장 변경 시 브라우저 confirm 경고 구현. |
328329
| 2026-04-14 | Phase 1.5.3 로그인 UX 개선 완료. 로그인 실패/잠금/세션 만료 UX 구현 (인라인 에러 텍스트, 5회 잠금 Alert, 30분 잠금). `ForgotPasswordView` 전면 재구성 — 경로 선택(이메일/관리자 토큰) → 각 경로별 단계 흐름 구현. 관리자 토큰 경로에 이메일+토큰 쌍 검증 구조 추가. Mock 계정 시스템 도입 (admin@/user@ 세트, 로그인 시 역할 자동 적용). `@fieldstack/core/browser` 브라우저 전용 엔트리포인트 분리 — Vite 번들링 시 Node.js 전용 패키지(jsonwebtoken 등) 포함 문제 해결. |
329330

0 commit comments

Comments
 (0)