Skip to content

Commit f938009

Browse files
committed
feat: 로그인 이후 웹 UI/UX를 새 앱 셸로 전면 개편
페이지 전환과 설정/관리 동선을 명확히 분리해 메인 화면의 역할을 허브 중심으로 재정의한다.
1 parent 983564c commit f938009

12 files changed

Lines changed: 1846 additions & 755 deletions

File tree

apps/web/src/app.css

Lines changed: 14 additions & 577 deletions
Large diffs are not rendered by default.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { ReactNode } from "react";
2+
3+
import "../styles/shell.css";
4+
5+
export type RouteKey = "login" | "home" | "admin";
6+
7+
interface AppShellProps {
8+
installMode: "normal" | "bypass";
9+
route: RouteKey;
10+
isAdmin: boolean;
11+
notice: string;
12+
onNavigate: (route: RouteKey) => void;
13+
onLogout: () => void;
14+
onOpenSettings: () => void;
15+
children: ReactNode;
16+
}
17+
18+
export function AppShell({
19+
installMode,
20+
route,
21+
isAdmin,
22+
notice,
23+
onNavigate,
24+
onLogout,
25+
onOpenSettings,
26+
children,
27+
}: AppShellProps) {
28+
const currentTime = new Date().toLocaleTimeString("ko-KR", {
29+
hour: "2-digit",
30+
minute: "2-digit",
31+
});
32+
33+
return (
34+
<main className="frame">
35+
<header className="shell-header">
36+
<div className="shell-brand-wrap">
37+
<div className="shell-logo">FS</div>
38+
<div>
39+
<p className="shell-brand">Fieldstack Control Plane</p>
40+
<p className="shell-subbrand">Personal modular workspace</p>
41+
</div>
42+
</div>
43+
44+
<div className="shell-header-right">
45+
<span className="shell-time">
46+
{currentTime}
47+
</span>
48+
<span className={`badge ${installMode === "bypass" ? "badge-danger" : "badge-soft"}`}>
49+
{installMode === "bypass" ? "DEV BYPASS" : "NORMAL MODE"}
50+
</span>
51+
</div>
52+
</header>
53+
54+
{notice ? <p className="shell-notice">{notice}</p> : null}
55+
56+
<section className="shell-layout">
57+
<aside className="shell-side" aria-label="Core navigation">
58+
<div className="shell-side-block">
59+
<p className="shell-side-title">Workspace</p>
60+
<ul className="shell-nav-list">
61+
<li>
62+
<button
63+
className="shell-nav-button"
64+
type="button"
65+
aria-current={route === "home" ? "page" : undefined}
66+
onClick={() => onNavigate("home")}
67+
>
68+
Home Hub
69+
</button>
70+
</li>
71+
<li>
72+
<button className="shell-nav-button" type="button" onClick={onOpenSettings}>
73+
General Settings
74+
</button>
75+
</li>
76+
</ul>
77+
</div>
78+
79+
<div className="shell-side-block">
80+
<p className="shell-side-title">Operations</p>
81+
<ul className="shell-nav-list">
82+
{isAdmin ? (
83+
<li>
84+
<button
85+
className="shell-nav-button"
86+
type="button"
87+
aria-current={route === "admin" ? "page" : undefined}
88+
onClick={() => onNavigate("admin")}
89+
>
90+
Admin Console
91+
</button>
92+
</li>
93+
) : null}
94+
<li>
95+
<button className="shell-nav-button" type="button" onClick={onLogout}>
96+
Sign Out
97+
</button>
98+
</li>
99+
</ul>
100+
</div>
101+
102+
<div className="shell-health">
103+
<p className="shell-health-title">System Snapshot</p>
104+
<p className="shell-health-line">Core: Online</p>
105+
<p className="shell-health-line">Modules: 0 active</p>
106+
<p className="shell-health-line">Auth: Session valid</p>
107+
</div>
108+
</aside>
109+
110+
<section className="shell-content">{children}</section>
111+
</section>
112+
</main>
113+
);
114+
}

apps/web/src/main.tsx

Lines changed: 56 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
11
import { FormEvent, useEffect, useMemo, useState } from "react";
22
import { createRoot } from "react-dom/client";
33

4-
import "./app.css";
5-
import { HomeView, type HomeViewState } from "./views/HomeView";
4+
import "./styles/global.css";
5+
import "./styles/login.css";
6+
7+
import { AppShell, type RouteKey } from "./components/AppShell";
8+
import { HomeView } from "./views/HomeView";
69
import { LoginView } from "./views/LoginView";
10+
import { SettingsView } from "./views/SettingsView";
11+
import { AdminView } from "./views/AdminView";
712

13+
// ─── Types ────────────────────────────────────────────────────
814
type InstallMode = "normal" | "bypass";
9-
type RouteKey = "login" | "home" | "settings" | "admin";
1015

1116
interface WebRuntimeEnv {
1217
MODE?: string;
1318
DEV?: boolean;
1419
VITE_INSTALL_MODE?: string;
1520
}
1621

22+
// ─── Helpers ──────────────────────────────────────────────────
1723
const WEB_BOOTSTRAP_MESSAGE = "Fieldstack Web bootstrap initialized";
1824

1925
function resolveInstallMode(runtimeEnv: WebRuntimeEnv): InstallMode {
2026
const requestedMode = runtimeEnv.VITE_INSTALL_MODE;
2127
const isDevelopment = runtimeEnv.DEV === true || runtimeEnv.MODE === "development";
2228

2329
if (requestedMode === "bypass") {
24-
if (isDevelopment) {
25-
return "bypass";
26-
}
27-
30+
if (isDevelopment) return "bypass";
2831
console.warn("[fieldstack][web] VITE_INSTALL_MODE=bypass ignored outside development");
2932
}
3033

@@ -33,54 +36,42 @@ function resolveInstallMode(runtimeEnv: WebRuntimeEnv): InstallMode {
3336

3437
function getRouteFromHash(rawHash: string): RouteKey {
3538
const hash = rawHash.replace("#", "");
36-
if (hash === "home" || hash === "settings" || hash === "admin" || hash === "login") {
39+
if (hash === "settings") {
40+
return "home";
41+
}
42+
if (hash === "home" || hash === "admin" || hash === "login") {
3743
return hash;
3844
}
3945
return "login";
4046
}
4147

4248
function canAccessRoute(route: RouteKey, isAuthenticated: boolean, isAdmin: boolean): boolean {
43-
if (route === "login") {
44-
return true;
45-
}
46-
47-
if (!isAuthenticated) {
48-
return false;
49-
}
50-
51-
if (route === "admin") {
52-
return isAdmin;
53-
}
54-
49+
if (route === "login") return true;
50+
if (!isAuthenticated) return false;
51+
if (route === "admin") return isAdmin;
5552
return true;
5653
}
5754

55+
// ─── App Root ─────────────────────────────────────────────────
5856
function App({ installMode }: { installMode: InstallMode }) {
5957
const [isAuthenticated, setIsAuthenticated] = useState(false);
6058
const [isAdmin, setIsAdmin] = useState(false);
61-
const [homeState, setHomeState] = useState<HomeViewState>("ready");
59+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
6260
const [notice, setNotice] = useState(
6361
installMode === "bypass"
64-
? "DEV bypass is active. Install is skipped, but auth flow starts from login."
65-
: "Normal mode is active. Install flow and auth integrations are expected.",
62+
? "DEV bypass active — install skipped, auth starts from login."
63+
: "",
6664
);
6765
const [route, setRoute] = useState<RouteKey>(() => getRouteFromHash(window.location.hash));
6866

6967
useEffect(() => {
70-
const handleHashChange = () => {
71-
setRoute(getRouteFromHash(window.location.hash));
72-
};
73-
68+
const handleHashChange = () => setRoute(getRouteFromHash(window.location.hash));
7469
window.addEventListener("hashchange", handleHashChange);
75-
return () => {
76-
window.removeEventListener("hashchange", handleHashChange);
77-
};
70+
return () => window.removeEventListener("hashchange", handleHashChange);
7871
}, []);
7972

8073
const effectiveRoute = useMemo<RouteKey>(() => {
81-
if (canAccessRoute(route, isAuthenticated, isAdmin)) {
82-
return route;
83-
}
74+
if (canAccessRoute(route, isAuthenticated, isAdmin)) return route;
8475
return "login";
8576
}, [isAdmin, isAuthenticated, route]);
8677

@@ -95,6 +86,7 @@ function App({ installMode }: { installMode: InstallMode }) {
9586
window.location.hash = nextRoute;
9687
};
9788

89+
// Auth handlers
9890
const onLogin = (event: FormEvent<HTMLFormElement>) => {
9991
event.preventDefault();
10092
setIsAuthenticated(true);
@@ -104,24 +96,17 @@ function App({ installMode }: { installMode: InstallMode }) {
10496

10597
const onQuickLogin = () => {
10698
setIsAuthenticated(true);
107-
setNotice("Quick login enabled for UI iteration.");
99+
setNotice("");
108100
navigate("home");
109101
};
110102

111103
const onLogout = () => {
112104
setIsAuthenticated(false);
113-
setNotice("Logged out. Re-authenticate to continue.");
105+
setNotice("Logged out.");
114106
navigate("login");
115107
};
116108

117-
const onToggleAdmin = () => {
118-
setIsAdmin((previous) => {
119-
const next = !previous;
120-
setNotice(next ? "Admin authority enabled for testing." : "Admin authority disabled.");
121-
return next;
122-
});
123-
};
124-
109+
// Login page (no shell)
125110
if (effectiveRoute === "login") {
126111
return (
127112
<main className="auth-shell">
@@ -136,109 +121,48 @@ function App({ installMode }: { installMode: InstallMode }) {
136121
);
137122
}
138123

124+
// Authenticated shell
139125
return (
140-
<main className="frame">
141-
<header className="topbar">
142-
<div className="brand">Fieldstack Core Control Plane</div>
143-
<span className={`badge ${installMode === "bypass" ? "badge-danger" : "badge-soft"}`}>
144-
{installMode === "bypass" ? "DEV BYPASS" : "NORMAL MODE"}
145-
</span>
146-
</header>
147-
148-
<p className="notice">{notice}</p>
149-
150-
<section className="layout">
151-
{isAuthenticated ? (
152-
<aside className="nav" aria-label="Core navigation">
153-
<ul className="nav-list">
154-
<li>
155-
<button className="nav-button" type="button" aria-current={effectiveRoute === "home" ? "page" : undefined} onClick={() => navigate("home")}>Home</button>
156-
</li>
157-
<li>
158-
<button className="nav-button" type="button" aria-current={effectiveRoute === "settings" ? "page" : undefined} onClick={() => navigate("settings")}>Settings</button>
159-
</li>
160-
<li>
161-
<button className="nav-button" type="button" aria-current={effectiveRoute === "admin" ? "page" : undefined} onClick={() => navigate("admin")}>Admin</button>
162-
</li>
163-
<li>
164-
<button className="nav-button" type="button" onClick={onLogout}>Logout</button>
165-
</li>
166-
</ul>
167-
</aside>
168-
) : null}
169-
170-
{effectiveRoute === "home" ? (
171-
<HomeView homeState={homeState} onChangeHomeState={setHomeState} />
172-
) : null}
173-
174-
{effectiveRoute === "settings" ? (
175-
<section className="panel" aria-labelledby="settings-title">
176-
<h1 className="title" id="settings-title">Settings</h1>
177-
<p className="subtitle">일반 설정 저장 UX와 관리자 권한 토글을 함께 점검합니다.</p>
178-
<div className="stack">
179-
<div className="grid">
180-
<label className="field">
181-
<span>표시 이름</span>
182-
<input className="input" type="text" placeholder="Fieldstack Owner" />
183-
</label>
184-
<label className="field">
185-
<span>언어</span>
186-
<select className="select">
187-
<option>한국어</option>
188-
<option>English</option>
189-
</select>
190-
</label>
191-
</div>
192-
<div className="actions">
193-
<button className="button button-primary" type="button" onClick={() => setNotice("Settings saved (mock).")}>저장</button>
194-
<button className="button" type="button" onClick={onToggleAdmin}>{isAdmin ? "관리자 권한 해제" : "관리자 권한 부여"}</button>
195-
</div>
196-
</div>
197-
</section>
198-
) : null}
199-
200-
{effectiveRoute === "admin" ? (
201-
<section className="panel" aria-labelledby="admin-title">
202-
<h1 className="title" id="admin-title">Admin</h1>
203-
{isAdmin ? (
204-
<>
205-
<p className="subtitle">관리자 전용 설정 및 감사 로그 진입점을 검증합니다.</p>
206-
<div className="stack">
207-
<article className="status status-ready">
208-
<h3>PIN Session</h3>
209-
<p>30분 만료 시 재인증 모달을 띄우는 자리입니다.</p>
210-
</article>
211-
<article className="status status-loading">
212-
<h3>Audit Logs</h3>
213-
<p>PIN 실패, 권한 변경, 주요 설정 저장 이력을 보여줄 카드 영역입니다.</p>
214-
</article>
215-
</div>
216-
</>
217-
) : (
218-
<article className="status status-error" role="alert">
219-
<h3>Unauthorized</h3>
220-
<p>관리자 권한이 없어서 접근할 수 없습니다. Settings에서 관리자 권한을 활성화하세요.</p>
221-
</article>
222-
)}
223-
</section>
224-
) : null}
225-
</section>
226-
</main>
126+
<AppShell
127+
installMode={installMode}
128+
route={effectiveRoute}
129+
isAdmin={isAdmin}
130+
notice={notice}
131+
onNavigate={navigate}
132+
onLogout={onLogout}
133+
onOpenSettings={() => setIsSettingsOpen(true)}
134+
>
135+
{effectiveRoute === "home" && <HomeView onOpenSettings={() => setIsSettingsOpen(true)} />}
136+
{effectiveRoute === "admin" && <AdminView isAdmin={isAdmin} />}
137+
{isSettingsOpen && (
138+
<SettingsView
139+
isAdmin={isAdmin}
140+
onClose={() => setIsSettingsOpen(false)}
141+
onToggleAdmin={() => {
142+
setIsAdmin((prev) => {
143+
const next = !prev;
144+
setNotice(next ? "Admin authority enabled (mock)." : "Admin authority disabled.");
145+
return next;
146+
});
147+
}}
148+
onSaved={() => setNotice("Settings saved (mock).")}
149+
/>
150+
)}
151+
</AppShell>
227152
);
228153
}
229154

155+
// ─── Bootstrap ────────────────────────────────────────────────
230156
const runtimeEnv = (import.meta as ImportMeta & { env?: WebRuntimeEnv }).env ?? {};
231157
const installMode = resolveInstallMode(runtimeEnv);
232158

233159
console.log(WEB_BOOTSTRAP_MESSAGE);
234160
console.log(`[fieldstack][web] install mode: ${installMode}`);
235-
236161
if (installMode === "bypass") {
237162
console.warn("[fieldstack][web] DEV INSTALL BYPASS ACTIVE");
238163
}
239164

240165
const appRootElement = document.querySelector<HTMLDivElement>("#app");
241-
242166
if (appRootElement === null) {
243167
throw new Error("App root element '#app' was not found.");
244168
}

0 commit comments

Comments
 (0)