@@ -12,6 +12,8 @@ import { SettingsView } from "./views/SettingsView";
1212import { AdminView } from "./views/AdminView" ;
1313import { MarketplaceView } from "./views/MarketplaceView" ;
1414import { ChangePasswordView } from "./views/ChangePasswordView" ;
15+ import { OtpView } from "./views/OtpView" ;
16+ import { ForgotPasswordView } from "./views/ForgotPasswordView" ;
1517
1618// ─── Types ────────────────────────────────────────────────────
1719type InstallMode = "normal" | "bypass" ;
@@ -40,17 +42,8 @@ function resolveInstallMode(runtimeEnv: WebRuntimeEnv): InstallMode {
4042function getRouteFromHash ( rawHash : string ) : RouteKey {
4143 const hash = rawHash . replace ( "#" , "" ) ;
4244 if ( hash === "settings" ) return "home" ;
43- if ( hash === "home" || hash === "marketplace" || hash === "admin" || hash === "login" || hash === "change-password" ) {
44- return hash ;
45- }
46- return "login" ;
47- }
48-
49- function canAccessRoute ( route : RouteKey , isAuthenticated : boolean ) : boolean {
50- if ( route === "login" ) return true ;
51- if ( ! isAuthenticated ) return false ;
52- // admin 라우트는 인증된 유저라면 진입 허용 — AdminView 내부에서 isAdmin으로 콘텐츠 게이팅
53- return true ;
45+ const valid : RouteKey [ ] = [ "login" , "otp" , "forgot-password" , "home" , "marketplace" , "admin" , "change-password" ] ;
46+ return ( valid as string [ ] ) . includes ( hash ) ? ( hash as RouteKey ) : "login" ;
5447}
5548
5649// ─── Session Storage Keys ─────────────────────────────────────
@@ -73,6 +66,8 @@ function App({ installMode }: { installMode: InstallMode }) {
7366 const [ isPinVerified , setIsPinVerified ] = useState (
7467 ( ) => sessionStorage . getItem ( SS . pinVerified ) === "true" ,
7568 ) ;
69+ // OTP 인증 대기 중인 이메일 (로그인 완료 전 임시 상태 — sessionStorage 미저장)
70+ const [ pendingOtpEmail , setPendingOtpEmail ] = useState < string | null > ( null ) ;
7671 const [ currentUser , setCurrentUser ] = useState < { email : string } | null > (
7772 ( ) => {
7873 const email = sessionStorage . getItem ( SS . email ) ;
@@ -98,11 +93,16 @@ function App({ installMode }: { installMode: InstallMode }) {
9893 } , [ ] ) ;
9994
10095 const effectiveRoute = useMemo < RouteKey > ( ( ) => {
101- if ( ! canAccessRoute ( route , isAuthenticated ) ) return "login" ;
102- // 비밀번호 변경 강제: change-password 이외의 모든 경로 차단
96+ // OTP 대기 중: otp 화면만 허용
97+ if ( pendingOtpEmail ) return "otp" ;
98+ // 미인증: login / forgot-password만 허용
99+ if ( ! isAuthenticated ) {
100+ return route === "forgot-password" ? "forgot-password" : "login" ;
101+ }
102+ // 비밀번호 변경 강제
103103 if ( mustChangePassword && route !== "change-password" ) return "change-password" ;
104104 return route ;
105- } , [ isAuthenticated , mustChangePassword , route ] ) ;
105+ } , [ isAuthenticated , mustChangePassword , pendingOtpEmail , route ] ) ;
106106
107107 useEffect ( ( ) => {
108108 if ( window . location . hash !== `#${ effectiveRoute } ` ) {
@@ -120,8 +120,16 @@ function App({ installMode }: { installMode: InstallMode }) {
120120 event . preventDefault ( ) ;
121121 const formData = new FormData ( event . currentTarget ) ;
122122 const email = ( formData . get ( "email" ) as string | null ) ?? "user@fieldstack.dev" ;
123- // mock: 비밀번호가 "temp1234"이면 임시 비번 첫 로그인으로 처리
124123 const password = formData . get ( "password" ) as string | null ;
124+
125+ // mock: "otp1234" → 2FA OTP 화면으로 이동
126+ if ( password === "otp1234" ) {
127+ setPendingOtpEmail ( email ) ;
128+ navigate ( "otp" ) ;
129+ return ;
130+ }
131+
132+ // mock: "temp1234" → 임시 비번 첫 로그인 강제 변경
125133 const isTempLogin = password === "temp1234" ;
126134
127135 setIsAuthenticated ( true ) ;
@@ -156,6 +164,23 @@ function App({ installMode }: { installMode: InstallMode }) {
156164 navigate ( "home" ) ;
157165 } ;
158166
167+ const onOtpVerified = ( ) => {
168+ if ( ! pendingOtpEmail ) return ;
169+ const email = pendingOtpEmail ;
170+ setPendingOtpEmail ( null ) ;
171+ setIsAuthenticated ( true ) ;
172+ setCurrentUser ( { email } ) ;
173+ sessionStorage . setItem ( SS . auth , "true" ) ;
174+ sessionStorage . setItem ( SS . email , email ) ;
175+ setNotice ( "2단계 인증 완료." ) ;
176+ navigate ( "home" ) ;
177+ } ;
178+
179+ const onOtpCancel = ( ) => {
180+ setPendingOtpEmail ( null ) ;
181+ navigate ( "login" ) ;
182+ } ;
183+
159184 const onPinVerified = ( ) => {
160185 setIsAdmin ( true ) ;
161186 setIsPinVerified ( true ) ;
@@ -187,13 +212,30 @@ function App({ installMode }: { installMode: InstallMode }) {
187212 < LoginView
188213 onLogin = { onLogin }
189214 onQuickLogin = { onQuickLogin }
215+ onForgotPassword = { ( ) => navigate ( "forgot-password" ) }
190216 showDevBypass = { installMode === "bypass" }
191217 />
192218 </ section >
193219 </ main >
194220 ) ;
195221 }
196222
223+ // 비밀번호 찾기 (no shell)
224+ if ( effectiveRoute === "forgot-password" ) {
225+ return < ForgotPasswordView onBack = { ( ) => navigate ( "login" ) } /> ;
226+ }
227+
228+ // 2FA OTP 인증 (no shell)
229+ if ( effectiveRoute === "otp" ) {
230+ return (
231+ < OtpView
232+ email = { pendingOtpEmail ?? "" }
233+ onVerified = { onOtpVerified }
234+ onCancel = { onOtpCancel }
235+ />
236+ ) ;
237+ }
238+
197239 // 비밀번호 강제 변경 (shell 없이 전체 화면)
198240 if ( effectiveRoute === "change-password" ) {
199241 return (
0 commit comments