# localhost:5000 로그인 화면 호출 흐름 이 문서는 브라우저에서 `http://localhost:5000/`로 접근했을 때 UserFront 로그인 화면이 뜨기까지의 순차 흐름을 정리합니다. 작성 기준: - 로컬 URL: `http://localhost:5000/` - gateway: `baron_gateway` - UserFront upstream: `baron_userfront:5000` - 로그인 화면 구현 파일: `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` ## 1. 결론 로그인 화면의 대표 URL은 다음입니다. - 한국어: `http://localhost:5000/ko/signin` - 영어: `http://localhost:5000/en/signin` 로그인 화면의 실제 소스 파일 절대경로는 다음입니다. ```text /mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart ``` `http://localhost:5000/`로 접근하면 gateway가 UserFront 정적 앱을 내려주고, Flutter 앱이 기동된 뒤 `GoRouter`가 locale과 세션 상태를 판단하여 미로그인 사용자를 `/{locale}/signin`으로 보냅니다. ## 2. 전체 순차 흐름 ### 2.1 브라우저 최초 요청 1. 브라우저가 다음 URL을 요청합니다. ```text GET http://localhost:5000/ ``` 2. 요청은 Docker host port `5000`으로 들어오고 `baron_gateway` Nginx가 받습니다. 관련 파일: ```text /mnt/e/h_workspace/baron-sso/gateway/nginx.conf ``` 관련 설정: ```nginx server { listen 5000; set $backend_upstream http://baron_backend:3000; set $userfront_upstream http://baron_userfront:5000; set $oathkeeper_upstream http://oathkeeper:4455; location / { proxy_pass $userfront_upstream; } } ``` 3. `/`는 `location /`에 매칭되고 `baron_userfront:5000`으로 proxy됩니다. ```text browser -> localhost:5000 -> baron_gateway -> baron_userfront:5000 ``` ### 2.2 UserFront 정적 bootstrap 응답 4. UserFront가 Flutter Web entry HTML을 반환합니다. 관련 파일: ```text /mnt/e/h_workspace/baron-sso/userfront/web/index.html ``` 5. `index.html`은 처음에 bootstrap shell을 보여줍니다. 이 시점에 보이는 것은 실제 Flutter 로그인 화면이 아니라 로딩용 HTML입니다. 주요 DOM: ```html

Baron SW Portal

Loading sign-in

``` 6. `index.html`은 로컬 개발 환경에서 기존 service worker와 Flutter cache를 제거한 뒤 `flutter_bootstrap.js`를 로드합니다. 사용 함수/동작: - `loadFlutter()` - `navigator.serviceWorker.getRegistrations()` - `registration.unregister()` - `caches.keys()` - `caches.delete()` 이후 브라우저는 일반적으로 다음 정적 리소스를 가져옵니다. ```text GET /flutter_bootstrap.js GET /manifest.json GET /main.dart.mjs GET /canvaskit/skwasm.js GET /canvaskit/skwasm.wasm GET /favicon.ico ``` ### 2.3 Flutter 앱 시작 7. Flutter runtime이 `main.dart`의 `main()`을 실행합니다. 관련 파일: ```text /mnt/e/h_workspace/baron-sso/userfront/lib/main.dart ``` 주요 함수: - `main()` - `WidgetsFlutterBinding.ensureInitialized()` - `usePathUrlStrategy()` - `EasyLocalization.ensureInitialized()` - `LocaleRegistry.primeWithDefaults()` - `LoggerService.init()` - `runApp(...)` 8. `runApp()`은 다음 wrapper를 구성합니다. ```text EasyLocalization -> ProviderScope -> BaronSSOApp ``` 9. `BaronSSOApp.build()`가 `MaterialApp.router`를 생성하고 `_router`를 연결합니다. 주요 함수/객체: - `BaronSSOApp` - `_BaronSSOAppState.build()` - `MaterialApp.router(...)` - `routerConfig: _router` ## 3. GoRouter 라우팅 흐름 라우터는 `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart`에 정의되어 있습니다. 주요 객체: ```dart final _router = GoRouter( initialLocation: '/', refreshListenable: AuthNotifier.instance, routes: [...], redirect: (context, state) { ... }, ); ``` ### 3.1 locale 결정 `http://localhost:5000/`에는 locale path segment가 없습니다. 따라서 앱은 선호 locale을 계산합니다. 사용 함수: - `extractLocaleFromPath(Uri uri)` - `resolvePreferredLocaleCode()` - `normalizeLocaleCode(String? code)` - `buildLocalizedPath(String localeCode, Uri uri)` - `buildLocalizedHomePath(Uri uri, {String? preferredLocaleCode})` 관련 파일: ```text /mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart ``` locale 결정 기준: 1. 저장된 locale이 있으면 사용 2. 없으면 브라우저/디바이스 locale 사용 3. 지원하지 않는 값이면 fallback locale 사용 따라서 한국어 환경에서는 보통 `ko`, 영어 환경에서는 `en`이 됩니다. ### 3.2 세션 상태 확인 라우터와 앱 초기화 코드는 로컬 토큰 또는 쿠키 기반 세션 여부를 봅니다. 사용 함수: - `_hasActiveLocalSession()` - `AuthTokenStore.getToken()` - `AuthTokenStore.usesCookie()` - `_shouldRunStartupSessionRecovery(Uri uri)` - `_silentSessionRecovery()` - `AuthProxyService.getMe()` - `AuthProxyService.getSessionStatus()` 관련 파일: ```text /mnt/e/h_workspace/baron-sso/userfront/lib/main.dart /mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart /mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart ``` 세션 확인 시 호출될 수 있는 backend API: ```text GET /api/v1/user/me ``` gateway 기준 흐름: ```text browser -> /api/v1/user/me -> baron_gateway location /api -> baron_backend:3000 ``` ### 3.3 미로그인 상태에서 최종 로그인 route로 이동 미로그인 상태라면 최종적으로 다음 경로로 이동합니다. ```text /{locale}/signin ``` 예: ```text http://localhost:5000/ko/signin http://localhost:5000/en/signin ``` 관련 redirect 함수: - root route redirect: `buildLocalizedHomePath(...)` - global router redirect: `redirect: (context, state) { ... }` - private route guard: `_redirectPrivateLocaleRoute(...)` - locale entry fallback: `LocaleEntryRedirectScreen._redirect()` - signin path builder: `buildSigninRedirectPath(...)` 중요 분기: - locale이 없으면 locale이 붙은 path로 보정합니다. - public auth path는 그대로 통과합니다. - 로그인하지 않았고 private path 또는 locale root에 있으면 `/{locale}/signin`으로 보냅니다. - 로그인되어 있고 locale root에 있으면 `/{locale}/dashboard`로 보냅니다. ## 4. 로그인 화면 route와 화면 생성 로그인 화면 route는 `main.dart`의 `/:locale/signin`입니다. 브라우저 URL: ```text http://localhost:5000/ko/signin ``` 라우터 path: ```text /:locale/signin ``` 관련 코드 구조: ```dart GoRoute( path: 'signin', builder: (context, state) { final loginChallenge = state.uri.queryParameters['login_challenge']; final redirectUrl = state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_url']; return ScopedTheme( controller: ThemeController.auth, child: LoginScreen( key: state.pageKey, loginChallenge: loginChallenge, redirectUrl: redirectUrl, ), ); }, ) ``` 생성되는 widget: ```text ScopedTheme -> LoginScreen ``` 로그인 화면 구현 파일: ```text /mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart ``` 주요 class/function: - `LoginScreen` - `_LoginScreenState` - `_LoginScreenState.initState()` - `_LoginScreenState.build()` - `_handlePasswordLogin()` - `_handleLinkLogin()` - `_startEnchantedFlow()` - `_pollForSession()` - `_startQrFlow()` - `_startQrPolling()` - `_tryCookieSession()` - `_attemptOidcAutoAccept()` - `_acceptOidcLoginAndRedirect()` - `_onLoginSuccess()` ## 5. LoginScreen 초기화 흐름 `LoginScreen`이 생성되면 `_LoginScreenState.initState()`가 실행됩니다. 초기화 순서: 1. `TabController(length: 3)` 생성 2. password/link/QR 입력 controller 준비 3. `drySend` query parameter 확인 4. `redirect_uri` 또는 `redirect_url` query parameter 확인 5. `login_challenge` 확인 6. magic link, short code, verification token이 있는지 확인 7. QR 로그인 필요 안내 notice 확인 8. OIDC login challenge가 있으면 자동 accept 시도 9. tenant-info 조회로 login ID label 커스터마이징 10. login challenge가 없으면 쿠키 세션 확인 초기 진입이 단순 `http://localhost:5000/`인 경우 보통 query parameter가 없으므로 다음 정도가 수행됩니다. ```text LoginScreen.initState() -> _resolveLoginChallenge() -> _attemptOidcAutoAccept() // login_challenge 없으면 바로 return -> AuthProxyService.getTenantInfo() -> _tryCookieSession() ``` 초기 화면 렌더링에 영향을 줄 수 있는 backend API: ```text GET /api/v1/auth/tenant-info GET /api/v1/user/me ``` ## 6. 로그인 화면 UI 구성 `LoginScreen.build()`는 탭 기반 로그인 UI를 구성합니다. 탭: ```text 1. 비밀번호 2. 로그인 링크 3. QR 코드 ``` 관련 UI 코드: - `TabBar` - `TabBarView` - password tab - link tab - QR tab ### 6.1 비밀번호 탭 UI 요소: - 로그인 ID 입력 - 비밀번호 입력 - 로그인 버튼 주요 key: ```text password_login_id_input password_login_password_input password_login_submit_button ``` 사용 함수: - `_handlePasswordLogin()` - `AuthProxyService.loginWithPassword(...)` - `_onLoginSuccess(...)` 호출 API: ```text POST /api/v1/auth/password/login ``` ### 6.2 로그인 링크 탭 UI 요소: - 로그인 ID 입력 - 로그인 링크 전송 버튼 - short code 입력 UI - 재전송 버튼 사용 함수: - `_handleLinkLogin()` - `_startEnchantedFlow(...)` - `_pollForSession(...)` - `_verifyShortCode(...)` - `AuthProxyService.initEnchantedLink(...)` - `AuthProxyService.pollEnchantedLink(...)` - `AuthProxyService.verifyLoginShortCode(...)` 호출 API: ```text POST /api/v1/auth/enchanted-link/init POST /api/v1/auth/enchanted-link/poll POST /api/v1/auth/login/code/verify-short ``` ### 6.3 QR 코드 탭 QR 탭은 선택될 때 QR flow를 시작합니다. 사용 함수: - `_handleTabSelection()` - `_startQrFlow()` - `_startQrPolling()` - `_startCountdown()` - `AuthProxyService.initQrLogin()` - `AuthProxyService.pollQrStatus(...)` 호출 API: ```text POST /api/v1/auth/qr/init POST /api/v1/auth/qr/poll ``` ## 7. OIDC login_challenge가 있는 경우 외부 RP 또는 Hydra 흐름에서 다음과 같은 URL로 들어올 수 있습니다. ```text http://localhost:5000/ko/signin?login_challenge=... ``` 이 경우 `LoginScreen`은 `login_challenge`를 보관하고 다음을 시도합니다. 1. `_attemptOidcAutoAccept()` 2. 로컬 token 또는 cookie session이 있으면 backend에 login accept 요청 3. backend가 반환한 `redirectTo`로 브라우저 이동 사용 함수: - `_resolveLoginChallenge(...)` - `_attemptOidcAutoAccept()` - `_acceptOidcLoginAndRedirect(...)` - `_redirectToOidcTarget(...)` - `AuthProxyService.acceptOidcLogin(...)` 호출 API: ```text POST /api/v1/auth/oidc/login/accept ``` 로그인 화면에서 사용자가 비밀번호 로그인을 완료한 뒤에도 `_onLoginSuccess()`가 `login_challenge`를 감지하면 같은 accept 흐름을 수행합니다. ## 8. 페이지/route 목록 UserFront auth 관련 route는 locale 아래에 붙습니다. | 브라우저 절대 URL 예시 | GoRouter path | 생성 화면 | 구현 파일 | | --- | --- | --- | --- | | `http://localhost:5000/ko/signin` | `/:locale/signin` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` | | `http://localhost:5000/ko/login` | `/:locale/login` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` | | `http://localhost:5000/ko/signup` | `/:locale/signup` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` | | `http://localhost:5000/ko/registration` | `/:locale/registration` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` | | `http://localhost:5000/ko/consent?consent_challenge=...` | `/:locale/consent` | `ConsentScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/consent_screen.dart` | | `http://localhost:5000/ko/forgot-password` | `/:locale/forgot-password` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` | | `http://localhost:5000/ko/recovery` | `/:locale/recovery` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` | | `http://localhost:5000/ko/reset-password` | `/:locale/reset-password` | `ResetPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/reset_password_screen.dart` | | `http://localhost:5000/ko/error` | `/:locale/error` | `ErrorScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/error_screen.dart` | | `http://localhost:5000/ko/dashboard` | `/:locale/dashboard` | `DashboardScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/dashboard/presentation/dashboard_screen.dart` | ## 9. 가장 중요한 파일 요약 | 목적 | 파일 절대경로 | | --- | --- | | gateway proxy 설정 | `/mnt/e/h_workspace/baron-sso/gateway/nginx.conf` | | Flutter Web HTML bootstrap | `/mnt/e/h_workspace/baron-sso/userfront/web/index.html` | | 앱 시작점과 GoRouter 정의 | `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart` | | 로그인 화면 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` | | 로그인/API proxy client | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart` | | token/cookie session store adapter | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart` | | locale path helper | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart` | | public auth path 판정 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/domain/login_link_route_policy.dart` | ## 10. 짧은 호출 흐름 다이어그램 ```mermaid sequenceDiagram participant B as Browser participant G as baron_gateway
localhost:5000 participant UF as baron_userfront:5000 participant FE as Flutter UserFront participant BE as baron_backend:3000 B->>G: GET / G->>UF: proxy GET / UF-->>G: index.html G-->>B: index.html B->>G: GET /flutter_bootstrap.js G->>UF: proxy static asset B->>G: GET /main.dart.mjs 등 G->>UF: proxy static assets FE->>FE: main() FE->>FE: MaterialApp.router(_router) FE->>FE: locale 결정 FE->>FE: local token/cookie 상태 확인 alt 미로그인 FE->>FE: route -> /{locale}/signin FE->>BE: GET /api/v1/auth/tenant-info FE->>BE: GET /api/v1/user/me (cookie/session check 가능) FE->>B: LoginScreen 렌더링 else 로그인됨 FE->>FE: route -> /{locale}/dashboard end ``` ## 11. 디버깅 포인트 로그인 화면이 뜨지 않을 때 먼저 확인할 것: 1. `baron_gateway`가 떠 있는지 2. `baron_userfront`가 healthy인지 3. gateway 로그에 `baron_userfront could not be resolved` 또는 `connect() failed`가 있는지 4. 브라우저 Network 탭에서 `/flutter_bootstrap.js`, `/main.dart.mjs`가 200인지 5. Flutter route가 `/ko/signin` 또는 `/en/signin`으로 이동하는지 6. `/api/v1/auth/tenant-info`, `/api/v1/user/me` 실패가 화면 렌더링을 막는지