11import { FormEvent , useEffect , useMemo , useState } from "react" ;
22import { 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" ;
69import { LoginView } from "./views/LoginView" ;
10+ import { SettingsView } from "./views/SettingsView" ;
11+ import { AdminView } from "./views/AdminView" ;
712
13+ // ─── Types ────────────────────────────────────────────────────
814type InstallMode = "normal" | "bypass" ;
9- type RouteKey = "login" | "home" | "settings" | "admin" ;
1015
1116interface WebRuntimeEnv {
1217 MODE ?: string ;
1318 DEV ?: boolean ;
1419 VITE_INSTALL_MODE ?: string ;
1520}
1621
22+ // ─── Helpers ──────────────────────────────────────────────────
1723const WEB_BOOTSTRAP_MESSAGE = "Fieldstack Web bootstrap initialized" ;
1824
1925function 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
3437function 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
4248function 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 ─────────────────────────────────────────────────
5856function 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 ────────────────────────────────────────────────
230156const runtimeEnv = ( import . meta as ImportMeta & { env ?: WebRuntimeEnv } ) . env ?? { } ;
231157const installMode = resolveInstallMode ( runtimeEnv ) ;
232158
233159console . log ( WEB_BOOTSTRAP_MESSAGE ) ;
234160console . log ( `[fieldstack][web] install mode: ${ installMode } ` ) ;
235-
236161if ( installMode === "bypass" ) {
237162 console . warn ( "[fieldstack][web] DEV INSTALL BYPASS ACTIVE" ) ;
238163}
239164
240165const appRootElement = document . querySelector < HTMLDivElement > ( "#app" ) ;
241-
242166if ( appRootElement === null ) {
243167 throw new Error ( "App root element '#app' was not found." ) ;
244168}
0 commit comments