@@ -634,16 +634,80 @@ function CompleteStep({ onGoLogin }: { onGoLogin: () => void }) {
634634 ) ;
635635}
636636
637+ // ─── 새로고침 복구 ────────────────────────────────────────────────────────────
638+
639+ const RECOVERY_KEY = 'fs_setup_recovery' ;
640+
641+ interface RecoveryData {
642+ entries : ProgressEntry [ ] ;
643+ hasError : boolean ;
644+ }
645+
646+ function saveRecovery ( entries : ProgressEntry [ ] , hasError : boolean ) {
647+ try {
648+ sessionStorage . setItem ( RECOVERY_KEY , JSON . stringify ( { entries, hasError } satisfies RecoveryData ) ) ;
649+ } catch { /* ignore */ }
650+ }
651+
652+ function loadRecovery ( ) : RecoveryData | null {
653+ try {
654+ const raw = sessionStorage . getItem ( RECOVERY_KEY ) ;
655+ if ( ! raw ) return null ;
656+ return JSON . parse ( raw ) as RecoveryData ;
657+ } catch { return null ; }
658+ }
659+
660+ function clearRecovery ( ) {
661+ try { sessionStorage . removeItem ( RECOVERY_KEY ) ; } catch { /* ignore */ }
662+ }
663+
637664// ─── Root Wizard ──────────────────────────────────────────────────────────────
638665
639666export function SetupWizardView ( { onComplete } : { onComplete : ( ) => void } ) {
640- const [ step , setStep ] = useState < WizardStep > ( "welcome" ) ;
667+ // 새로고침 시 progress 단계 복원
668+ const recovered = loadRecovery ( ) ;
669+
670+ const [ step , setStep ] = useState < WizardStep > ( recovered ? "progress" : "welcome" ) ;
641671 const [ formData , setFormData ] = useState < ConfigFormData | null > ( null ) ;
642- const [ progressEntries , setProgressEntries ] = useState < ProgressEntry [ ] > ( [ ] ) ;
643- const [ hasError , setHasError ] = useState ( false ) ;
672+ const [ progressEntries , setProgressEntries ] = useState < ProgressEntry [ ] > ( recovered ?. entries ?? [ ] ) ;
673+ const [ hasError , setHasError ] = useState ( recovered ?. hasError ?? false ) ;
674+ const [ isRestored , setIsRestored ] = useState ( recovered !== null ) ;
675+
676+ // 복원된 상태: /setup/status 폴링 → 서버가 이미 완료했으면 complete로 이동
677+ useEffect ( ( ) => {
678+ if ( ! isRestored ) return ;
679+ let cancelled = false ;
680+
681+ const poll = async ( ) => {
682+ for ( let i = 0 ; i < 8 && ! cancelled ; i ++ ) {
683+ try {
684+ const res = await fetch ( '/setup/status' ) ;
685+ const json = await res . json ( ) as { success : boolean ; data ?: { installed ?: boolean } } ;
686+ if ( json . data ?. installed === true ) {
687+ clearRecovery ( ) ;
688+ setStep ( 'complete' ) ;
689+ return ;
690+ }
691+ } catch { /* API 아직 재시작 중 */ }
692+ await new Promise < void > ( ( r ) => setTimeout ( r , 1500 ) ) ;
693+ }
694+ // 폴링 종료 — 설치 상태 미확정: 복구 배너 유지, 유저가 재시도 가능
695+ } ;
696+
697+ void poll ( ) ;
698+ return ( ) => { cancelled = true ; } ;
699+ } , [ isRestored ] ) ;
700+
701+ // progress 단계의 상태가 바뀔 때마다 sessionStorage에 저장
702+ useEffect ( ( ) => {
703+ if ( step === 'progress' ) {
704+ saveRecovery ( progressEntries , hasError ) ;
705+ }
706+ } , [ step , progressEntries , hasError ] ) ;
644707
645708 const handleConfigNext = ( data : ConfigFormData ) => {
646709 setFormData ( data ) ;
710+ setIsRestored ( false ) ;
647711 setStep ( "progress" ) ;
648712 void runSetup ( data ) ;
649713 } ;
@@ -710,7 +774,8 @@ export function SetupWizardView({ onComplete }: { onComplete: () => void }) {
710774 }
711775 }
712776
713- // 성공 완료 — 잠시 후 complete step
777+ // 성공 완료 — 복구 데이터 제거 후 complete step으로 이동
778+ clearRecovery ( ) ;
714779 setTimeout ( ( ) => setStep ( "complete" ) , 1000 ) ;
715780 } catch ( e : unknown ) {
716781 setProgressEntries ( ( prev ) => [
@@ -748,16 +813,24 @@ export function SetupWizardView({ onComplete }: { onComplete: () => void }) {
748813 ) }
749814 { step === "progress" && (
750815 < >
816+ { /* 새로고침/재접속 복구 배너 */ }
817+ { isRestored && (
818+ < div className = "setup-alert warn" style = { { marginBottom : 16 } } >
819+ < strong > 재접속 감지됨</ strong > — 이전 설치 진행 상태를 복원했습니다.
820+ 서버가 아직 설치 중이라면 자동으로 완료 단계로 이동합니다.
821+ 설치가 중단된 경우 아래 버튼으로 다시 시도할 수 있습니다.
822+ </ div >
823+ ) }
751824 < ProgressStep entries = { progressEntries } hasError = { hasError } />
752- { hasError && (
825+ { ( hasError || isRestored ) && (
753826 < div className = "setup-footer" >
754827 < button
755828 className = "setup-btn secondary"
756- onClick = { ( ) => setStep ( "config" ) }
829+ onClick = { ( ) => { clearRecovery ( ) ; setIsRestored ( false ) ; setStep ( "config" ) ; } }
757830 >
758831 ← 구성으로 돌아가기
759832 </ button >
760- { formData && (
833+ { formData && ! isRestored && (
761834 < button
762835 className = "setup-btn primary"
763836 onClick = { ( ) => { void runSetup ( formData ) ; } }
0 commit comments