Merge feature/i18n into dev (userfront only)
This commit is contained in:
commit
47193e56ff
55 files changed
+3410
-532
No files matched your search
@@ -0,0 +1,18 @@
|
||||
.git
|
||||
.gitea
|
||||
.codex
|
||||
.env
|
||||
.env.*
|
||||
**/.dart_tool
|
||||
**/.packages
|
||||
**/build
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.next
|
||||
**/.cache
|
||||
**/coverage
|
||||
**/tmp
|
||||
**/logs
|
||||
**/*.log
|
||||
**/*.swp
|
||||
**/.DS_Store
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Code Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_lint:
|
||||
@@ -21,7 +24,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
if: ${{ inputs.run_lint == true }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_lint == true }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -34,7 +37,10 @@ jobs:
|
||||
|
||||
- name: i18n resource check
|
||||
run: |
|
||||
mkdir -p reports
|
||||
node tools/i18n-scanner/index.js
|
||||
node tools/i18n-scanner/report.js
|
||||
cat reports/i18n-report.txt
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -62,7 +68,7 @@ jobs:
|
||||
|
||||
backend-tests:
|
||||
needs: lint
|
||||
if: ${{ inputs.run_backend_tests == true }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_backend_tests == true }}
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
@@ -96,7 +102,7 @@ jobs:
|
||||
|
||||
userfront-tests:
|
||||
needs: lint
|
||||
if: ${{ inputs.run_userfront_tests == true }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_userfront_tests == true }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -113,6 +119,7 @@ jobs:
|
||||
cd userfront
|
||||
if [ -d test ]; then
|
||||
flutter test
|
||||
flutter test --platform chrome test/locale_storage_web_test.dart
|
||||
else
|
||||
echo "No userfront tests: skipping (test/ directory not found)."
|
||||
fi
|
||||
@@ -55,7 +55,7 @@ flowchart
|
||||
- userfront가 바라보는 backend
|
||||
|
||||
### 2. UserFront(Flutter Web/App)
|
||||
- **Framework**: Flutter 3.32+
|
||||
- **Framework**: Flutter 3.38.0+
|
||||
- **Key Packages**: `flutter_riverpod`, `go_router`
|
||||
- **Features**:
|
||||
- 탭 기반 로그인 UI (비밀번호 기반 / 링크 기반 / QR 기반 등)
|
||||
@@ -147,7 +147,7 @@ Kratos가 사용자 SoT이며 Hydra는 순수 OIDC 토큰 엔진입니다. 비
|
||||
|
||||
### 사전 요구사항 (Prerequisites)
|
||||
- Docker & Docker Compose
|
||||
- Flutter SDK (로컬 개발용)
|
||||
- Flutter SDK (로컬 개발용, 3.38.0+)
|
||||
- Go (로컬 백엔드 개발용)
|
||||
|
||||
### 환경 설정 (Environment Setup)
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ It leverages **Descope** for secure, passwordless authentication (Enchanted Link
|
||||
## 🏗 Architecture
|
||||
|
||||
### 1. Frontend (Flutter Web)
|
||||
- **Framework**: Flutter 3.32+
|
||||
- **Framework**: Flutter 3.38.0+
|
||||
- **Organization**: `kr.co.baroncs`
|
||||
- **Key Packages**: `descope`, `flutter_riverpod`, `go_router`
|
||||
- **Features**:
|
||||
@@ -32,7 +32,7 @@ It leverages **Descope** for secure, passwordless authentication (Enchanted Link
|
||||
|
||||
### Prerequisites
|
||||
- Docker & Docker Compose
|
||||
- Flutter SDK (for local development)
|
||||
- Flutter SDK (for local development, 3.38.0+)
|
||||
- Go (for local backend development)
|
||||
|
||||
### Environment Setup
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState } from "react";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
const LOCALE_STORAGE_KEY = "locale";
|
||||
const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
||||
|
||||
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
|
||||
function resolveLocale(): Locale {
|
||||
if (typeof window === "undefined") {
|
||||
return "ko";
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
|
||||
if (stored === "ko" || stored === "en") {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const pathLocale = window.location.pathname.split("/")[1];
|
||||
if (pathLocale === "ko" || pathLocale === "en") {
|
||||
return pathLocale;
|
||||
}
|
||||
|
||||
const browserLang = window.navigator.language.toLowerCase();
|
||||
return browserLang.startsWith("ko") ? "ko" : "en";
|
||||
}
|
||||
|
||||
function LanguageSelector() {
|
||||
const [locale, setLocale] = useState<Locale>(resolveLocale());
|
||||
|
||||
const handleChange = (next: Locale) => {
|
||||
if (next === locale) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, next);
|
||||
setLocale(next);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
value={locale}
|
||||
onChange={(event) => handleChange(event.target.value as Locale)}
|
||||
className="rounded-full border border-border bg-transparent px-3 py-2 text-sm text-muted-foreground transition hover:bg-muted/20"
|
||||
aria-label={t("ui.common.language", "언어")}
|
||||
>
|
||||
<option value="ko">
|
||||
{t("ui.common.language_ko", "한국어")}
|
||||
</option>
|
||||
<option value="en">
|
||||
{t("ui.common.language_en", "English")}
|
||||
</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||
import { t } from "../../lib/i18n";
|
||||
import LanguageSelector from "../common/LanguageSelector";
|
||||
import RoleSwitcher from "./RoleSwitcher";
|
||||
|
||||
const navItems = [
|
||||
@@ -170,6 +171,7 @@ function AppLayout() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<LanguageSelector />
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState } from "react";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
const LOCALE_STORAGE_KEY = "locale";
|
||||
const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
||||
|
||||
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
|
||||
function resolveLocale(): Locale {
|
||||
if (typeof window === "undefined") {
|
||||
return "ko";
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
|
||||
if (stored === "ko" || stored === "en") {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const pathLocale = window.location.pathname.split("/")[1];
|
||||
if (pathLocale === "ko" || pathLocale === "en") {
|
||||
return pathLocale;
|
||||
}
|
||||
|
||||
const browserLang = window.navigator.language.toLowerCase();
|
||||
return browserLang.startsWith("ko") ? "ko" : "en";
|
||||
}
|
||||
|
||||
function LanguageSelector() {
|
||||
const [locale, setLocale] = useState<Locale>(resolveLocale());
|
||||
|
||||
const handleChange = (next: Locale) => {
|
||||
if (next === locale) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, next);
|
||||
setLocale(next);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
value={locale}
|
||||
onChange={(event) => handleChange(event.target.value as Locale)}
|
||||
className="rounded-full border border-border bg-transparent px-3 py-2 text-sm text-muted-foreground transition hover:bg-muted/20"
|
||||
aria-label={t("ui.common.language", "언어")}
|
||||
>
|
||||
<option value="ko">
|
||||
{t("ui.common.language_ko", "한국어")}
|
||||
</option>
|
||||
<option value="en">
|
||||
{t("ui.common.language_en", "English")}
|
||||
</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { t } from "../../lib/i18n";
|
||||
import LanguageSelector from "../common/LanguageSelector";
|
||||
import { Toaster } from "../ui/toaster";
|
||||
|
||||
const navItems = [
|
||||
@@ -134,6 +135,7 @@ function AppLayout() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<LanguageSelector />
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
|
||||
+5
-5
@@ -11,8 +11,8 @@ services:
|
||||
- GO_ENV=${APP_ENV:-development}
|
||||
- COOKIE_SECRET=${COOKIE_SECRET}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- DESCOPE_PROJECT_ID=${DESCOPE_PROJECT_ID}
|
||||
- DESCOPE_MANAGEMENT_KEY=${DESCOPE_MANAGEMENT_KEY}
|
||||
- DESCOPE_PROJECT_ID=${DESCOPE_PROJECT_ID:-}
|
||||
- DESCOPE_MANAGEMENT_KEY=${DESCOPE_MANAGEMENT_KEY:-}
|
||||
- NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY}
|
||||
- NAVER_CLOUD_SECRET_KEY=${NAVER_CLOUD_SECRET_KEY}
|
||||
- NAVER_CLOUD_SERVICE_ID=${NAVER_CLOUD_SERVICE_ID}
|
||||
@@ -86,13 +86,13 @@ services:
|
||||
|
||||
userfront:
|
||||
build:
|
||||
context: ./userfront
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
dockerfile: userfront/Dockerfile
|
||||
container_name: baron_userfront
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- BACKEND_URL=${BACKEND_URL}
|
||||
- BACKEND_URL=${BACKEND_URL:-}
|
||||
- USERFRONT_URL=${USERFRONT_URL}
|
||||
- APP_ENV=${APP_ENV}
|
||||
networks:
|
||||
|
||||
@@ -101,6 +101,7 @@ services:
|
||||
command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml
|
||||
volumes:
|
||||
- ./docker/ory/oathkeeper:/etc/config/oathkeeper
|
||||
- oathkeeper_logs:/var/log/oathkeeper
|
||||
networks:
|
||||
- ory-net
|
||||
- baron_net # Backend가 통신하기 위해 필수
|
||||
|
||||
@@ -64,7 +64,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- BACKEND_URL=${BACKEND_URL}
|
||||
- BACKEND_URL=${BACKEND_URL:-}
|
||||
- USERFRONT_URL=${USERFRONT_URL}
|
||||
- APP_ENV=stage
|
||||
networks:
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
condition: service_healthy
|
||||
command: >
|
||||
/bin/sh -c "mkdir -p /usr/share/nginx/html/assets &&
|
||||
echo \"BACKEND_URL=${BACKEND_URL}\" >> /usr/share/nginx/html/assets/.env &&
|
||||
echo \"BACKEND_URL=${BACKEND_URL:-}\" >> /usr/share/nginx/html/assets/.env &&
|
||||
echo \"USERFRONT_URL=${USERFRONT_URL}\" >> /usr/share/nginx/html/assets/.env &&
|
||||
echo \"APP_ENV=stage\" >> /usr/share/nginx/html/assets/.env &&
|
||||
cp /usr/share/nginx/html/assets/.env /usr/share/nginx/html/.env &&
|
||||
|
||||
@@ -37,7 +37,11 @@ mkdir -p "$LOG_DIR"
|
||||
if ! touch "$LOG_FILE" 2>/dev/null; then
|
||||
echo "[oathkeeper] log file not writable: $LOG_FILE"
|
||||
ls -ld "$LOG_DIR" || true
|
||||
exit 1
|
||||
LOG_FILE=""
|
||||
fi
|
||||
|
||||
if [ -n "$LOG_FILE" ]; then
|
||||
exec /bin/sh -c "oathkeeper serve proxy -c /etc/config/oathkeeper/oathkeeper.yml 2>&1 | tee \"$LOG_FILE\""
|
||||
fi
|
||||
|
||||
exec /bin/sh -c "oathkeeper serve proxy -c /etc/config/oathkeeper/oathkeeper.yml"
|
||||
@@ -0,0 +1,44 @@
|
||||
# Baron SSO Data SoT (Source of Truth) Architecture Policy
|
||||
|
||||
## 1. Core Principle: "Ory Stack is the Single Source of Truth"
|
||||
Baron SSO 시스템에서 인증(Identity), 인가(Authorization), OAuth2 위임(Delegation)의 데이터 원천은 **Ory Stack (Kratos, Keto, Hydra)** 입니다.
|
||||
Backend의 로컬 데이터베이스(PostgreSQL)는 성능 최적화, 검색, 감사(Audit), 비즈니스 메타데이터 관리를 위한 **Read-Model** 및 **Cold Storage**의 역할만 수행합니다.
|
||||
|
||||
## 2. Component Policies
|
||||
|
||||
### 2.1 Identity & User Profile (Ory Kratos)
|
||||
* **SoT:** Ory Kratos Identity (`traits`, `metadata_public`)
|
||||
* **Local DB (`users` Table):** **Read-Model & Search Index**
|
||||
* **목적:** 대규모 사용자 목록의 고속 검색(`LIKE`), 필터링, 정렬, 테넌트 조인(Join) 지원.
|
||||
* **동기화 전략:** `Async Write-Behind`
|
||||
* 사용자 생성/수정 API는 Kratos 처리가 성공하면 즉시 성공 응답을 반환합니다.
|
||||
* 로컬 DB 동기화는 별도 고루틴(Goroutine)에서 비동기로 수행됩니다.
|
||||
* **장애 격리:** 로컬 DB 장애가 사용자의 로그인/가입 프로세스를 차단하지 않습니다.
|
||||
|
||||
### 2.2 Permissions & Relationships (Ory Keto)
|
||||
* **SoT:** Ory Keto (Relation Tuples)
|
||||
* **Local DB:**
|
||||
* 권한 판단 로직을 로컬 DB에 저장하지 않습니다.
|
||||
* `Tenant`, `TenantGroup` 등 비즈니스 객체의 **생성/삭제 이벤트**를 Keto의 관계(Relation)로 비동기 동기화합니다.
|
||||
* 모든 권한 검증(`CheckPermission`)은 반드시 Keto API를 통해 실시간으로 수행합니다.
|
||||
|
||||
### 2.3 OAuth2 Clients & Sessions (Ory Hydra)
|
||||
* **SoT:** Ory Hydra (OAuth2 Clients, Access/Refresh Tokens, Consent Sessions)
|
||||
* **Local DB (`client_secrets`, `client_consents`):** **Backup & Query-Model**
|
||||
* `client_secrets`: Hydra는 해시된 시크릿만 저장하므로, 시크릿 재발급 및 관리를 위한 **원본 보관소(Cold Storage)**로 사용합니다.
|
||||
* `client_consents`: Hydra API는 "특정 사용자의 동의 내역" 조회만 지원하므로, "특정 클라이언트의 전체 사용자 동의 목록"을 제공하기 위한 **조회용 모델(Query-Model)**로 사용합니다.
|
||||
|
||||
## 3. Data Flow & Synchronization Strategy
|
||||
|
||||
### 3.1 Write Path (Command)
|
||||
1. **Request:** 클라이언트가 Backend API 요청.
|
||||
2. **Ory Exec:** Backend가 Ory 서비스(Kratos/Hydra/Keto) API를 동기(Synchronous) 호출.
|
||||
3. **Response:** Ory 성공 시 클라이언트에게 즉시 성공 응답 반환 (SoT 확정).
|
||||
4. **Sync:** Backend가 비동기(Goroutine)로 로컬 DB 테이블을 갱신.
|
||||
|
||||
### 3.2 Read Path (Query)
|
||||
* **Self Context (내 정보, 내 권한):** Ory Session/Token을 통해 직접 검증하거나 Kratos/Keto를 실시간 조회 (Always Fresh).
|
||||
* **Admin Context (목록 조회, 검색):** 로컬 DB를 조회하여 빠른 응답 제공 (Eventually Consistent).
|
||||
|
||||
### 3.3 Conflict Resolution
|
||||
* 데이터 불일치가 발견될 경우, 항상 **Ory Stack의 데이터를 기준(Authority)**으로 로컬 DB를 보정(Self-healing)합니다.
|
||||
@@ -79,6 +79,7 @@ TOML에서는 `[Section]`을 사용하여 계층을 표현합니다.
|
||||
* `ko` 계열이면 `/ko/dashboard`로 리다이렉트합니다.
|
||||
* 그 외에는 `/en/dashboard`로 리다이렉트합니다.
|
||||
* *단, 이전에 언어를 선택한 쿠키나 로컬스토리지 값이 있다면 그 값을 우선합니다.*
|
||||
* 로컬스토리지 키는 **`locale`**을 사용합니다.
|
||||
2. **언어 변경 시**:
|
||||
* 모든 화면에는 **언어 선택기(Language Selector)**가 노출되어야 합니다.
|
||||
* 변경 시 해당 언어의 URL로 이동(`window.location` 변경 혹은 Router Push)하며, 선택된 언어를 로컬스토리지에 저장합니다.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# userfront locale 전환 문제 분석 및 대응
|
||||
|
||||
## 증상
|
||||
- URL은 `/ko` ↔ `/en`으로 변경되지만 실제 텍스트가 바뀌지 않음.
|
||||
- 브라우저에서 `window.localStorage.getItem('locale')`가 계속 `null`.
|
||||
- 빌드/배포 후에도 동일 증상 유지.
|
||||
|
||||
## 현재 구조 요약
|
||||
- Flutter Web + `easy_localization` 사용.
|
||||
- 경로 기반 로케일: `/:locale` 라우트 + `LocaleGate`로 로케일 적용.
|
||||
- 번역 리소스: `userfront/assets/translations/*.toml`.
|
||||
- 로케일 저장: `localStorage` key `locale`.
|
||||
|
||||
## 이미 적용한 변경
|
||||
- `easy_localization` + TOML loader 적용.
|
||||
- `LocaleGate`에서 로케일 적용 및 저장.
|
||||
- `LanguageSelector`에서 로케일 저장 + URL 변경.
|
||||
- `userfront/assets/translations`에 `en.toml`, `ko.toml`, `template.toml` 포함.
|
||||
- `pubspec.yaml` assets 등록.
|
||||
- GoRouter 호환성 수정(빌드 오류 대응).
|
||||
|
||||
## 원인 후보(우선순위)
|
||||
1) **로케일 저장이 실제로 실행되지 않음**
|
||||
- `LanguageSelector` 클릭 이벤트가 동작하지 않거나,
|
||||
- `LocaleGate`가 호출되지 않는 라우트 진입(예: 라우트 변경 없이 SPA 상태만 변경).
|
||||
|
||||
2) **서비스 워커/캐시로 인해 구 번들이 계속 로드됨**
|
||||
- Flutter Web 기본 `flutter_service_worker.js`가 캐시를 고정.
|
||||
- 기존 번들이 남아 `LocaleStorage.write()` 반영 전 코드가 계속 실행될 가능성.
|
||||
|
||||
3) **번역 리소스 미로딩**
|
||||
- `assets/translations/en.toml`, `ko.toml` 요청이 404 또는 미요청.
|
||||
- Nginx 경로/정적 파일 설정에서 `assets/`가 누락된 경우.
|
||||
|
||||
4) **en.toml 키 누락으로 fallback 문자열이 표시됨**
|
||||
- UI에서 `tr(..., fallback: '한국어')`를 쓰는 경우,
|
||||
en.toml에 키가 없으면 한국어가 그대로 노출됨.
|
||||
|
||||
## 확인 방법(권장 순서)
|
||||
1) **로케일 저장 확인**
|
||||
- 언어 변경 후 콘솔:
|
||||
- `window.localStorage.getItem('locale')`
|
||||
- `null`이면 저장 로직 실행 실패.
|
||||
|
||||
2) **번역 리소스 요청 확인**
|
||||
- DevTools Network에서:
|
||||
- `/assets/translations/ko.toml`
|
||||
- `/assets/translations/en.toml`
|
||||
- 404 또는 미요청이면 assets 로딩 문제.
|
||||
|
||||
3) **로케일 반영 확인**
|
||||
- UI 내 임시 텍스트로 `context.locale.languageCode` 출력
|
||||
- 로케일이 바뀌는데 텍스트가 그대로면 TOML 키 누락 가능성.
|
||||
|
||||
4) **캐시 무효화**
|
||||
- 하드 리로드(Shift+Reload)
|
||||
- service worker 제거 후 재접속
|
||||
|
||||
## 현재까지 실행한 대응
|
||||
- `LocaleGate`에서 로케일 동일 여부와 상관없이 `LocaleStorage.write()`가 실행되도록 수정.
|
||||
- 라우터 로케일 파싱/전환 로직 보강.
|
||||
- 번역 리소스 파일 배치 및 assets 등록.
|
||||
|
||||
## 앞으로의 대응 계획
|
||||
1) **서비스 워커 캐시 무효화 전략**
|
||||
- `flutter_service_worker.js` 버전 갱신 또는 Nginx 캐시 무력화 정책 적용.
|
||||
- 배포 시 `index.html`/`flutter_service_worker.js` 캐시 제어 헤더 추가.
|
||||
|
||||
2) **번역 파일 누락 방지**
|
||||
- `scripts/sync_userfront_locales.sh`를 CI에서 자동 실행.
|
||||
- en/ko 키 누락 검증 테스트 추가.
|
||||
|
||||
3) **로케일 저장 검증**
|
||||
- 언어 변경 후 `localStorage`에 값이 반드시 들어오는지 E2E 체크 추가.
|
||||
|
||||
4) **fallback 사용 최소화**
|
||||
- userfront에서 `fallback` 문자열(한국어) 남용 구간 정리.
|
||||
- en.toml에 키가 없을 때 한국어가 보이는 현상 방지.
|
||||
|
||||
## 참고 파일
|
||||
- `userfront/lib/core/i18n/locale_gate.dart`
|
||||
- `userfront/lib/core/widgets/language_selector.dart`
|
||||
- `userfront/lib/core/i18n/locale_storage_web.dart`
|
||||
- `userfront/lib/core/i18n/toml_asset_loader.dart`
|
||||
- `userfront/assets/translations/en.toml`
|
||||
- `userfront/assets/translations/ko.toml`
|
||||
+12
-10
@@ -355,7 +355,6 @@ title_with_code = "Title With Code"
|
||||
type = "Type"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = "$NormalizedCode"
|
||||
settings_disabled = "Account settings are currently unavailable."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
@@ -372,10 +371,10 @@ link_failed = "Link Failed"
|
||||
link_send_failed = "Link Send Failed"
|
||||
link_sent_email = "Link Sent Email"
|
||||
link_sent_phone = "Link Sent Phone"
|
||||
link_timeout = "Link Timeout"
|
||||
no_account = "No Account"
|
||||
link_timeout = "Time expired."
|
||||
no_account = "New to Baron?"
|
||||
oidc_failed = "OIDC Failed"
|
||||
qr_expired = "QR Expired"
|
||||
qr_expired = "Time expired."
|
||||
qr_init_failed = "QR Init Failed"
|
||||
qr_login_required = "QR Login Required"
|
||||
token_missing = "Token Missing"
|
||||
@@ -383,7 +382,7 @@ verification_failed = "Verification Failed"
|
||||
|
||||
[msg.userfront.login.link]
|
||||
approved = "Approved"
|
||||
helper = "Helper"
|
||||
helper = "Sending you a login link"
|
||||
missing_login_id = "Missing Login Id"
|
||||
missing_phone = "Missing Phone"
|
||||
resend_wait = "Resend Wait"
|
||||
@@ -871,6 +870,9 @@ retry = "Retry"
|
||||
save = "Save"
|
||||
search = "Search"
|
||||
show_more = "Show More"
|
||||
language = "Language"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "Theme Toggle"
|
||||
@@ -1091,7 +1093,7 @@ subtitle = "Manage your applications"
|
||||
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "App Title"
|
||||
app_title = "Baron SW Portal"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
@@ -1161,7 +1163,7 @@ signup = "Signup"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.field]
|
||||
login_id = "Login Id"
|
||||
login_id = "Emain or Phone Number"
|
||||
password = "Password"
|
||||
|
||||
[ui.userfront.login.link]
|
||||
@@ -1175,7 +1177,7 @@ title = "Title"
|
||||
[ui.userfront.login.qr]
|
||||
expired = "Expired"
|
||||
refresh = "Refresh"
|
||||
remaining = "Remaining"
|
||||
remaining = "Remaining: {{time}}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = "Digits"
|
||||
@@ -1184,9 +1186,9 @@ prefix = "Prefix"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.tabs]
|
||||
link = "Link"
|
||||
link = "Link/Code"
|
||||
password = "Password"
|
||||
qr = "QR"
|
||||
qr = "QR Code"
|
||||
|
||||
[ui.userfront.login.unregistered]
|
||||
action = "Action"
|
||||
|
||||
+6
-4
@@ -355,7 +355,6 @@ title_with_code = "오류: {{code}}"
|
||||
type = "오류 종류: {{type}}"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = "에러가 계속되면 관리자에게 문의해주세요"
|
||||
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
@@ -372,10 +371,10 @@ link_failed = "오류: {{error}}"
|
||||
link_send_failed = "전송 실패: {{error}}"
|
||||
link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
|
||||
link_timeout = "로그인 요청 시간이 초과되었습니다."
|
||||
link_timeout = "시간이 경과되었습니다."
|
||||
no_account = "계정이 없으신가요?"
|
||||
oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
|
||||
qr_expired = "QR 세션이 만료되었습니다."
|
||||
qr_expired = "시간이 경과되었습니다."
|
||||
qr_init_failed = "QR 초기화에 실패했습니다: {{error}}"
|
||||
qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
|
||||
token_missing = "로그인 토큰을 확인할 수 없습니다."
|
||||
@@ -871,6 +870,9 @@ retry = "다시 시도"
|
||||
save = "저장"
|
||||
search = "검색"
|
||||
show_more = "+ 더보기"
|
||||
language = "언어"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "테마 전환"
|
||||
@@ -1091,7 +1093,7 @@ subtitle = "Manage your applications"
|
||||
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "Baron 로그인"
|
||||
app_title = "Baron SW 포탈"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
|
||||
@@ -355,7 +355,6 @@ title_with_code = ""
|
||||
type = ""
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = ""
|
||||
settings_disabled = ""
|
||||
|
||||
[msg.userfront.forgot]
|
||||
@@ -883,6 +882,9 @@ retry = ""
|
||||
save = ""
|
||||
search = ""
|
||||
show_more = ""
|
||||
language = ""
|
||||
language_ko = ""
|
||||
language_en = ""
|
||||
theme_dark = ""
|
||||
theme_light = ""
|
||||
theme_toggle = ""
|
||||
@@ -1187,7 +1189,7 @@ title = ""
|
||||
[ui.userfront.login.qr]
|
||||
expired = ""
|
||||
refresh = ""
|
||||
remaining = ""
|
||||
remaining = "Remaining: {{time}}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = ""
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# 루트 locales/*.toml -> userfront/assets/translations/ 동기화
|
||||
# - userfront에서 사용하는 섹션만 추출
|
||||
# - {{param}} -> {param} 변환 (easy_localization 포맷)
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
src_dir="$repo_root/locales"
|
||||
dest_dir="$repo_root/userfront/assets/translations"
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
filter_toml() {
|
||||
src_file="$1"
|
||||
dest_file="$2"
|
||||
awk '
|
||||
function allowed(section) {
|
||||
return section ~ /^(ui\.userfront|msg\.userfront|err\.userfront|ui\.common)(\.|$)/;
|
||||
}
|
||||
BEGIN { keep = 0; }
|
||||
{
|
||||
line = $0;
|
||||
if (match(line, /^[[:space:]]*\[[^]]+\][[:space:]]*$/)) {
|
||||
section = line;
|
||||
gsub(/^[[:space:]]*\[/, "", section);
|
||||
gsub(/\][[:space:]]*$/, "", section);
|
||||
keep = allowed(section);
|
||||
if (keep) {
|
||||
print line;
|
||||
}
|
||||
next;
|
||||
}
|
||||
if (keep) {
|
||||
print line;
|
||||
}
|
||||
}
|
||||
' "$src_file" \
|
||||
| sed -E 's/\{\{[[:space:]]*([a-zA-Z0-9_]+)[[:space:]]*\}\}/{\1}/g' \
|
||||
> "$dest_file"
|
||||
}
|
||||
|
||||
for file in "$src_dir"/*.toml; do
|
||||
base="$(basename "$file")"
|
||||
filter_toml "$file" "$dest_dir/$base"
|
||||
done
|
||||
|
||||
echo "Synced locales to userfront assets: $dest_dir"
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const LOCALES_DIR = path.join(ROOT_DIR, 'locales');
|
||||
const TEMPLATE_PATH = path.join(LOCALES_DIR, 'template.toml');
|
||||
const LANG_FILES = ['ko.toml', 'en.toml'];
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'.dart_tool',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'coverage',
|
||||
'.next',
|
||||
'.cache',
|
||||
'tmp',
|
||||
'logs',
|
||||
]);
|
||||
|
||||
const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.dart']);
|
||||
|
||||
const CODE_PATTERNS = [
|
||||
/\b(?:i18n\.)?t\s*\(\s*['"]([^'"]+)['"]/g,
|
||||
/\btr\s*\(\s*['"]([^'"]+)['"]/g,
|
||||
/['"]([^'"]+)['"]\s*\.tr\s*\(/g,
|
||||
];
|
||||
|
||||
function readFileRequired(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { ok: false, error: `파일이 없습니다: ${filePath}` };
|
||||
}
|
||||
return { ok: true, value: fs.readFileSync(filePath, 'utf8') };
|
||||
}
|
||||
|
||||
function parseTomlKeys(filePath) {
|
||||
const result = readFileRequired(filePath);
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.error, keys: new Set() };
|
||||
}
|
||||
|
||||
const keys = new Set();
|
||||
const lines = result.value.split(/\r?\n/);
|
||||
let currentSection = [];
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('[[') && line.endsWith(']]')) {
|
||||
const sectionName = line.slice(2, -2).trim();
|
||||
currentSection = sectionName
|
||||
? sectionName.split('.').map((p) => p.trim()).filter(Boolean)
|
||||
: [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('[') && line.endsWith(']')) {
|
||||
const sectionName = line.slice(1, -1).trim();
|
||||
currentSection = sectionName
|
||||
? sectionName.split('.').map((p) => p.trim()).filter(Boolean)
|
||||
: [];
|
||||
continue;
|
||||
}
|
||||
|
||||
const eqIndex = line.indexOf('=');
|
||||
if (eqIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, eqIndex).trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullKey = [...currentSection, key].join('.');
|
||||
keys.add(fullKey);
|
||||
}
|
||||
|
||||
return { ok: true, keys };
|
||||
}
|
||||
|
||||
function walkDir(dirPath, files) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (SKIP_DIRS.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
walkDir(path.join(dirPath, entry.name), files);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!CODE_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push(path.join(dirPath, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
function collectCodeKeys() {
|
||||
const files = [];
|
||||
walkDir(ROOT_DIR, files);
|
||||
|
||||
const keys = new Set();
|
||||
for (const filePath of files) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const pattern of CODE_PATTERNS) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
keys.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function difference(aSet, bSet) {
|
||||
const result = [];
|
||||
for (const item of aSet) {
|
||||
if (!bSet.has(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function buildReport() {
|
||||
const report = {
|
||||
generated_at: new Date().toISOString(),
|
||||
errors: [],
|
||||
warnings: [],
|
||||
details: {
|
||||
missing_in_template: [],
|
||||
missing_in_lang: {},
|
||||
unused_in_template: [],
|
||||
},
|
||||
};
|
||||
|
||||
const templateResult = parseTomlKeys(TEMPLATE_PATH);
|
||||
if (!templateResult.ok) {
|
||||
report.errors.push(templateResult.error);
|
||||
return report;
|
||||
}
|
||||
|
||||
const templateKeys = templateResult.keys;
|
||||
const codeKeys = collectCodeKeys();
|
||||
|
||||
const langKeyMap = new Map();
|
||||
for (const fileName of LANG_FILES) {
|
||||
const langPath = path.join(LOCALES_DIR, fileName);
|
||||
const langResult = parseTomlKeys(langPath);
|
||||
if (!langResult.ok) {
|
||||
report.errors.push(langResult.error);
|
||||
continue;
|
||||
}
|
||||
langKeyMap.set(fileName, langResult.keys);
|
||||
}
|
||||
|
||||
for (const [fileName, langKeys] of langKeyMap.entries()) {
|
||||
const missingInLang = difference(templateKeys, langKeys);
|
||||
if (missingInLang.length > 0) {
|
||||
report.errors.push(
|
||||
`[Sync Error] ${fileName} 누락 키 ${missingInLang.length}개`,
|
||||
);
|
||||
report.details.missing_in_lang[fileName] = missingInLang;
|
||||
}
|
||||
}
|
||||
|
||||
const missingInTemplate = difference(codeKeys, templateKeys);
|
||||
if (missingInTemplate.length > 0) {
|
||||
report.errors.push(
|
||||
`[Missing Key] template.toml 누락 키 ${missingInTemplate.length}개`,
|
||||
);
|
||||
report.details.missing_in_template = missingInTemplate;
|
||||
}
|
||||
|
||||
const unusedInTemplate = difference(templateKeys, codeKeys);
|
||||
if (unusedInTemplate.length > 0) {
|
||||
report.warnings.push(
|
||||
`[Unused Key] template.toml 미사용 키 ${unusedInTemplate.length}개`,
|
||||
);
|
||||
report.details.unused_in_template = unusedInTemplate;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const report = buildReport();
|
||||
const outDir = path.join(ROOT_DIR, 'reports');
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
const outPath = path.join(outDir, 'i18n-report.json');
|
||||
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
||||
const summaryPath = path.join(outDir, 'i18n-report.txt');
|
||||
|
||||
const lines = [];
|
||||
lines.push(`generated_at: ${report.generated_at}`);
|
||||
if (report.errors.length > 0) {
|
||||
lines.push('errors:');
|
||||
report.errors.forEach((err) => lines.push(`- ${err}`));
|
||||
} else {
|
||||
lines.push('errors: none');
|
||||
}
|
||||
if (report.warnings.length > 0) {
|
||||
lines.push('warnings:');
|
||||
report.warnings.forEach((warn) => lines.push(`- ${warn}`));
|
||||
} else {
|
||||
lines.push('warnings: none');
|
||||
}
|
||||
fs.writeFileSync(summaryPath, lines.join('\n'));
|
||||
|
||||
if (report.errors.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,10 +1,11 @@
|
||||
# Stage 1: Build Flutter
|
||||
FROM ghcr.io/cirruslabs/flutter:stable AS build
|
||||
FROM ghcr.io/cirruslabs/flutter:3.38.0 AS build
|
||||
ENV RUN_FLUTTER_AS_ROOT=true
|
||||
# ENV RUN_FLUTTER_AS_ROOT=true
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
# Get dependencies and build for web
|
||||
RUN /bin/sh ./scripts/sync_userfront_locales.sh
|
||||
WORKDIR /app/userfront
|
||||
RUN flutter pub get
|
||||
RUN touch .env
|
||||
RUN flutter build web --wasm --release --no-tree-shake-icons
|
||||
@@ -12,9 +13,9 @@ RUN flutter build web --wasm --release --no-tree-shake-icons
|
||||
# Stage 2: Serve with Nginx
|
||||
FROM nginx:alpine
|
||||
# Copy built assets
|
||||
COPY --from=build /app/build/web /usr/share/nginx/html
|
||||
COPY --from=build /app/userfront/build/web /usr/share/nginx/html
|
||||
# Copy custom Nginx config
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY userfront/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 5000
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,536 @@
|
||||
[err.userfront]
|
||||
|
||||
[err.userfront.auth_proxy]
|
||||
consent_accept = "Consent Accept"
|
||||
consent_fetch = "Consent Fetch"
|
||||
consent_reject = "Consent Reject"
|
||||
linked_app_revoke = "Linked App Revoke"
|
||||
login_failed = "Login Failed"
|
||||
oidc_accept = "OIDC Accept"
|
||||
password_reset_complete = "Password Reset Complete"
|
||||
password_reset_init = "Password Reset Init"
|
||||
|
||||
[err.userfront.profile]
|
||||
load_failed = "Load Failed"
|
||||
password_change_failed = "Password Change Failed"
|
||||
send_code_failed = "Send Code Failed"
|
||||
update_failed = "Update Failed"
|
||||
verify_code_failed = "Verify Code Failed"
|
||||
|
||||
[err.userfront.session]
|
||||
missing = "Missing"
|
||||
|
||||
[msg.userfront]
|
||||
greeting = "Greeting"
|
||||
|
||||
[msg.userfront.audit]
|
||||
date = "Date"
|
||||
device = "Device"
|
||||
end = "End"
|
||||
ip = "Ip"
|
||||
load_more_error = "Load More Error"
|
||||
result = "Result"
|
||||
session_id = "Session ID: {value}"
|
||||
status = "Status"
|
||||
|
||||
[msg.userfront.dashboard]
|
||||
approved_device = "Approved Device"
|
||||
approved_ip = "Approve IP: {ip}"
|
||||
audit_empty = "Audit Empty"
|
||||
audit_load_error = "Audit Load Error"
|
||||
auth_method = "Auth Method"
|
||||
client_id = "Client ID: {id}"
|
||||
client_id_missing = "Client Id Missing"
|
||||
current_status = "Current Status"
|
||||
last_auth = "Last Auth"
|
||||
link_missing = "Link Missing"
|
||||
link_open_error = "Link Open Error"
|
||||
session_id_copied = "Session Id Copied"
|
||||
|
||||
[msg.userfront.dashboard.activities]
|
||||
empty = "Empty"
|
||||
empty_detail = "Empty Detail"
|
||||
error = "Error"
|
||||
|
||||
[msg.userfront.dashboard.approved_session]
|
||||
copy_click = "Copy Click"
|
||||
copy_tap = "Copy Tap"
|
||||
none = "None"
|
||||
|
||||
[msg.userfront.dashboard.revoke]
|
||||
confirm = "Confirm"
|
||||
error = "Error"
|
||||
success = "Success"
|
||||
|
||||
[msg.userfront.dashboard.scopes]
|
||||
empty = "Empty"
|
||||
|
||||
[msg.userfront.dashboard.timeline]
|
||||
load_error = "Load Error"
|
||||
|
||||
[msg.userfront.error]
|
||||
detail_contact = "msg.userfront.error.detail_contact"
|
||||
detail_generic = "Detail Generic"
|
||||
detail_request = "Detail Request"
|
||||
id = "Id"
|
||||
title = "Title"
|
||||
title_generic = "Title Generic"
|
||||
title_with_code = "Title With Code"
|
||||
type = "Type"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
settings_disabled = "Account settings are currently unavailable."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "Description"
|
||||
dry_send = "Dry Send"
|
||||
error = "Error"
|
||||
input_required = "Input Required"
|
||||
sent = "Sent"
|
||||
|
||||
[msg.userfront.login]
|
||||
cookie_check_failed = "Cookie Check Failed"
|
||||
dry_send = "Dry Send"
|
||||
link_failed = "Link Failed"
|
||||
link_send_failed = "Link Send Failed"
|
||||
link_sent_email = "Link Sent Email"
|
||||
link_sent_phone = "Link Sent Phone"
|
||||
link_timeout = "Time expired."
|
||||
no_account = "No Account"
|
||||
oidc_failed = "OIDC Failed"
|
||||
qr_expired = "Time expired."
|
||||
qr_init_failed = "QR Init Failed"
|
||||
qr_login_required = "QR Login Required"
|
||||
token_missing = "Token Missing"
|
||||
verification_failed = "Verification Failed"
|
||||
|
||||
[msg.userfront.login.link]
|
||||
approved = "Approved"
|
||||
helper = "Sending you a login link"
|
||||
missing_login_id = "Missing Login Id"
|
||||
missing_phone = "Missing Phone"
|
||||
resend_wait = "Resend Wait"
|
||||
short_code_help = "Short Code Help"
|
||||
|
||||
[msg.userfront.login.password]
|
||||
failed = "Failed"
|
||||
missing_credentials = "Missing Credentials"
|
||||
|
||||
[msg.userfront.login.qr]
|
||||
load_failed = "Load Failed"
|
||||
scan_hint = "Scan Hint"
|
||||
|
||||
[msg.userfront.login.short_code]
|
||||
invalid = "Invalid"
|
||||
|
||||
[msg.userfront.login.unregistered]
|
||||
body = "Body"
|
||||
|
||||
[msg.userfront.login.verification]
|
||||
approved = "Approved"
|
||||
approved_local = "Approved Local"
|
||||
success = "Success"
|
||||
|
||||
[msg.userfront.login_success]
|
||||
subtitle = "Subtitle"
|
||||
|
||||
[msg.userfront.profile]
|
||||
department_missing = "Department Missing"
|
||||
department_required = "Department Required"
|
||||
email_missing = "Email Missing"
|
||||
greeting = "Greeting"
|
||||
load_failed = "Load Failed"
|
||||
name_missing = "Name Missing"
|
||||
name_required = "Name Required"
|
||||
phone_required = "Phone Required"
|
||||
phone_verify_required = "Phone Verify Required"
|
||||
update_failed = "Update Failed"
|
||||
update_success = "Update Success"
|
||||
|
||||
[msg.userfront.profile.password]
|
||||
change_failed = "Change Failed"
|
||||
changed = "Changed"
|
||||
current_required = "Current Required"
|
||||
mismatch = "Mismatch"
|
||||
new_required = "New Required"
|
||||
subtitle = "Subtitle"
|
||||
|
||||
[msg.userfront.profile.phone]
|
||||
code_sent = "Code Sent"
|
||||
send_failed = "Send Failed"
|
||||
verified = "Verified"
|
||||
verify_failed = "Verify Failed"
|
||||
verify_notice = "Verify Notice"
|
||||
|
||||
[msg.userfront.profile.section]
|
||||
basic = "Basic"
|
||||
organization = "Organization"
|
||||
security = "Security"
|
||||
|
||||
[msg.userfront.qr]
|
||||
approve_error = "Approve Error"
|
||||
approve_success = "Approve Success"
|
||||
camera_error = "Camera Error"
|
||||
permission_error = "Permission Error"
|
||||
permission_required = "Permission Required"
|
||||
|
||||
[msg.userfront.reset]
|
||||
invalid_body = "Invalid Body"
|
||||
invalid_link = "Invalid Link"
|
||||
invalid_title = "Invalid Title"
|
||||
policy_loading = "Policy Loading"
|
||||
success = "Success"
|
||||
|
||||
[msg.userfront.reset.error]
|
||||
empty_password = "Please enter Password."
|
||||
generic = "Generic"
|
||||
lowercase = "Lowercase"
|
||||
min_length = "Min Length"
|
||||
min_types = "Min Types"
|
||||
mismatch = "Mismatch"
|
||||
number = "Number"
|
||||
symbol = "Symbol"
|
||||
uppercase = "Uppercase"
|
||||
|
||||
[msg.userfront.reset.policy]
|
||||
lowercase = "Lowercase"
|
||||
min_length = "Min Length"
|
||||
min_types = "Min Types"
|
||||
number = "Number"
|
||||
symbol = "Symbol"
|
||||
uppercase = "Uppercase"
|
||||
|
||||
[msg.userfront.sections]
|
||||
apps_subtitle = "Apps Subtitle"
|
||||
audit_subtitle = "Audit Subtitle"
|
||||
|
||||
[msg.userfront.settings]
|
||||
disabled = "Disabled"
|
||||
|
||||
[msg.userfront.signup]
|
||||
failed = "Failed"
|
||||
privacy_full = "\\n개인정보 수집 및 이용 동의\\n\\n바론서비스 개인정보처리방침\\n\\n제1조 (목적)\\n바론컨설턴트(이하 \\\"회사\\\")는 바론서비스(이하 \\\"서비스\\\")를 이용하는 고객(이하 \\\"이용자\\\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\\n제2조 (개인정보의 처리목적)\\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\\n- 제품소개서 다운로드: 설명자료 전달\\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\\n- 보안가이드 제공: 안내자료 전달\\n- 기술지원 문의: 서비스 사용 지원\\n- 서비스 개선 의견 접수: 서비스 품질 개선\\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\\n제3조 (개인정보의 처리 및 보유 기간)\\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\\n- 홍보, 상담, 계약용 개인정보: 2년\\n제4조 (개인정보의 제3자 제공)\\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\\n- 이용 목적: 개인정보 침해 민원 처리\\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\\n제5조 (개인정보 처리 위탁)\\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\\n제6조 (정보주체의 권리·의무 및 행사 방법)\\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\\n- 서면: 회사 주소로 서면 제출\\n- 전자우편: 회사 이메일로 요청\\n- 모사전송(FAX): 회사 FAX로 요청\\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\\n제7조 (처리하는 개인정보의 항목)\\n회사는 다음의 개인정보 항목을 처리합니다:\\n- 수집 항목:\\n- 필수 항목: 성명, 휴대전화번호, 이메일\\n- 선택 항목: 회사전화번호, 문의사항\\n- 수집 방법:\\n- 홈페이지, 전화, 이메일을 통해 수집\\n제8조 (개인정보의 파기)\\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 Line truncated
|
||||
tos_full = "\\n바론 소프트웨어 이용약관\\n\\n제1장 총칙\\n제1조 (목적)\\n이 약관은 바론컨설턴트(이하 \\\"회사\\\"라 합니다)가 제공하는 바론소프트웨어(이하 \\\"서비스\\\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\\n제2조 (용어의 정의)\\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\\n제3조 (약관의 효력 및 변경)\\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\\n제4조 (약관 외 준칙)\\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\\n제2장 서비스 이용계약\\n제5조 (이용계약의 성립)\\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\\n제6조 (이용계약의 유보와 거절)\\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\\n제7조 (계약사항의 변경)\\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\\n제3장 개인정보 보호\\n제8조 (개인정보 보호의 원칙)\\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\\n제9조 (개인정보처리방침 준수)\\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\\n제10조 (14세 미만 아동의 개인정보 보호)\\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\\n제4장 서비스 제공 및 이용\\n제11조 (서비스 제공)\\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\\n제12조 (서비스의 변경 및 중단)\\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\\n제5장 정보 제공 및 광고\\n제13조 (정보 제공 및 광고)\\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\\n제6장 게시물 관리\\n제14조 (게시물의 관리)\\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\\n제15조 (게시물의 저작권)\\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\\n제7장 계약 해지 및 이용 제한\\n제16조 (계약 해지)\\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\\n제17조 (이용 제한)\\n회사는 회원이 Line truncated
|
||||
|
||||
[msg.userfront.signup.agreement]
|
||||
title = "Title"
|
||||
|
||||
[msg.userfront.signup.auth]
|
||||
affiliate_notice = "Affiliate Notice"
|
||||
title = "Title"
|
||||
|
||||
[msg.userfront.signup.email]
|
||||
code_mismatch = "Code Mismatch"
|
||||
duplicate = "Duplicate"
|
||||
invalid = "Invalid"
|
||||
send_failed = "Send Failed"
|
||||
verified = "Verified"
|
||||
verify_failed = "Verify Failed"
|
||||
|
||||
[msg.userfront.signup.password]
|
||||
length_required = "Length Required"
|
||||
lowercase_required = "Lowercase Required"
|
||||
mismatch = "Mismatch"
|
||||
number_required = "Number Required"
|
||||
symbol_required = "Symbol Required"
|
||||
title = "Title"
|
||||
uppercase_required = "Uppercase Required"
|
||||
|
||||
[msg.userfront.signup.password.rule]
|
||||
lowercase = "Lowercase"
|
||||
min_length = "Min Length"
|
||||
min_types = "Min Types"
|
||||
number = "Number"
|
||||
symbol = "Symbol"
|
||||
uppercase = "Uppercase"
|
||||
|
||||
[msg.userfront.signup.phone]
|
||||
code_mismatch = "Code Mismatch"
|
||||
send_failed = "Send Failed"
|
||||
verified = "Verified"
|
||||
verify_failed = "Verify Failed"
|
||||
|
||||
[msg.userfront.signup.policy]
|
||||
loading = "Loading"
|
||||
lowercase = "Lowercase"
|
||||
min_length = "Min Length"
|
||||
min_types = "Min Types"
|
||||
number = "Number"
|
||||
summary = "Summary"
|
||||
symbol = "Symbol"
|
||||
uppercase = "Uppercase"
|
||||
|
||||
[msg.userfront.signup.profile]
|
||||
affiliate_hint = "Affiliate Hint"
|
||||
title = "Title"
|
||||
|
||||
[msg.userfront.signup.success]
|
||||
body = "Body"
|
||||
title = "Title"
|
||||
|
||||
[ui.common]
|
||||
add = "Add"
|
||||
back = "Back"
|
||||
cancel = "Cancel"
|
||||
close = "Close"
|
||||
collapse = "Collapse"
|
||||
confirm = "Confirm"
|
||||
copy = "Copy"
|
||||
create = "Create"
|
||||
delete = "Delete"
|
||||
details = "Details"
|
||||
edit = "Edit"
|
||||
hyphen = "-"
|
||||
na = "N/A"
|
||||
never = "Never"
|
||||
next = "Next"
|
||||
page_of = "Page {page} of {total}"
|
||||
prev = "Prev"
|
||||
previous = "Previous"
|
||||
qr = "QR"
|
||||
read_only = "Read Only"
|
||||
refresh = "Refresh"
|
||||
requesting = "Requesting"
|
||||
resend = "Resend"
|
||||
retry = "Retry"
|
||||
save = "Save"
|
||||
search = "Search"
|
||||
show_more = "Show More"
|
||||
language = "Language"
|
||||
language_ko = "Korean"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "Theme Toggle"
|
||||
unknown = "Unknown"
|
||||
|
||||
[ui.common.badge]
|
||||
admin_only = "Admin only"
|
||||
command_only = "Command only"
|
||||
system = "System"
|
||||
|
||||
[ui.common.role]
|
||||
admin = "Admin"
|
||||
user = "User"
|
||||
|
||||
[ui.common.status]
|
||||
active = "Active"
|
||||
blocked = "Blocked"
|
||||
failure = "Failure"
|
||||
inactive = "Inactive"
|
||||
ok = "Ok"
|
||||
pending = "Pending"
|
||||
success = "Success"
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "Baron SW Portal"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
baron = "Baron"
|
||||
dev_console = "Dev Console"
|
||||
|
||||
[ui.userfront.audit]
|
||||
|
||||
[ui.userfront.audit.table]
|
||||
app = "App"
|
||||
auth_method = "Auth Method"
|
||||
date = "Date"
|
||||
device = "Device"
|
||||
ip = "IP"
|
||||
pending = "Pending"
|
||||
result = "Result"
|
||||
session_id = "Session ID"
|
||||
status = "Status"
|
||||
|
||||
[ui.userfront.auth_method]
|
||||
ory = "Ory"
|
||||
session = "Session"
|
||||
|
||||
[ui.userfront.dashboard]
|
||||
last_auth_label = "Last Auth Label"
|
||||
status_history = "Status History"
|
||||
|
||||
[ui.userfront.dashboard.activity]
|
||||
linked = "Linked"
|
||||
|
||||
[ui.userfront.dashboard.approved_session]
|
||||
default = "Default"
|
||||
userfront = "Userfront"
|
||||
|
||||
[ui.userfront.dashboard.revoke]
|
||||
confirm_button = "Confirm Button"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.dashboard.scopes]
|
||||
title = "Permission (Scopes)"
|
||||
|
||||
[ui.userfront.dashboard.status]
|
||||
revoked = "Revoked"
|
||||
|
||||
[ui.userfront.device]
|
||||
android = "Mobile(Android)"
|
||||
ios = "Mobile(iOS)"
|
||||
linux = "Desktop(Linux)"
|
||||
macos = "Desktop(macOS)"
|
||||
windows = "Desktop(Windows)"
|
||||
|
||||
[ui.userfront.error]
|
||||
go_home = "Go Home"
|
||||
go_login = "Go Login"
|
||||
|
||||
[ui.userfront.forgot]
|
||||
heading = "Heading"
|
||||
input_label = "Input Label"
|
||||
submit = "Submit"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.login]
|
||||
forgot_password = "Forgot Password"
|
||||
signup = "Signup"
|
||||
|
||||
[ui.userfront.login.action]
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.field]
|
||||
login_id = "Emain or Phone Number"
|
||||
password = "Password"
|
||||
|
||||
[ui.userfront.login.link]
|
||||
action_label = "Action Label"
|
||||
code_only = "Code Only"
|
||||
page_title = "Page Title"
|
||||
resend_with_time = "Resend With Time"
|
||||
send = "Send"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.login.qr]
|
||||
expired = "Expired"
|
||||
refresh = "Refresh"
|
||||
remaining = "Remaining: {time}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = "Digits"
|
||||
expire_time = "Expire Time"
|
||||
prefix = "Prefix"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.tabs]
|
||||
link = "Link/Code"
|
||||
password = "Password"
|
||||
qr = "QR Code"
|
||||
|
||||
[ui.userfront.login.unregistered]
|
||||
action = "Action"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.login.verification]
|
||||
action_label = "Confirm"
|
||||
page_title = "Page Title"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.login_success]
|
||||
later = "Later"
|
||||
qr = "QR"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.nav]
|
||||
dashboard = "Dashboard"
|
||||
logout = "Logout"
|
||||
profile = "Profile"
|
||||
qr_scan = "QR Scan"
|
||||
|
||||
[ui.userfront.profile]
|
||||
department_empty = "Department Empty"
|
||||
manage = "Manage"
|
||||
user_fallback = "User"
|
||||
|
||||
[ui.userfront.profile.field]
|
||||
affiliation = "Affiliation"
|
||||
company_code = "Company Code"
|
||||
department = "Department"
|
||||
email = "Email"
|
||||
name = "Name"
|
||||
tenant = "Tenant"
|
||||
|
||||
[ui.userfront.profile.password]
|
||||
change = "Change"
|
||||
confirm = "Confirm"
|
||||
current = "Current"
|
||||
forgot = "Forgot"
|
||||
new = "New"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.profile.phone]
|
||||
code_hint = "Code Hint"
|
||||
request_code = "Request Code"
|
||||
title = "Phone number"
|
||||
|
||||
[ui.userfront.profile.section]
|
||||
basic = "Basic"
|
||||
organization = "Organization"
|
||||
security = "Security"
|
||||
|
||||
[ui.userfront.qr]
|
||||
request_permission = "Request Permission"
|
||||
rescan = "Rescan"
|
||||
result_failure = "Result Failure"
|
||||
result_success = "Result Success"
|
||||
title = "Scan QR Code"
|
||||
|
||||
[ui.userfront.reset]
|
||||
confirm_password = "Confirm Password"
|
||||
new_password = "New Password"
|
||||
submit = "Submit"
|
||||
subtitle = "Subtitle"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.sections]
|
||||
apps = "Apps"
|
||||
audit = "Audit"
|
||||
|
||||
[ui.userfront.session]
|
||||
active = "Active"
|
||||
unknown = "Unknown"
|
||||
|
||||
[ui.userfront.signup]
|
||||
complete = "Complete"
|
||||
next_step = "Next Step"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.signup.agreement]
|
||||
all = "All"
|
||||
privacy_title = "Privacy Title"
|
||||
tos_title = "Tos Title"
|
||||
|
||||
[ui.userfront.signup.auth]
|
||||
code_label = "Code Label"
|
||||
request_code = "Request Code"
|
||||
|
||||
[ui.userfront.signup.auth.email]
|
||||
label = "Label"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.signup.password]
|
||||
confirm_label = "Password Confirm"
|
||||
label = "Password"
|
||||
|
||||
[ui.userfront.signup.phone]
|
||||
label = "Label"
|
||||
title = "Title"
|
||||
|
||||
[ui.userfront.signup.profile]
|
||||
affiliation_type = "Affiliation Type"
|
||||
company = "Company"
|
||||
department = "Department"
|
||||
department_optional = "Department Optional"
|
||||
name = "Name"
|
||||
|
||||
[ui.userfront.signup.steps]
|
||||
agreement = "Agreement"
|
||||
password = "Password"
|
||||
profile = "Profile"
|
||||
verify = "Verify"
|
||||
|
||||
[ui.userfront.signup.success]
|
||||
action = "Action"
|
||||
@@ -0,0 +1,536 @@
|
||||
[err.userfront]
|
||||
|
||||
[err.userfront.auth_proxy]
|
||||
consent_accept = "동의 처리에 실패했습니다."
|
||||
consent_fetch = "동의 정보를 가져오지 못했습니다."
|
||||
consent_reject = "동의 거부에 실패했습니다."
|
||||
linked_app_revoke = "연동 해지에 실패했습니다."
|
||||
login_failed = "로그인에 실패했습니다."
|
||||
oidc_accept = "OIDC 로그인 승인에 실패했습니다."
|
||||
password_reset_complete = "비밀번호 재설정에 실패했습니다."
|
||||
password_reset_init = "비밀번호 재설정을 시작하지 못했습니다."
|
||||
|
||||
[err.userfront.profile]
|
||||
load_failed = "프로필을 불러오지 못했습니다: {error}"
|
||||
password_change_failed = "비밀번호 변경에 실패했습니다: {error}"
|
||||
send_code_failed = "인증번호 전송 실패: {error}"
|
||||
update_failed = "프로필 업데이트에 실패했습니다: {error}"
|
||||
verify_code_failed = "인증 실패: {error}"
|
||||
|
||||
[err.userfront.session]
|
||||
missing = "활성 세션이 없습니다."
|
||||
|
||||
[msg.userfront]
|
||||
greeting = "안녕하세요, {name}님"
|
||||
|
||||
[msg.userfront.audit]
|
||||
date = "접속일자: {value}"
|
||||
device = "접속환경: {value}"
|
||||
end = "더 이상 항목이 없습니다."
|
||||
ip = "접속 IP: {value}"
|
||||
load_more_error = "더 불러오지 못했습니다."
|
||||
result = "인증결과: {value}"
|
||||
session_id = "Session ID: {value}"
|
||||
status = "현황: (준비중)"
|
||||
|
||||
[msg.userfront.dashboard]
|
||||
approved_device = "승인 기기: {device}"
|
||||
approved_ip = "승인 IP: {ip}"
|
||||
audit_empty = "최근 접속 이력이 없습니다."
|
||||
audit_load_error = "접속이력을 불러오지 못했습니다."
|
||||
auth_method = "인증수단: {method}"
|
||||
client_id = "Client ID: {id}"
|
||||
client_id_missing = "Client ID 없음"
|
||||
current_status = "현재 상태: {status}"
|
||||
last_auth = "최근 인증: {value}"
|
||||
link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
|
||||
link_open_error = "해당 링크를 열 수 없습니다."
|
||||
session_id_copied = "세션 ID가 복사되었습니다."
|
||||
|
||||
[msg.userfront.dashboard.activities]
|
||||
empty = "연동된 앱이 없습니다."
|
||||
empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
|
||||
error = "연동 정보를 불러오지 못했습니다."
|
||||
|
||||
[msg.userfront.dashboard.approved_session]
|
||||
copy_click = "{label}: {id}\\\\\\\\n클릭하면 복사됩니다."
|
||||
copy_tap = "{label}: {id}\\\\\\\\n탭하면 복사됩니다."
|
||||
none = "{label} 없음"
|
||||
|
||||
[msg.userfront.dashboard.revoke]
|
||||
confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
|
||||
error = "해지 실패: {error}"
|
||||
success = "{app} 연동이 해지되었습니다."
|
||||
|
||||
[msg.userfront.dashboard.scopes]
|
||||
empty = "요청된 권한이 없습니다."
|
||||
|
||||
[msg.userfront.dashboard.timeline]
|
||||
load_error = "접속이력을 불러오지 못했습니다."
|
||||
|
||||
[msg.userfront.error]
|
||||
detail_contact = "msg.userfront.error.detail_contact"
|
||||
detail_generic = "오류가 발생했습니다."
|
||||
detail_request = "요청을 처리하는 중 문제가 발생했습니다."
|
||||
id = "오류 ID: {id}"
|
||||
title = "인증 과정에서 오류가 발생했습니다"
|
||||
title_generic = "오류가 발생했습니다"
|
||||
title_with_code = "오류: {code}"
|
||||
type = "오류 종류: {type}"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
|
||||
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
|
||||
error = "전송에 실패했습니다: {error}"
|
||||
input_required = "이메일 또는 휴대폰 번호를 입력해주세요."
|
||||
sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요."
|
||||
|
||||
[msg.userfront.login]
|
||||
cookie_check_failed = "로그인 확인 실패: {error}"
|
||||
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
|
||||
link_failed = "오류: {error}"
|
||||
link_send_failed = "전송 실패: {error}"
|
||||
link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
|
||||
link_timeout = "시간이 경과되었습니다."
|
||||
no_account = "계정이 없으신가요?"
|
||||
oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
|
||||
qr_expired = "시간이 경과되었습니다."
|
||||
qr_init_failed = "QR 초기화에 실패했습니다: {error}"
|
||||
qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
|
||||
token_missing = "로그인 토큰을 확인할 수 없습니다."
|
||||
verification_failed = "승인 처리에 실패했습니다: {error}"
|
||||
|
||||
[msg.userfront.login.link]
|
||||
approved = "링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다."
|
||||
helper = "입력하신 정보로 로그인 링크를 전송합니다."
|
||||
missing_login_id = "이메일 또는 휴대폰 번호를 입력해 주세요."
|
||||
missing_phone = "휴대폰 번호를 입력해 주세요."
|
||||
resend_wait = "재발송은 {time} 후 가능합니다."
|
||||
short_code_help = "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다."
|
||||
|
||||
[msg.userfront.login.password]
|
||||
failed = "로그인 실패: {error}"
|
||||
missing_credentials = "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요."
|
||||
|
||||
[msg.userfront.login.qr]
|
||||
load_failed = "QR 코드를 불러오지 못했습니다."
|
||||
scan_hint = "모바일 앱으로 스캔하세요"
|
||||
|
||||
[msg.userfront.login.short_code]
|
||||
invalid = "문자 2개와 숫자 6자리를 입력해 주세요."
|
||||
|
||||
[msg.userfront.login.unregistered]
|
||||
body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주세요."
|
||||
|
||||
[msg.userfront.login.verification]
|
||||
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
|
||||
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
|
||||
success = "로그인 승인에 성공했습니다."
|
||||
|
||||
[msg.userfront.login_success]
|
||||
subtitle = "성공적으로 로그인되었습니다."
|
||||
|
||||
[msg.userfront.profile]
|
||||
department_missing = "소속 정보 없음"
|
||||
department_required = "소속을 입력해주세요."
|
||||
email_missing = "이메일 없음"
|
||||
greeting = "안녕하세요, {name}님"
|
||||
load_failed = "정보를 불러올 수 없습니다."
|
||||
name_missing = "이름 없음"
|
||||
name_required = "이름을 입력해주세요."
|
||||
phone_required = "휴대폰 번호를 입력해주세요."
|
||||
phone_verify_required = "휴대폰 번호 인증이 필요합니다."
|
||||
update_failed = "수정 실패: {error}"
|
||||
update_success = "정보가 수정되었습니다."
|
||||
|
||||
[msg.userfront.profile.password]
|
||||
change_failed = "비밀번호 변경 실패: {error}"
|
||||
changed = "비밀번호가 변경되었습니다."
|
||||
current_required = "현재 비밀번호를 입력해 주세요."
|
||||
mismatch = "새 비밀번호가 일치하지 않습니다."
|
||||
new_required = "새 비밀번호를 입력해 주세요."
|
||||
subtitle = "현재 비밀번호 확인 후 새 비밀번호로 변경합니다."
|
||||
|
||||
[msg.userfront.profile.phone]
|
||||
code_sent = "인증번호가 전송되었습니다."
|
||||
send_failed = "전송 실패: {error}"
|
||||
verified = "인증되었습니다."
|
||||
verify_failed = "인증 실패: {error}"
|
||||
verify_notice = "휴대폰 번호를 변경하려면 SMS 인증이 필요합니다."
|
||||
|
||||
[msg.userfront.profile.section]
|
||||
basic = "계정 기본 정보를 관리합니다."
|
||||
organization = "소속 및 구분 정보입니다."
|
||||
security = "비밀번호를 안전하게 관리합니다."
|
||||
|
||||
[msg.userfront.qr]
|
||||
approve_error = "QR 승인 실패: {error}"
|
||||
approve_success = "QR 승인 완료! PC 화면에서 로그인이 진행됩니다."
|
||||
camera_error = "카메라 오류: {error}"
|
||||
permission_error = "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요."
|
||||
permission_required = "카메라 권한이 필요합니다."
|
||||
|
||||
[msg.userfront.reset]
|
||||
invalid_body = "비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요."
|
||||
invalid_link = "유효하지 않은 재설정 링크입니다. (loginId/token 누락)"
|
||||
invalid_title = "유효하지 않은 링크입니다."
|
||||
policy_loading = "비밀번호 정책을 불러오는 중입니다..."
|
||||
success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요."
|
||||
|
||||
[msg.userfront.reset.error]
|
||||
empty_password = "비밀번호를 입력해주세요."
|
||||
generic = "비밀번호 변경에 실패했습니다: {error}"
|
||||
lowercase = "최소 1개 이상의 소문자를 포함해야 합니다."
|
||||
min_length = "비밀번호는 최소 {count}자 이상이어야 합니다."
|
||||
min_types = "비밀번호는 영문 대/소문자/숫자/특수문자 중 {count}가지 이상 포함해야 합니다."
|
||||
mismatch = "비밀번호가 일치하지 않습니다."
|
||||
number = "최소 1개 이상의 숫자를 포함해야 합니다."
|
||||
symbol = "최소 1개 이상의 특수문자를 포함해야 합니다."
|
||||
uppercase = "최소 1개 이상의 대문자를 포함해야 합니다."
|
||||
|
||||
[msg.userfront.reset.policy]
|
||||
lowercase = "소문자 1개 이상"
|
||||
min_length = "최소 {count}자 이상"
|
||||
min_types = "영문 대/소문자/숫자/특수문자 중 {count}가지 이상"
|
||||
number = "숫자 1개 이상"
|
||||
symbol = "특수문자 1개 이상"
|
||||
uppercase = "대문자 1개 이상"
|
||||
|
||||
[msg.userfront.sections]
|
||||
apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다."
|
||||
audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다."
|
||||
|
||||
[msg.userfront.settings]
|
||||
disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||
|
||||
[msg.userfront.signup]
|
||||
failed = "가입 실패: {error}"
|
||||
privacy_full = "\\n개인정보 수집 및 이용 동의\\n\\n바론서비스 개인정보처리방침\\n\\n제1조 (목적)\\n바론컨설턴트(이하 \\\"회사\\\")는 바론서비스(이하 \\\"서비스\\\")를 이용하는 고객(이하 \\\"이용자\\\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\\n제2조 (개인정보의 처리목적)\\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\\n- 제품소개서 다운로드: 설명자료 전달\\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\\n- 보안가이드 제공: 안내자료 전달\\n- 기술지원 문의: 서비스 사용 지원\\n- 서비스 개선 의견 접수: 서비스 품질 개선\\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\\n제3조 (개인정보의 처리 및 보유 기간)\\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\\n- 홍보, 상담, 계약용 개인정보: 2년\\n제4조 (개인정보의 제3자 제공)\\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\\n- 이용 목적: 개인정보 침해 민원 처리\\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\\n제5조 (개인정보 처리 위탁)\\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\\n제6조 (정보주체의 권리·의무 및 행사 방법)\\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\\n- 서면: 회사 주소로 서면 제출\\n- 전자우편: 회사 이메일로 요청\\n- 모사전송(FAX): 회사 FAX로 요청\\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\\n제7조 (처리하는 개인정보의 항목)\\n회사는 다음의 개인정보 항목을 처리합니다:\\n- 수집 항목:\\n- 필수 항목: 성명, 휴대전화번호, 이메일\\n- 선택 항목: 회사전화번호, 문의사항\\n- 수집 방법:\\n- 홈페이지, 전화, 이메일을 통해 수집\\n제8조 (개인정보의 파기)\\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 Line truncated
|
||||
tos_full = "\\n바론 소프트웨어 이용약관\\n\\n제1장 총칙\\n제1조 (목적)\\n이 약관은 바론컨설턴트(이하 \\\"회사\\\"라 합니다)가 제공하는 바론소프트웨어(이하 \\\"서비스\\\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\\n제2조 (용어의 정의)\\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\\n제3조 (약관의 효력 및 변경)\\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\\n제4조 (약관 외 준칙)\\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\\n제2장 서비스 이용계약\\n제5조 (이용계약의 성립)\\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\\n제6조 (이용계약의 유보와 거절)\\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\\n제7조 (계약사항의 변경)\\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\\n제3장 개인정보 보호\\n제8조 (개인정보 보호의 원칙)\\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\\n제9조 (개인정보처리방침 준수)\\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\\n제10조 (14세 미만 아동의 개인정보 보호)\\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\\n제4장 서비스 제공 및 이용\\n제11조 (서비스 제공)\\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\\n제12조 (서비스의 변경 및 중단)\\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\\n제5장 정보 제공 및 광고\\n제13조 (정보 제공 및 광고)\\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\\n제6장 게시물 관리\\n제14조 (게시물의 관리)\\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\\n제15조 (게시물의 저작권)\\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\\n제7장 계약 해지 및 이용 제한\\n제16조 (계약 해지)\\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\\n제17조 (이용 제한)\\n회사는 회원이 Line truncated
|
||||
|
||||
[msg.userfront.signup.agreement]
|
||||
title = "서비스 이용을 위해\\\\n약관에 동의해주세요"
|
||||
|
||||
[msg.userfront.signup.auth]
|
||||
affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요."
|
||||
title = "본인 확인을 위해\\\\n인증을 진행해주세요"
|
||||
|
||||
[msg.userfront.signup.email]
|
||||
code_mismatch = "인증코드가 일치하지 않습니다."
|
||||
duplicate = "이미 가입된 이메일입니다."
|
||||
invalid = "유효한 이메일 형식이 아닙니다."
|
||||
send_failed = "발송 실패: {error}"
|
||||
verified = "✅ 이메일 인증 완료"
|
||||
verify_failed = "인증 실패: {error}"
|
||||
|
||||
[msg.userfront.signup.password]
|
||||
length_required = "비밀번호는 최소 12자 이상이어야 합니다."
|
||||
lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다."
|
||||
mismatch = "비밀번호가 일치하지 않습니다."
|
||||
number_required = "숫자가 최소 1개 이상 포함되어야 합니다."
|
||||
symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다."
|
||||
title = "마지막으로\\\\n비밀번호를 설정해주세요"
|
||||
uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다."
|
||||
|
||||
[msg.userfront.signup.password.rule]
|
||||
lowercase = "소문자"
|
||||
min_length = "{count}자 이상"
|
||||
min_types = "문자 유형 {count}가지 이상"
|
||||
number = "숫자"
|
||||
symbol = "특수문자"
|
||||
uppercase = "대문자"
|
||||
|
||||
[msg.userfront.signup.phone]
|
||||
code_mismatch = "인증코드가 일치하지 않습니다."
|
||||
send_failed = "발송 실패: {error}"
|
||||
verified = "✅ 휴대폰 인증 완료"
|
||||
verify_failed = "인증 실패: {error}"
|
||||
|
||||
[msg.userfront.signup.policy]
|
||||
loading = "비밀번호 정책을 불러오는 중입니다..."
|
||||
lowercase = "소문자"
|
||||
min_length = "최소 {count}자 이상"
|
||||
min_types = "영문 대/소문자/숫자/특수문자 중 {count}가지 이상"
|
||||
number = "숫자"
|
||||
summary = "보안 정책: {rules}"
|
||||
symbol = "특수문자"
|
||||
uppercase = "대문자"
|
||||
|
||||
[msg.userfront.signup.profile]
|
||||
affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다."
|
||||
title = "회원님의\\\\n소속 정보를 알려주세요"
|
||||
|
||||
[msg.userfront.signup.success]
|
||||
body = "성공적으로 가입되었습니다."
|
||||
title = "회원가입 완료"
|
||||
|
||||
[ui.common]
|
||||
add = "추가"
|
||||
back = "돌아가기"
|
||||
cancel = "취소"
|
||||
close = "닫기"
|
||||
collapse = "접기"
|
||||
confirm = "확인"
|
||||
copy = "복사"
|
||||
create = "생성"
|
||||
delete = "삭제"
|
||||
details = "상세정보"
|
||||
edit = "편집"
|
||||
hyphen = "-"
|
||||
na = "N/A"
|
||||
never = "Never"
|
||||
next = "Next"
|
||||
page_of = "Page {page} of {total}"
|
||||
prev = "이전"
|
||||
previous = "Previous"
|
||||
qr = "QR"
|
||||
read_only = "읽기 전용"
|
||||
refresh = "새로고침"
|
||||
requesting = "요청 중..."
|
||||
resend = "재발송"
|
||||
retry = "다시 시도"
|
||||
save = "저장"
|
||||
search = "검색"
|
||||
show_more = "+ 더보기"
|
||||
language = "언어"
|
||||
language_ko = "한국어"
|
||||
language_en = "영어"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "테마 전환"
|
||||
unknown = "Unknown"
|
||||
|
||||
[ui.common.badge]
|
||||
admin_only = "Admin only"
|
||||
command_only = "Command only"
|
||||
system = "System"
|
||||
|
||||
[ui.common.role]
|
||||
admin = "Admin"
|
||||
user = "User"
|
||||
|
||||
[ui.common.status]
|
||||
active = "Active"
|
||||
blocked = "Blocked"
|
||||
failure = "실패"
|
||||
inactive = "Inactive"
|
||||
ok = "정상"
|
||||
pending = "준비 중"
|
||||
success = "성공"
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "Baron SW 포탈"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
baron = "Baron 로그인"
|
||||
dev_console = "Dev Console"
|
||||
|
||||
[ui.userfront.audit]
|
||||
|
||||
[ui.userfront.audit.table]
|
||||
app = "애플리케이션"
|
||||
auth_method = "인증수단"
|
||||
date = "접속일자"
|
||||
device = "접속환경"
|
||||
ip = "IP"
|
||||
pending = "(준비중)"
|
||||
result = "인증결과"
|
||||
session_id = "Session ID"
|
||||
status = "현황"
|
||||
|
||||
[ui.userfront.auth_method]
|
||||
ory = "Ory 세션"
|
||||
session = "세션"
|
||||
|
||||
[ui.userfront.dashboard]
|
||||
last_auth_label = "최근 인증"
|
||||
status_history = "상태 이력"
|
||||
|
||||
[ui.userfront.dashboard.activity]
|
||||
linked = "연동됨"
|
||||
|
||||
[ui.userfront.dashboard.approved_session]
|
||||
default = "승인한 세션 ID"
|
||||
userfront = "승인한 Userfront 세션 ID"
|
||||
|
||||
[ui.userfront.dashboard.revoke]
|
||||
confirm_button = "해지하기"
|
||||
title = "연동 해지"
|
||||
|
||||
[ui.userfront.dashboard.scopes]
|
||||
title = "권한 (Scopes)"
|
||||
|
||||
[ui.userfront.dashboard.status]
|
||||
revoked = "해지됨"
|
||||
|
||||
[ui.userfront.device]
|
||||
android = "Mobile(Android)"
|
||||
ios = "Mobile(iOS)"
|
||||
linux = "Desktop(Linux)"
|
||||
macos = "Desktop(macOS)"
|
||||
windows = "Desktop(Windows)"
|
||||
|
||||
[ui.userfront.error]
|
||||
go_home = "홈으로 이동"
|
||||
go_login = "로그인으로 이동"
|
||||
|
||||
[ui.userfront.forgot]
|
||||
heading = "비밀번호를 잊으셨나요?"
|
||||
input_label = "이메일 또는 휴대폰 번호"
|
||||
submit = "재설정 링크 전송"
|
||||
title = "비밀번호 재설정"
|
||||
|
||||
[ui.userfront.login]
|
||||
forgot_password = "비밀번호를 잊으셨나요?"
|
||||
signup = "회원가입"
|
||||
|
||||
[ui.userfront.login.action]
|
||||
submit = "로그인"
|
||||
|
||||
[ui.userfront.login.field]
|
||||
login_id = "이메일 또는 휴대폰 번호"
|
||||
password = "비밀번호"
|
||||
|
||||
[ui.userfront.login.link]
|
||||
action_label = "로그인 화면으로 이동"
|
||||
code_only = "코드만 받기({time})"
|
||||
page_title = "링크 로그인"
|
||||
resend_with_time = "재발송 ({time})"
|
||||
send = "로그인 링크 전송"
|
||||
title = "링크 로그인 완료"
|
||||
|
||||
[ui.userfront.login.qr]
|
||||
expired = "QR 코드 만료됨"
|
||||
refresh = "QR 코드 새로고침"
|
||||
remaining = "남은 시간: {time}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = "숫자 6자리"
|
||||
expire_time = "유효시간 {time}"
|
||||
prefix = "영문 2자리"
|
||||
submit = "코드로 로그인"
|
||||
|
||||
[ui.userfront.login.tabs]
|
||||
link = "로그인 링크"
|
||||
password = "비밀번호"
|
||||
qr = "QR 코드"
|
||||
|
||||
[ui.userfront.login.unregistered]
|
||||
action = "회원가입 하기"
|
||||
title = "미등록 회원"
|
||||
|
||||
[ui.userfront.login.verification]
|
||||
action_label = "확인"
|
||||
page_title = "로그인 승인"
|
||||
title = "승인 완료"
|
||||
|
||||
[ui.userfront.login_success]
|
||||
later = "나중에 하기 (대시보드로 이동)"
|
||||
qr = "QR 인증 (카메라 켜기)"
|
||||
title = "로그인 완료"
|
||||
|
||||
[ui.userfront.nav]
|
||||
dashboard = "대시보드"
|
||||
logout = "로그아웃"
|
||||
profile = "내 정보"
|
||||
qr_scan = "QR 스캔"
|
||||
|
||||
[ui.userfront.profile]
|
||||
department_empty = "소속 정보 없음"
|
||||
manage = "프로필 관리"
|
||||
user_fallback = "User"
|
||||
|
||||
[ui.userfront.profile.field]
|
||||
affiliation = "구분"
|
||||
company_code = "회사코드"
|
||||
department = "소속"
|
||||
email = "이메일"
|
||||
name = "이름"
|
||||
tenant = "소속 테넌트"
|
||||
|
||||
[ui.userfront.profile.password]
|
||||
change = "비밀번호 변경"
|
||||
confirm = "새 비밀번호 확인"
|
||||
current = "현재 비밀번호"
|
||||
forgot = "비밀번호를 잊으셨나요?"
|
||||
new = "새 비밀번호"
|
||||
title = "비밀번호 변경"
|
||||
|
||||
[ui.userfront.profile.phone]
|
||||
code_hint = "인증번호 6자리"
|
||||
request_code = "인증요청"
|
||||
title = "전화번호"
|
||||
|
||||
[ui.userfront.profile.section]
|
||||
basic = "기본 정보"
|
||||
organization = "조직 정보"
|
||||
security = "보안"
|
||||
|
||||
[ui.userfront.qr]
|
||||
request_permission = "카메라 권한 요청하기"
|
||||
rescan = "다시 스캔"
|
||||
result_failure = "승인 실패"
|
||||
result_success = "승인 완료"
|
||||
title = "Scan QR Code"
|
||||
|
||||
[ui.userfront.reset]
|
||||
confirm_password = "새 비밀번호 확인"
|
||||
new_password = "새 비밀번호"
|
||||
submit = "비밀번호 변경"
|
||||
subtitle = "새로운 비밀번호 설정"
|
||||
title = "새 비밀번호 설정"
|
||||
|
||||
[ui.userfront.sections]
|
||||
apps = "나의 App 현황"
|
||||
audit = "접속이력"
|
||||
|
||||
[ui.userfront.session]
|
||||
active = "세션 활성"
|
||||
unknown = "알 수 없음"
|
||||
|
||||
[ui.userfront.signup]
|
||||
complete = "가입 완료"
|
||||
next_step = "다음 단계"
|
||||
title = "회원가입"
|
||||
|
||||
[ui.userfront.signup.agreement]
|
||||
all = "모두 동의합니다"
|
||||
privacy_title = "개인정보 수집 및 이용 동의 (필수)"
|
||||
tos_title = "바론 소프트웨어 이용약관 (필수)"
|
||||
|
||||
[ui.userfront.signup.auth]
|
||||
code_label = "인증코드 6자리"
|
||||
request_code = "인증요청"
|
||||
|
||||
[ui.userfront.signup.auth.email]
|
||||
label = "이메일 주소"
|
||||
title = "이메일 인증"
|
||||
|
||||
[ui.userfront.signup.password]
|
||||
confirm_label = "비밀번호 확인"
|
||||
label = "비밀번호"
|
||||
|
||||
[ui.userfront.signup.phone]
|
||||
label = "휴대폰 번호 (-없이)"
|
||||
title = "휴대폰 인증"
|
||||
|
||||
[ui.userfront.signup.profile]
|
||||
affiliation_type = "소속 유형"
|
||||
company = "가족사 선택"
|
||||
department = "부서명"
|
||||
department_optional = "소속 정보 (선택)"
|
||||
name = "이름"
|
||||
|
||||
[ui.userfront.signup.steps]
|
||||
agreement = "약관동의"
|
||||
password = "비밀번호"
|
||||
profile = "정보입력"
|
||||
verify = "본인인증"
|
||||
|
||||
[ui.userfront.signup.success]
|
||||
action = "로그인하기"
|
||||
@@ -0,0 +1,536 @@
|
||||
[err.userfront]
|
||||
|
||||
[err.userfront.auth_proxy]
|
||||
consent_accept = ""
|
||||
consent_fetch = ""
|
||||
consent_reject = ""
|
||||
linked_app_revoke = ""
|
||||
login_failed = ""
|
||||
oidc_accept = ""
|
||||
password_reset_complete = ""
|
||||
password_reset_init = ""
|
||||
|
||||
[err.userfront.profile]
|
||||
load_failed = ""
|
||||
password_change_failed = ""
|
||||
send_code_failed = ""
|
||||
update_failed = ""
|
||||
verify_code_failed = ""
|
||||
|
||||
[err.userfront.session]
|
||||
missing = ""
|
||||
|
||||
[msg.userfront]
|
||||
greeting = ""
|
||||
|
||||
[msg.userfront.audit]
|
||||
date = ""
|
||||
device = ""
|
||||
end = ""
|
||||
ip = ""
|
||||
load_more_error = ""
|
||||
result = ""
|
||||
session_id = ""
|
||||
status = ""
|
||||
|
||||
[msg.userfront.dashboard]
|
||||
approved_device = ""
|
||||
approved_ip = ""
|
||||
audit_empty = ""
|
||||
audit_load_error = ""
|
||||
auth_method = ""
|
||||
client_id = ""
|
||||
client_id_missing = ""
|
||||
current_status = ""
|
||||
last_auth = ""
|
||||
link_missing = ""
|
||||
link_open_error = ""
|
||||
session_id_copied = ""
|
||||
|
||||
[msg.userfront.dashboard.activities]
|
||||
empty = ""
|
||||
empty_detail = ""
|
||||
error = ""
|
||||
|
||||
[msg.userfront.dashboard.approved_session]
|
||||
copy_click = ""
|
||||
copy_tap = ""
|
||||
none = ""
|
||||
|
||||
[msg.userfront.dashboard.revoke]
|
||||
confirm = ""
|
||||
error = ""
|
||||
success = ""
|
||||
|
||||
[msg.userfront.dashboard.scopes]
|
||||
empty = ""
|
||||
|
||||
[msg.userfront.dashboard.timeline]
|
||||
load_error = ""
|
||||
|
||||
[msg.userfront.error]
|
||||
detail_contact = ""
|
||||
detail_generic = ""
|
||||
detail_request = ""
|
||||
id = ""
|
||||
title = ""
|
||||
title_generic = ""
|
||||
title_with_code = ""
|
||||
type = ""
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
settings_disabled = ""
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = ""
|
||||
dry_send = ""
|
||||
error = ""
|
||||
input_required = ""
|
||||
sent = ""
|
||||
|
||||
[msg.userfront.login]
|
||||
cookie_check_failed = ""
|
||||
dry_send = ""
|
||||
link_failed = ""
|
||||
link_send_failed = ""
|
||||
link_sent_email = ""
|
||||
link_sent_phone = ""
|
||||
link_timeout = ""
|
||||
no_account = ""
|
||||
oidc_failed = ""
|
||||
qr_expired = ""
|
||||
qr_init_failed = ""
|
||||
qr_login_required = ""
|
||||
token_missing = ""
|
||||
verification_failed = ""
|
||||
|
||||
[msg.userfront.login.link]
|
||||
approved = ""
|
||||
helper = ""
|
||||
missing_login_id = ""
|
||||
missing_phone = ""
|
||||
resend_wait = ""
|
||||
short_code_help = ""
|
||||
|
||||
[msg.userfront.login.password]
|
||||
failed = ""
|
||||
missing_credentials = ""
|
||||
|
||||
[msg.userfront.login.qr]
|
||||
load_failed = ""
|
||||
scan_hint = ""
|
||||
|
||||
[msg.userfront.login.short_code]
|
||||
invalid = ""
|
||||
|
||||
[msg.userfront.login.unregistered]
|
||||
body = ""
|
||||
|
||||
[msg.userfront.login.verification]
|
||||
approved = ""
|
||||
approved_local = ""
|
||||
success = ""
|
||||
|
||||
[msg.userfront.login_success]
|
||||
subtitle = ""
|
||||
|
||||
[msg.userfront.profile]
|
||||
department_missing = ""
|
||||
department_required = ""
|
||||
email_missing = ""
|
||||
greeting = ""
|
||||
load_failed = ""
|
||||
name_missing = ""
|
||||
name_required = ""
|
||||
phone_required = ""
|
||||
phone_verify_required = ""
|
||||
update_failed = ""
|
||||
update_success = ""
|
||||
|
||||
[msg.userfront.profile.password]
|
||||
change_failed = ""
|
||||
changed = ""
|
||||
current_required = ""
|
||||
mismatch = ""
|
||||
new_required = ""
|
||||
subtitle = ""
|
||||
|
||||
[msg.userfront.profile.phone]
|
||||
code_sent = ""
|
||||
send_failed = ""
|
||||
verified = ""
|
||||
verify_failed = ""
|
||||
verify_notice = ""
|
||||
|
||||
[msg.userfront.profile.section]
|
||||
basic = ""
|
||||
organization = ""
|
||||
security = ""
|
||||
|
||||
[msg.userfront.qr]
|
||||
approve_error = ""
|
||||
approve_success = ""
|
||||
camera_error = ""
|
||||
permission_error = ""
|
||||
permission_required = ""
|
||||
|
||||
[msg.userfront.reset]
|
||||
invalid_body = ""
|
||||
invalid_link = ""
|
||||
invalid_title = ""
|
||||
policy_loading = ""
|
||||
success = ""
|
||||
|
||||
[msg.userfront.reset.error]
|
||||
empty_password = ""
|
||||
generic = ""
|
||||
lowercase = ""
|
||||
min_length = ""
|
||||
min_types = ""
|
||||
mismatch = ""
|
||||
number = ""
|
||||
symbol = ""
|
||||
uppercase = ""
|
||||
|
||||
[msg.userfront.reset.policy]
|
||||
lowercase = ""
|
||||
min_length = ""
|
||||
min_types = ""
|
||||
number = ""
|
||||
symbol = ""
|
||||
uppercase = ""
|
||||
|
||||
[msg.userfront.sections]
|
||||
apps_subtitle = ""
|
||||
audit_subtitle = ""
|
||||
|
||||
[msg.userfront.settings]
|
||||
disabled = ""
|
||||
|
||||
[msg.userfront.signup]
|
||||
failed = ""
|
||||
privacy_full = ""
|
||||
tos_full = ""
|
||||
|
||||
[msg.userfront.signup.agreement]
|
||||
title = ""
|
||||
|
||||
[msg.userfront.signup.auth]
|
||||
affiliate_notice = ""
|
||||
title = ""
|
||||
|
||||
[msg.userfront.signup.email]
|
||||
code_mismatch = ""
|
||||
duplicate = ""
|
||||
invalid = ""
|
||||
send_failed = ""
|
||||
verified = ""
|
||||
verify_failed = ""
|
||||
|
||||
[msg.userfront.signup.password]
|
||||
length_required = ""
|
||||
lowercase_required = ""
|
||||
mismatch = ""
|
||||
number_required = ""
|
||||
symbol_required = ""
|
||||
title = ""
|
||||
uppercase_required = ""
|
||||
|
||||
[msg.userfront.signup.password.rule]
|
||||
lowercase = ""
|
||||
min_length = ""
|
||||
min_types = ""
|
||||
number = ""
|
||||
symbol = ""
|
||||
uppercase = ""
|
||||
|
||||
[msg.userfront.signup.phone]
|
||||
code_mismatch = ""
|
||||
send_failed = ""
|
||||
verified = ""
|
||||
verify_failed = ""
|
||||
|
||||
[msg.userfront.signup.policy]
|
||||
loading = ""
|
||||
lowercase = ""
|
||||
min_length = ""
|
||||
min_types = ""
|
||||
number = ""
|
||||
summary = ""
|
||||
symbol = ""
|
||||
uppercase = ""
|
||||
|
||||
[msg.userfront.signup.profile]
|
||||
affiliate_hint = ""
|
||||
title = ""
|
||||
|
||||
[msg.userfront.signup.success]
|
||||
body = ""
|
||||
title = ""
|
||||
|
||||
[ui.common]
|
||||
add = ""
|
||||
back = ""
|
||||
cancel = ""
|
||||
close = ""
|
||||
collapse = ""
|
||||
confirm = ""
|
||||
copy = ""
|
||||
create = ""
|
||||
delete = ""
|
||||
details = ""
|
||||
edit = ""
|
||||
hyphen = ""
|
||||
na = ""
|
||||
never = ""
|
||||
next = ""
|
||||
page_of = ""
|
||||
prev = ""
|
||||
previous = ""
|
||||
qr = ""
|
||||
read_only = ""
|
||||
refresh = ""
|
||||
requesting = ""
|
||||
resend = ""
|
||||
retry = ""
|
||||
save = ""
|
||||
search = ""
|
||||
show_more = ""
|
||||
language = ""
|
||||
language_ko = ""
|
||||
language_en = ""
|
||||
theme_dark = ""
|
||||
theme_light = ""
|
||||
theme_toggle = ""
|
||||
unknown = ""
|
||||
|
||||
[ui.common.badge]
|
||||
admin_only = ""
|
||||
command_only = ""
|
||||
system = ""
|
||||
|
||||
[ui.common.role]
|
||||
admin = ""
|
||||
user = ""
|
||||
|
||||
[ui.common.status]
|
||||
active = ""
|
||||
blocked = ""
|
||||
failure = ""
|
||||
inactive = ""
|
||||
ok = ""
|
||||
pending = ""
|
||||
success = ""
|
||||
|
||||
[ui.userfront]
|
||||
app_title = ""
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = ""
|
||||
baron = ""
|
||||
dev_console = ""
|
||||
|
||||
[ui.userfront.audit]
|
||||
|
||||
[ui.userfront.audit.table]
|
||||
app = ""
|
||||
auth_method = ""
|
||||
date = ""
|
||||
device = ""
|
||||
ip = ""
|
||||
pending = ""
|
||||
result = ""
|
||||
session_id = ""
|
||||
status = ""
|
||||
|
||||
[ui.userfront.auth_method]
|
||||
ory = ""
|
||||
session = ""
|
||||
|
||||
[ui.userfront.dashboard]
|
||||
last_auth_label = ""
|
||||
status_history = ""
|
||||
|
||||
[ui.userfront.dashboard.activity]
|
||||
linked = ""
|
||||
|
||||
[ui.userfront.dashboard.approved_session]
|
||||
default = ""
|
||||
userfront = ""
|
||||
|
||||
[ui.userfront.dashboard.revoke]
|
||||
confirm_button = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.dashboard.scopes]
|
||||
title = ""
|
||||
|
||||
[ui.userfront.dashboard.status]
|
||||
revoked = ""
|
||||
|
||||
[ui.userfront.device]
|
||||
android = ""
|
||||
ios = ""
|
||||
linux = ""
|
||||
macos = ""
|
||||
windows = ""
|
||||
|
||||
[ui.userfront.error]
|
||||
go_home = ""
|
||||
go_login = ""
|
||||
|
||||
[ui.userfront.forgot]
|
||||
heading = ""
|
||||
input_label = ""
|
||||
submit = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.login]
|
||||
forgot_password = ""
|
||||
signup = ""
|
||||
|
||||
[ui.userfront.login.action]
|
||||
submit = ""
|
||||
|
||||
[ui.userfront.login.field]
|
||||
login_id = ""
|
||||
password = ""
|
||||
|
||||
[ui.userfront.login.link]
|
||||
action_label = ""
|
||||
code_only = ""
|
||||
page_title = ""
|
||||
resend_with_time = ""
|
||||
send = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.login.qr]
|
||||
expired = ""
|
||||
refresh = ""
|
||||
remaining = "Remaining: {time}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = ""
|
||||
expire_time = ""
|
||||
prefix = ""
|
||||
submit = ""
|
||||
|
||||
[ui.userfront.login.tabs]
|
||||
link = ""
|
||||
password = ""
|
||||
qr = ""
|
||||
|
||||
[ui.userfront.login.unregistered]
|
||||
action = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.login.verification]
|
||||
action_label = ""
|
||||
page_title = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.login_success]
|
||||
later = ""
|
||||
qr = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.nav]
|
||||
dashboard = ""
|
||||
logout = ""
|
||||
profile = ""
|
||||
qr_scan = ""
|
||||
|
||||
[ui.userfront.profile]
|
||||
department_empty = ""
|
||||
manage = ""
|
||||
user_fallback = ""
|
||||
|
||||
[ui.userfront.profile.field]
|
||||
affiliation = ""
|
||||
company_code = ""
|
||||
department = ""
|
||||
email = ""
|
||||
name = ""
|
||||
tenant = ""
|
||||
|
||||
[ui.userfront.profile.password]
|
||||
change = ""
|
||||
confirm = ""
|
||||
current = ""
|
||||
forgot = ""
|
||||
new = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.profile.phone]
|
||||
code_hint = ""
|
||||
request_code = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.profile.section]
|
||||
basic = ""
|
||||
organization = ""
|
||||
security = ""
|
||||
|
||||
[ui.userfront.qr]
|
||||
request_permission = ""
|
||||
rescan = ""
|
||||
result_failure = ""
|
||||
result_success = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.reset]
|
||||
confirm_password = ""
|
||||
new_password = ""
|
||||
submit = ""
|
||||
subtitle = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.sections]
|
||||
apps = ""
|
||||
audit = ""
|
||||
|
||||
[ui.userfront.session]
|
||||
active = ""
|
||||
unknown = ""
|
||||
|
||||
[ui.userfront.signup]
|
||||
complete = ""
|
||||
next_step = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.signup.agreement]
|
||||
all = ""
|
||||
privacy_title = ""
|
||||
tos_title = ""
|
||||
|
||||
[ui.userfront.signup.auth]
|
||||
code_label = ""
|
||||
request_code = ""
|
||||
|
||||
[ui.userfront.signup.auth.email]
|
||||
label = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.signup.password]
|
||||
confirm_label = ""
|
||||
label = ""
|
||||
|
||||
[ui.userfront.signup.phone]
|
||||
label = ""
|
||||
title = ""
|
||||
|
||||
[ui.userfront.signup.profile]
|
||||
affiliation_type = ""
|
||||
company = ""
|
||||
department = ""
|
||||
department_optional = ""
|
||||
name = ""
|
||||
|
||||
[ui.userfront.signup.steps]
|
||||
agreement = ""
|
||||
password = ""
|
||||
profile = ""
|
||||
verify = ""
|
||||
|
||||
[ui.userfront.signup.success]
|
||||
action = ""
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../services/web_window.dart';
|
||||
import 'locale_storage.dart';
|
||||
import 'locale_utils.dart';
|
||||
|
||||
class LocaleGate extends StatefulWidget {
|
||||
const LocaleGate({super.key, required this.localeCode, required this.child});
|
||||
|
||||
final String localeCode;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<LocaleGate> createState() => _LocaleGateState();
|
||||
}
|
||||
|
||||
class _LocaleGateState extends State<LocaleGate> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_applyLocale();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LocaleGate oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.localeCode != widget.localeCode) {
|
||||
_applyLocale();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyLocale() async {
|
||||
final normalized = normalizeLocaleCode(widget.localeCode);
|
||||
LocaleStorage.write(normalized);
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
if (context.locale.languageCode == normalized) {
|
||||
return;
|
||||
}
|
||||
await context.setLocale(Locale(normalized));
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'locale_storage_stub.dart'
|
||||
if (dart.library.html) 'locale_storage_web.dart';
|
||||
|
||||
abstract class LocaleStorage {
|
||||
static String? read() => localeStorage.read();
|
||||
static void write(String locale) => localeStorage.write(locale);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class LocaleStorageImpl {
|
||||
String? _locale;
|
||||
|
||||
String? read() => _locale;
|
||||
|
||||
void write(String locale) {
|
||||
_locale = locale;
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
@@ -0,0 +1,120 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:html' as html;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class LocaleStorageImpl {
|
||||
static const _key = 'locale';
|
||||
static const _legacyKey = 'baron_locale';
|
||||
static final Map<String, String> _memory = {};
|
||||
static bool _forceMemory = false;
|
||||
static bool _forceSession = false;
|
||||
|
||||
@visibleForTesting
|
||||
static void forceMemoryStorageForTests(bool value) {
|
||||
_forceMemory = value;
|
||||
if (!value) {
|
||||
_memory.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void forceSessionStorageForTests(bool value) {
|
||||
_forceSession = value;
|
||||
}
|
||||
|
||||
String? _read(String key) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
return html.window.localStorage[key];
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
return html.window.sessionStorage[key];
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
return html.window.sessionStorage[key];
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
return _memory[key];
|
||||
}
|
||||
|
||||
void _write(String key, String value) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
html.window.localStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
html.window.sessionStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
html.window.sessionStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
_memory[key] = value;
|
||||
}
|
||||
|
||||
void _remove(String key) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
html.window.localStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
html.window.sessionStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
html.window.sessionStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
_memory.remove(key);
|
||||
}
|
||||
|
||||
String? read() {
|
||||
final current = _read(_key);
|
||||
if (current != null && current.isNotEmpty) {
|
||||
return current;
|
||||
}
|
||||
final legacy = _read(_legacyKey);
|
||||
if (legacy != null && legacy.isNotEmpty) {
|
||||
_write(_key, legacy);
|
||||
_remove(_legacyKey);
|
||||
return legacy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void write(String locale) {
|
||||
_write(_key, locale);
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'locale_storage.dart';
|
||||
|
||||
const supportedLocaleCodes = ['en', 'ko'];
|
||||
const defaultLocaleCode = 'en';
|
||||
|
||||
String normalizeLocaleCode(String? code) {
|
||||
if (code == null || code.isEmpty) {
|
||||
return defaultLocaleCode;
|
||||
}
|
||||
final normalized = code.toLowerCase();
|
||||
if (normalized == 'ko' || normalized.startsWith('ko-')) {
|
||||
return 'ko';
|
||||
}
|
||||
if (normalized == 'en' || normalized.startsWith('en-')) {
|
||||
return 'en';
|
||||
}
|
||||
return defaultLocaleCode;
|
||||
}
|
||||
|
||||
String resolvePreferredLocaleCode() {
|
||||
final stored = LocaleStorage.read();
|
||||
if (stored != null && supportedLocaleCodes.contains(stored)) {
|
||||
return stored;
|
||||
}
|
||||
final deviceLocale = PlatformDispatcher.instance.locale;
|
||||
return normalizeLocaleCode(deviceLocale.languageCode);
|
||||
}
|
||||
|
||||
String? extractLocaleFromPath(Uri uri) {
|
||||
if (uri.pathSegments.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final code = uri.pathSegments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(code)) {
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String stripLocalePath(Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.isNotEmpty && supportedLocaleCodes.contains(segments.first)) {
|
||||
final rest = segments.skip(1).join('/');
|
||||
if (rest.isEmpty) {
|
||||
return '/';
|
||||
}
|
||||
return '/$rest';
|
||||
}
|
||||
return uri.path;
|
||||
}
|
||||
|
||||
String buildLocalizedPath(String localeCode, Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
Iterable<String> restSegments = segments;
|
||||
if (segments.isNotEmpty) {
|
||||
final head = segments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(head) || head.length == 2) {
|
||||
restSegments = segments.skip(1);
|
||||
}
|
||||
}
|
||||
final newSegments = [localeCode, ...restSegments];
|
||||
final path = '/${newSegments.join('/')}';
|
||||
if (uri.queryParameters.isEmpty) {
|
||||
return path;
|
||||
}
|
||||
return Uri(path: path, queryParameters: uri.queryParameters).toString();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toml/toml.dart';
|
||||
|
||||
class TomlAssetLoader extends AssetLoader {
|
||||
const TomlAssetLoader();
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> load(String path, Locale locale) async {
|
||||
final assetPath = '$path/${locale.languageCode}.toml';
|
||||
try {
|
||||
final content = await rootBundle.loadString(assetPath);
|
||||
final document = TomlDocument.parse(content);
|
||||
return document.toMap();
|
||||
} catch (e) {
|
||||
// 로딩 실패 시 빈 맵을 반환해 렌더링을 지속합니다.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,25 @@ class AuthProxyService {
|
||||
if (!dotenv.isInitialized) {
|
||||
return fallback;
|
||||
}
|
||||
return dotenv.env[key] ?? fallback;
|
||||
final value = dotenv.env[key];
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static String _fallbackOrigin() {
|
||||
try {
|
||||
final origin = Uri.base.origin;
|
||||
if (origin.isNotEmpty && origin != 'null') {
|
||||
return origin;
|
||||
}
|
||||
} catch (_) {}
|
||||
return 'http://sso.hmac.kr';
|
||||
}
|
||||
|
||||
static String get _baseUrl {
|
||||
final rawUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
final rawUrl = _envOrDefault('BACKEND_URL', _fallbackOrigin());
|
||||
// 배포 환경에서 $ 기호나 공백이 섞여 들어오는 경우를 방지하기 위해 정제합니다.
|
||||
return rawUrl.replaceAll(r'$', '').trim().replaceAll(RegExp(r'/$'), '');
|
||||
}
|
||||
@@ -103,7 +117,7 @@ class AuthProxyService {
|
||||
bool? drySend,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/enchanted-link/init');
|
||||
final userfrontUrl = _envOrDefault('USERFRONT_URL', 'http://sso.hmac.kr');
|
||||
final userfrontUrl = _envOrDefault('USERFRONT_URL', _fallbackOrigin());
|
||||
|
||||
final body = <String, dynamic>{'loginId': loginId, 'uri': userfrontUrl};
|
||||
if (_shouldSendDrySend(drySend)) {
|
||||
@@ -269,7 +283,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.login_failed',
|
||||
fallback: '로그인에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -294,7 +307,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_fetch',
|
||||
fallback: '동의 정보를 가져오지 못했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -324,7 +336,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_accept',
|
||||
fallback: '동의 처리에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -350,7 +361,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_reject',
|
||||
fallback: '동의 거부에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -381,7 +391,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.oidc_accept',
|
||||
fallback: 'OIDC 로그인 승인에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -412,7 +421,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.password_reset_init',
|
||||
fallback: '비밀번호 재설정을 시작하지 못했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -447,7 +455,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.password_reset_complete',
|
||||
fallback: '비밀번호 재설정에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -780,7 +787,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.linked_app_revoke',
|
||||
fallback: '연동 해지에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class WebWindow {
|
||||
void setTitle(String title) {}
|
||||
|
||||
void redirectTo(String url) {}
|
||||
|
||||
void alert(String message) {}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
import 'dart:html' as html;
|
||||
|
||||
class WebWindow {
|
||||
void setTitle(String title) {
|
||||
html.document.title = title;
|
||||
}
|
||||
|
||||
void redirectTo(String url) {
|
||||
html.window.location.href = url;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
|
||||
import '../i18n/locale_storage.dart';
|
||||
import '../i18n/locale_utils.dart';
|
||||
|
||||
class LanguageSelector extends StatelessWidget {
|
||||
const LanguageSelector({super.key, this.compact = false});
|
||||
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final current = context.locale.languageCode;
|
||||
final items = [
|
||||
DropdownMenuItem(
|
||||
value: 'ko',
|
||||
child: Text(tr('ui.common.language_ko')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'en',
|
||||
child: Text(tr('ui.common.language_en', fallback: 'English')),
|
||||
),
|
||||
];
|
||||
|
||||
final iconSize = compact ? 16.0 : 18.0;
|
||||
final dropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: current,
|
||||
items: items,
|
||||
isDense: true,
|
||||
icon: Icon(Icons.arrow_drop_down, size: compact ? 18 : 20),
|
||||
onChanged: (value) async {
|
||||
if (value == null || value == current) {
|
||||
return;
|
||||
}
|
||||
LocaleStorage.write(value);
|
||||
await context.setLocale(Locale(value));
|
||||
final uri = GoRouterState.of(context).uri;
|
||||
final target = buildLocalizedPath(value, uri);
|
||||
if (context.mounted) {
|
||||
context.go(target);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: compact ? 0 : 2),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: compact ? 24 : 28),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.language, size: iconSize),
|
||||
const SizedBox(width: 6),
|
||||
dropdown,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,16 +30,14 @@ class ErrorScreen extends StatelessWidget {
|
||||
? (isWhitelisted && hasCode ? normalizedCode : 'unknown_error')
|
||||
: (hasCode ? normalizedCode : 'unknown_error');
|
||||
final title = isProd
|
||||
? tr('msg.userfront.error.title', fallback: '인증 과정에서 오류가 발생했습니다')
|
||||
? tr('msg.userfront.error.title')
|
||||
: (hasCode
|
||||
? tr(
|
||||
'msg.userfront.error.title_with_code',
|
||||
fallback: '오류: {{code}}',
|
||||
params: {'code': normalizedCode},
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.title_generic',
|
||||
fallback: '오류가 발생했습니다',
|
||||
));
|
||||
final detail = isProd
|
||||
? (isWhitelisted
|
||||
@@ -49,18 +47,15 @@ class ErrorScreen extends StatelessWidget {
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.detail_contact',
|
||||
fallback: '에러가 계속되면 관리자에게 문의해주세요',
|
||||
))
|
||||
: ((description?.isNotEmpty == true)
|
||||
? description!
|
||||
: (hasCode
|
||||
? tr(
|
||||
'msg.userfront.error.detail_generic',
|
||||
fallback: '오류가 발생했습니다.',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.detail_request',
|
||||
fallback: '요청을 처리하는 중 문제가 발생했습니다.',
|
||||
)));
|
||||
|
||||
return Scaffold(
|
||||
@@ -100,7 +95,6 @@ class ErrorScreen extends StatelessWidget {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.error.type',
|
||||
fallback: '오류 종류: {{type}}',
|
||||
params: {'type': errorType},
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
@@ -112,7 +106,6 @@ class ErrorScreen extends StatelessWidget {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.error.id',
|
||||
fallback: '오류 ID: {{id}}',
|
||||
params: {'id': errorId!},
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
@@ -141,7 +134,6 @@ class ErrorScreen extends StatelessWidget {
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.error.go_login',
|
||||
fallback: '로그인으로 이동',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -159,7 +151,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tr('ui.userfront.error.go_home', fallback: '홈으로 이동'),
|
||||
tr('ui.userfront.error.go_home'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -28,7 +28,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.forgot.input_required',
|
||||
fallback: '이메일 또는 휴대폰 번호를 입력해주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -56,7 +55,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.forgot.sent',
|
||||
fallback: '비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요.',
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
@@ -69,7 +67,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.forgot.error',
|
||||
fallback: '전송에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -99,7 +96,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr('ui.userfront.forgot.title', fallback: '비밀번호 재설정')),
|
||||
title: Text(tr('ui.userfront.forgot.title')),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Center(
|
||||
@@ -111,7 +108,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.forgot.heading', fallback: '비밀번호를 잊으셨나요?'),
|
||||
tr('ui.userfront.forgot.heading'),
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -138,7 +135,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.forgot.dry_send',
|
||||
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8A6D3B),
|
||||
@@ -154,8 +150,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.forgot.description',
|
||||
fallback:
|
||||
'계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
@@ -166,7 +160,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.forgot.input_label',
|
||||
fallback: '이메일 또는 휴대폰 번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
@@ -189,7 +182,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
tr('ui.userfront.forgot.submit', fallback: '재설정 링크 전송'),
|
||||
tr('ui.userfront.forgot.submit'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../../../core/widgets/language_selector.dart';
|
||||
import '../../../core/services/web_auth_integration.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/services/auth_token_store.dart';
|
||||
@@ -43,6 +44,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
String? _qrImageBase64;
|
||||
String? _qrPendingRef;
|
||||
bool _isQrLoading = false;
|
||||
bool _qrExpired = false;
|
||||
Timer? _qrPollingTimer;
|
||||
int _qrRemainingSeconds = 0;
|
||||
Timer? _qrCountdownTimer;
|
||||
@@ -58,20 +60,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Timer? _linkResendTimer;
|
||||
int _linkExpireSeconds = 0;
|
||||
Timer? _linkExpireTimer;
|
||||
bool _linkExpired = false;
|
||||
bool _verificationOnly = false;
|
||||
bool _verificationApproved = false;
|
||||
bool _dismissedOverlays = false;
|
||||
String _verificationMessage = '';
|
||||
String _verificationTitle = tr(
|
||||
'ui.userfront.login.verification.title',
|
||||
fallback: '승인 완료',
|
||||
);
|
||||
String _verificationPageTitle = tr(
|
||||
'ui.userfront.login.verification.page_title',
|
||||
fallback: '로그인 승인',
|
||||
);
|
||||
String _verificationActionLabel = tr(
|
||||
'ui.userfront.login.verification.action_label',
|
||||
fallback: '확인',
|
||||
);
|
||||
Timer? _verificationRedirectTimer;
|
||||
bool _noticeHandled = false;
|
||||
@@ -129,7 +130,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showInfo(
|
||||
tr(
|
||||
'msg.userfront.login.qr_login_required',
|
||||
fallback: '로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -163,7 +163,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.cookie_check_failed',
|
||||
fallback: '로그인 확인 실패: {{error}}',
|
||||
params: {'error': e.toString().replaceFirst('Exception: ', '')},
|
||||
),
|
||||
);
|
||||
@@ -255,10 +254,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_linkExpireTimer?.cancel();
|
||||
_linkExpireTimer = null;
|
||||
_linkExpireSeconds = 0;
|
||||
_linkExpired = false;
|
||||
_shortCodePrefixController.clear();
|
||||
_shortCodeDigitsController.clear();
|
||||
}
|
||||
|
||||
void _dismissOverlays() {
|
||||
if (!mounted || _dismissedOverlays) {
|
||||
return;
|
||||
}
|
||||
_dismissedOverlays = true;
|
||||
final navigator = Navigator.of(context, rootNavigator: true);
|
||||
navigator.popUntil((route) => route is! PopupRoute);
|
||||
}
|
||||
|
||||
bool _parseBoolParam(String? value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
@@ -295,8 +304,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
timer.cancel();
|
||||
if (mounted) {
|
||||
setState(_resetLinkLoginState);
|
||||
context.go('/signin');
|
||||
setState(() {
|
||||
_linkExpired = true;
|
||||
});
|
||||
_showInfo(
|
||||
tr('msg.userfront.login.link_timeout'),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -331,6 +344,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_isQrLoading = true;
|
||||
_qrImageBase64 = null;
|
||||
_qrRemainingSeconds = 0;
|
||||
_qrExpired = false;
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -355,7 +369,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.qr_init_failed',
|
||||
fallback: 'QR 초기화에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -368,7 +381,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_qrCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!mounted || _qrRemainingSeconds <= 0) {
|
||||
timer.cancel();
|
||||
if (_qrRemainingSeconds <= 0) _stopQrPolling();
|
||||
if (_qrRemainingSeconds <= 0) {
|
||||
_stopQrPolling();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_qrExpired = true;
|
||||
});
|
||||
_showInfo(
|
||||
tr('msg.userfront.login.qr_expired'),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
@@ -412,8 +435,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (res['error'] == 'expired_token') {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_qrExpired = true;
|
||||
});
|
||||
}
|
||||
_showError(
|
||||
tr('msg.userfront.login.qr_expired', fallback: 'QR 세션이 만료되었습니다.'),
|
||||
tr('msg.userfront.login.qr_expired'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -421,14 +449,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (res['status'] == 'ok') {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
final token = res['sessionJwt'] ?? res['token'];
|
||||
final token =
|
||||
res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
|
||||
if (token is String && token.isNotEmpty) {
|
||||
_completeLoginFromToken(token);
|
||||
} else {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.token_missing',
|
||||
fallback: '로그인 토큰을 확인할 수 없습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -506,13 +534,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}) {
|
||||
if (!mounted) return;
|
||||
final resolvedTitle =
|
||||
title ?? tr('ui.userfront.login.verification.title', fallback: '승인 완료');
|
||||
title ?? tr('ui.userfront.login.verification.title');
|
||||
final resolvedPageTitle =
|
||||
pageTitle ??
|
||||
tr('ui.userfront.login.verification.page_title', fallback: '로그인 승인');
|
||||
tr('ui.userfront.login.verification.page_title');
|
||||
final resolvedActionLabel =
|
||||
actionLabel ??
|
||||
tr('ui.userfront.login.verification.action_label', fallback: '확인');
|
||||
tr('ui.userfront.login.verification.action_label');
|
||||
setState(() {
|
||||
_verificationApproved = true;
|
||||
_verificationMessage = message;
|
||||
@@ -555,7 +583,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_verificationMessage.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.login.verification.success',
|
||||
fallback: '로그인 승인에 성공했습니다.',
|
||||
)
|
||||
: _verificationMessage,
|
||||
textAlign: TextAlign.center,
|
||||
@@ -588,11 +615,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint("[Auth] Starting verification for token: $token");
|
||||
final approvedMessage = tr(
|
||||
'msg.userfront.login.verification.approved',
|
||||
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
|
||||
);
|
||||
final localSessionMessage = tr(
|
||||
'msg.userfront.login.verification.approved_local',
|
||||
fallback: '승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다',
|
||||
);
|
||||
try {
|
||||
// Use Backend to verify the token (Backend-Driven Flow)
|
||||
@@ -601,7 +626,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
verifyOnly: _verificationOnly,
|
||||
);
|
||||
debugPrint("[Auth] Verification successful for token: $token");
|
||||
final jwt = res['token'] ?? res['sessionJwt'];
|
||||
final jwt = res['token'] ?? res['sessionJwt'] ?? res['sessionToken'];
|
||||
final status = res['status']?.toString();
|
||||
final hasLocalSession = await _hasValidLocalSession();
|
||||
final actionPath = hasLocalSession ? '/' : '/signin';
|
||||
@@ -634,7 +659,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.verification_failed',
|
||||
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -653,15 +677,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
);
|
||||
final approvedMessage = tr(
|
||||
'msg.userfront.login.verification.approved',
|
||||
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
|
||||
);
|
||||
final localSessionMessage = tr(
|
||||
'msg.userfront.login.verification.approved_local',
|
||||
fallback: '승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다',
|
||||
);
|
||||
final linkLoginMessage = tr(
|
||||
'msg.userfront.login.link.approved',
|
||||
fallback: '링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.',
|
||||
);
|
||||
try {
|
||||
final res = await AuthProxyService.verifyLoginCode(
|
||||
@@ -670,7 +691,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
pendingRef: pendingRef,
|
||||
verifyOnly: _verificationOnly,
|
||||
);
|
||||
final jwt = res['sessionJwt'] ?? res['token'];
|
||||
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
|
||||
final status = res['status']?.toString();
|
||||
debugPrint(
|
||||
"[Auth] Code verification successful for loginId: $sanitizedLoginId",
|
||||
@@ -699,14 +720,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
_markVerificationApproved(
|
||||
linkLoginMessage,
|
||||
title: tr('ui.userfront.login.link.title', fallback: '링크 로그인 완료'),
|
||||
title: tr('ui.userfront.login.link.title'),
|
||||
pageTitle: tr(
|
||||
'ui.userfront.login.link.page_title',
|
||||
fallback: '링크 로그인',
|
||||
),
|
||||
actionLabel: tr(
|
||||
'ui.userfront.login.link.action_label',
|
||||
fallback: '로그인 화면으로 이동',
|
||||
),
|
||||
actionPath: '/signin',
|
||||
autoRedirect: true,
|
||||
@@ -725,7 +744,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.verification_failed',
|
||||
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -739,18 +757,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint("[Auth] Starting short code verification for code: $sanitized");
|
||||
final approvedMessage = tr(
|
||||
'msg.userfront.login.verification.approved',
|
||||
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
|
||||
);
|
||||
final localSessionMessage = tr(
|
||||
'msg.userfront.login.verification.approved_local',
|
||||
fallback: '승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다',
|
||||
);
|
||||
try {
|
||||
final res = await AuthProxyService.verifyLoginShortCode(
|
||||
sanitized,
|
||||
verifyOnly: _verificationOnly,
|
||||
);
|
||||
final jwt = res['sessionJwt'] ?? res['token'];
|
||||
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
|
||||
final status = res['status']?.toString();
|
||||
debugPrint("[Auth] Short code verification successful");
|
||||
final hasLocalSession = await _hasValidLocalSession();
|
||||
@@ -788,7 +804,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.verification_failed',
|
||||
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -817,7 +832,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.password.missing_credentials',
|
||||
fallback: '이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -831,24 +845,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
|
||||
try {
|
||||
final res = await AuthProxyService.loginWithPassword(
|
||||
loginId,
|
||||
password,
|
||||
loginChallenge: _loginChallenge,
|
||||
);
|
||||
final jwt = res['sessionJwt'];
|
||||
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
|
||||
final provider = res['provider'] as String?;
|
||||
final redirectTo = res['redirectTo'] as String?;
|
||||
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
|
||||
if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||
webWindow.redirectTo(redirectTo);
|
||||
return;
|
||||
@@ -858,14 +864,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_onLoginSuccess(jwt, provider: provider);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
if (e.toString().contains("User not registered")) {
|
||||
_showUnregisteredDialog();
|
||||
} else {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.password.failed',
|
||||
fallback: '로그인 실패: {{error}}',
|
||||
params: {'error': e.toString().replaceFirst('Exception: ', '')},
|
||||
),
|
||||
);
|
||||
@@ -896,7 +900,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_failed',
|
||||
fallback: '오류: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -910,15 +913,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
bool codeOnly = false,
|
||||
}) async {
|
||||
try {
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final initResponse = await AuthProxyService.initEnchantedLink(
|
||||
loginId,
|
||||
codeOnly: codeOnly,
|
||||
@@ -939,18 +933,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_linkPendingRef = pendingRef?.toString();
|
||||
_lastLinkLoginId = loginId;
|
||||
_lastLinkIsEmail = isEmail;
|
||||
_linkExpired = false;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
|
||||
_dismissOverlays();
|
||||
_showInfo(
|
||||
isEmail
|
||||
? tr(
|
||||
'msg.userfront.login.link_sent_email',
|
||||
fallback: '입력하신 이메일로 로그인 링크를 보냈습니다.',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.login.link_sent_phone',
|
||||
fallback: '입력하신 번호로 로그인 링크를 보냈습니다.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -967,7 +959,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Initialization failed: $e");
|
||||
if (mounted && Navigator.canPop(context)) Navigator.of(context).pop();
|
||||
if (mounted) {
|
||||
setState(_resetLinkLoginState);
|
||||
}
|
||||
@@ -977,7 +968,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_send_failed',
|
||||
fallback: '전송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -1022,7 +1012,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_timeout',
|
||||
fallback: '로그인 요청 시간이 초과되었습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1030,7 +1019,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
|
||||
if (result['status'] == 'ok') {
|
||||
final token = result['sessionJwt'] ?? result['token'];
|
||||
final token =
|
||||
result['sessionJwt'] ?? result['sessionToken'] ?? result['token'];
|
||||
if (token is String && token.isNotEmpty) {
|
||||
debugPrint("[Auth] Polling SUCCESS. Token received.");
|
||||
_completeLoginFromToken(
|
||||
@@ -1047,7 +1037,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.token_missing',
|
||||
fallback: '로그인 토큰을 확인할 수 없습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1061,7 +1050,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
|
||||
Navigator.of(context).pop();
|
||||
_showError(
|
||||
tr('msg.userfront.login.link_timeout', fallback: '로그인 요청 시간이 초과되었습니다.'),
|
||||
tr('msg.userfront.login.link_timeout'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1119,6 +1108,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
AuthTokenStore.setToken(token, provider: providerName);
|
||||
AuthTokenStore.clearPendingProvider();
|
||||
_dismissOverlays();
|
||||
|
||||
try {
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
@@ -1142,7 +1132,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.oidc_failed',
|
||||
fallback: 'OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1175,18 +1164,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('ui.userfront.login.unregistered.title', fallback: '미등록 회원'),
|
||||
tr('ui.userfront.login.unregistered.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.login.unregistered.body',
|
||||
fallback: '가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요.',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
@@ -1195,7 +1183,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
context.push('/signup');
|
||||
},
|
||||
child: Text(
|
||||
tr('ui.userfront.login.unregistered.action', fallback: '회원가입 하기'),
|
||||
tr('ui.userfront.login.unregistered.action'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1233,7 +1221,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
tr('ui.userfront.app_title'),
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -1263,8 +1251,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.login.dry_send',
|
||||
fallback:
|
||||
'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8A6D3B),
|
||||
@@ -1284,19 +1270,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Tab(
|
||||
text: tr(
|
||||
'ui.userfront.login.tabs.password',
|
||||
fallback: '비밀번호',
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
text: tr(
|
||||
'ui.userfront.login.tabs.link',
|
||||
fallback: '로그인 링크',
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
text: tr(
|
||||
'ui.userfront.login.tabs.qr',
|
||||
fallback: 'QR 코드',
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1317,7 +1300,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.field.login_id',
|
||||
fallback: '이메일 또는 휴대폰 번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(
|
||||
@@ -1333,7 +1315,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.field.password',
|
||||
fallback: '비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(
|
||||
@@ -1351,7 +1332,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.action.submit',
|
||||
fallback: '로그인',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1369,7 +1349,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.field.login_id',
|
||||
fallback: '이메일 또는 휴대폰 번호',
|
||||
),
|
||||
hintText: '',
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -1388,7 +1367,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.link.send',
|
||||
fallback: '로그인 링크 전송',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1396,7 +1374,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.link.helper',
|
||||
fallback: '입력하신 정보로 로그인 링크를 전송합니다.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
@@ -1406,11 +1383,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
],
|
||||
if (_linkPendingRef != null) ...[
|
||||
if (_linkExpired) ...[
|
||||
Text(
|
||||
tr('msg.userfront.login.link_timeout'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
setState(_resetLinkLoginState);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize:
|
||||
const Size.fromHeight(45),
|
||||
),
|
||||
child: Text(tr('ui.common.refresh')),
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.link.short_code_help',
|
||||
fallback:
|
||||
'링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
@@ -1431,7 +1427,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.short_code.prefix',
|
||||
fallback: '영문 2자리',
|
||||
),
|
||||
border:
|
||||
const OutlineInputBorder(),
|
||||
@@ -1449,11 +1444,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: TextField(
|
||||
controller:
|
||||
_shortCodeDigitsController,
|
||||
keyboardType: TextInputType.number,
|
||||
keyboardType:
|
||||
TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.short_code.digits',
|
||||
fallback: '숫자 6자리',
|
||||
),
|
||||
border:
|
||||
const OutlineInputBorder(),
|
||||
@@ -1461,10 +1456,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
suffixText: _linkExpireSeconds > 0
|
||||
suffixText:
|
||||
_linkExpireSeconds > 0
|
||||
? tr(
|
||||
'ui.userfront.login.short_code.expire_time',
|
||||
fallback: '유효시간 {{time}}',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkExpireSeconds,
|
||||
@@ -1493,8 +1488,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.short_code.invalid',
|
||||
fallback:
|
||||
'문자 2개와 숫자 6자리를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1502,12 +1495,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_verifyShortCode(prefix + digits);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(45),
|
||||
minimumSize:
|
||||
const Size.fromHeight(45),
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.short_code.submit',
|
||||
fallback: '코드로 로그인',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1518,8 +1511,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showInfo(
|
||||
tr(
|
||||
'msg.userfront.login.link.resend_wait',
|
||||
fallback:
|
||||
'재발송은 {{time}} 후 가능합니다.',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkResendSeconds,
|
||||
@@ -1536,8 +1527,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link.missing_login_id',
|
||||
fallback:
|
||||
'이메일 또는 휴대폰 번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1554,7 +1543,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_linkResendSeconds > 0
|
||||
? tr(
|
||||
'ui.userfront.login.link.resend_with_time',
|
||||
fallback: '재발송 ({{time}})',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkResendSeconds,
|
||||
@@ -1563,7 +1551,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
)
|
||||
: tr(
|
||||
'ui.common.resend',
|
||||
fallback: '재발송',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1575,8 +1562,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showInfo(
|
||||
tr(
|
||||
'msg.userfront.login.link.resend_wait',
|
||||
fallback:
|
||||
'재발송은 {{time}} 후 가능합니다.',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkResendSeconds,
|
||||
@@ -1593,7 +1578,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link.missing_phone',
|
||||
fallback: '휴대폰 번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1607,7 +1591,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.link.code_only',
|
||||
fallback: '코드만 받기({{time}})',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_linkResendSeconds,
|
||||
@@ -1619,6 +1602,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1628,6 +1612,28 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
children: [
|
||||
if (_isQrLoading)
|
||||
const CircularProgressIndicator()
|
||||
else if (_qrExpired)
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
tr('msg.userfront.login.qr_expired'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: _startQrFlow,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize:
|
||||
const Size.fromHeight(45),
|
||||
),
|
||||
child: Text(tr('ui.common.refresh')),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (_qrImageBase64 != null)
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
@@ -1654,7 +1660,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_qrRemainingSeconds > 0
|
||||
? tr(
|
||||
'ui.userfront.login.qr.remaining',
|
||||
fallback: '남은 시간: {{time}}',
|
||||
params: {
|
||||
'time': _formatTime(
|
||||
_qrRemainingSeconds,
|
||||
@@ -1663,7 +1668,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.login.qr.expired',
|
||||
fallback: 'QR 코드 만료됨',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
@@ -1677,7 +1681,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.qr.scan_hint',
|
||||
fallback: '모바일 앱으로 스캔하세요',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
@@ -1690,7 +1693,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.qr.refresh',
|
||||
fallback: 'QR 코드 새로고침',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1700,7 +1702,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.qr.load_failed',
|
||||
fallback: 'QR 코드를 불러오지 못했습니다.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -1717,7 +1718,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.forgot_password',
|
||||
fallback: '비밀번호를 잊으셨나요?',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1727,7 +1727,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.no_account',
|
||||
fallback: '계정이 없으신가요?',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
@@ -1739,7 +1738,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.signup',
|
||||
fallback: '회원가입',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1747,6 +1745,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Align(
|
||||
alignment: Alignment.center,
|
||||
child: LanguageSelector(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,14 +21,13 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr('ui.userfront.login_success.title', fallback: '로그인 완료'),
|
||||
tr('ui.userfront.login_success.title'),
|
||||
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login_success.subtitle',
|
||||
fallback: '성공적으로 로그인되었습니다.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||
@@ -44,7 +43,6 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.login_success.qr',
|
||||
fallback: 'QR 인증 (카메라 켜기)',
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
@@ -67,7 +65,6 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login_success.later',
|
||||
fallback: '나중에 하기 (대시보드로 이동)',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
|
||||
@@ -146,7 +146,6 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
_isSuccess = true;
|
||||
_resultMessage = tr(
|
||||
'msg.userfront.qr.approve_success',
|
||||
fallback: 'QR 승인 완료! PC 화면에서 로그인이 진행됩니다.',
|
||||
);
|
||||
_isProcessing = false;
|
||||
});
|
||||
@@ -158,7 +157,6 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
_isSuccess = false;
|
||||
_resultMessage = tr(
|
||||
'msg.userfront.qr.approve_error',
|
||||
fallback: 'QR 승인 실패: {{error}}',
|
||||
params: {'error': '$e'},
|
||||
);
|
||||
_isProcessing = false;
|
||||
@@ -193,7 +191,6 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.qr.permission_error',
|
||||
fallback: '카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.',
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
@@ -212,8 +209,8 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
final icon = success ? Icons.check_circle_outline : Icons.error_outline;
|
||||
final color = success ? Colors.green : Colors.red;
|
||||
final title = success
|
||||
? tr('ui.userfront.qr.result_success', fallback: '승인 완료')
|
||||
: tr('ui.userfront.qr.result_failure', fallback: '승인 실패');
|
||||
? tr('ui.userfront.qr.result_success')
|
||||
: tr('ui.userfront.qr.result_failure');
|
||||
final message = _resultMessage ?? '';
|
||||
|
||||
return Center(
|
||||
@@ -242,12 +239,12 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
if (!success)
|
||||
FilledButton(
|
||||
onPressed: _resetScan,
|
||||
child: Text(tr('ui.userfront.qr.rescan', fallback: '다시 스캔')),
|
||||
child: Text(tr('ui.userfront.qr.rescan')),
|
||||
),
|
||||
if (success)
|
||||
FilledButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(tr('ui.common.close', fallback: '닫기')),
|
||||
child: Text(tr('ui.common.close')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -285,11 +282,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
isPermissionDenied
|
||||
? tr(
|
||||
'msg.userfront.qr.permission_required',
|
||||
fallback: '카메라 권한이 필요합니다.',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.qr.camera_error',
|
||||
fallback: '카메라 오류: {{error}}',
|
||||
params: {'error': '${error.errorCode}'},
|
||||
),
|
||||
),
|
||||
@@ -302,11 +297,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
_isRequestingCamera
|
||||
? tr(
|
||||
'ui.common.requesting',
|
||||
fallback: '요청 중...',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.qr.request_permission',
|
||||
fallback: '카메라 권한 요청하기',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -72,7 +72,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.reset.invalid_link',
|
||||
fallback: '유효하지 않은 재설정 링크입니다. (loginId/token 누락)',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -93,7 +92,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.reset.success',
|
||||
fallback: '비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요.',
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
@@ -106,7 +104,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.reset.error.generic',
|
||||
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -128,7 +125,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (_isPolicyLoading) {
|
||||
return tr(
|
||||
'msg.userfront.reset.policy_loading',
|
||||
fallback: '비밀번호 정책을 불러오는 중입니다...',
|
||||
);
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
@@ -141,7 +137,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
final parts = <String>[
|
||||
tr(
|
||||
'msg.userfront.reset.policy.min_length',
|
||||
fallback: '최소 {{count}}자 이상',
|
||||
params: {'count': '$minLength'},
|
||||
),
|
||||
];
|
||||
@@ -149,27 +144,26 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.reset.policy.min_types',
|
||||
fallback: '영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상',
|
||||
params: {'count': '$minTypes'},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (requiresLower) {
|
||||
parts.add(
|
||||
tr('msg.userfront.reset.policy.lowercase', fallback: '소문자 1개 이상'),
|
||||
tr('msg.userfront.reset.policy.lowercase'),
|
||||
);
|
||||
}
|
||||
if (requiresUpper) {
|
||||
parts.add(
|
||||
tr('msg.userfront.reset.policy.uppercase', fallback: '대문자 1개 이상'),
|
||||
tr('msg.userfront.reset.policy.uppercase'),
|
||||
);
|
||||
}
|
||||
if (requiresNumber) {
|
||||
parts.add(tr('msg.userfront.reset.policy.number', fallback: '숫자 1개 이상'));
|
||||
parts.add(tr('msg.userfront.reset.policy.number'));
|
||||
}
|
||||
if (requiresSymbol) {
|
||||
parts.add(
|
||||
tr('msg.userfront.reset.policy.symbol', fallback: '특수문자 1개 이상'),
|
||||
tr('msg.userfront.reset.policy.symbol'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +174,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr('ui.userfront.reset.title', fallback: '새 비밀번호 설정')),
|
||||
title: Text(tr('ui.userfront.reset.title')),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Center(
|
||||
@@ -200,7 +194,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.reset.subtitle',
|
||||
fallback: '새로운 비밀번호 설정',
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
@@ -221,7 +214,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.reset.new_password',
|
||||
fallback: '새 비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
@@ -243,7 +235,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (val.isEmpty) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.empty_password',
|
||||
fallback: '비밀번호를 입력해주세요.',
|
||||
);
|
||||
}
|
||||
final minLength =
|
||||
@@ -251,7 +242,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (val.length < minLength) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.min_length',
|
||||
fallback: '비밀번호는 최소 {{count}}자 이상이어야 합니다.',
|
||||
params: {'count': '$minLength'},
|
||||
);
|
||||
}
|
||||
@@ -270,8 +260,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (minTypes > 0 && typeCount < minTypes) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.min_types',
|
||||
fallback:
|
||||
'비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.',
|
||||
params: {'count': '$minTypes'},
|
||||
);
|
||||
}
|
||||
@@ -279,26 +267,22 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.lowercase',
|
||||
fallback: '최소 1개 이상의 소문자를 포함해야 합니다.',
|
||||
);
|
||||
}
|
||||
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.uppercase',
|
||||
fallback: '최소 1개 이상의 대문자를 포함해야 합니다.',
|
||||
);
|
||||
}
|
||||
if ((_policy?['number'] ?? true) && !hasNumber) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.number',
|
||||
fallback: '최소 1개 이상의 숫자를 포함해야 합니다.',
|
||||
);
|
||||
}
|
||||
if ((_policy?['nonAlphanumeric'] ?? true) &&
|
||||
!hasSymbol) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.symbol',
|
||||
fallback: '최소 1개 이상의 특수문자를 포함해야 합니다.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -311,7 +295,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.reset.confirm_password',
|
||||
fallback: '새 비밀번호 확인',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
@@ -333,7 +316,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (value != _passwordController.text) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.mismatch',
|
||||
fallback: '비밀번호가 일치하지 않습니다.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -357,7 +339,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
: Text(
|
||||
tr(
|
||||
'ui.userfront.reset.submit',
|
||||
fallback: '비밀번호 변경',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -377,7 +358,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 60),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
tr('msg.userfront.reset.invalid_title', fallback: '유효하지 않은 링크입니다.'),
|
||||
tr('msg.userfront.reset.invalid_title'),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -385,7 +366,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.reset.invalid_body',
|
||||
fallback: '비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
@@ -167,7 +167,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.invalid',
|
||||
fallback: '유효한 이메일 형식이 아닙니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -182,7 +181,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.duplicate',
|
||||
fallback: '이미 가입된 이메일입니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -193,7 +191,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.send_failed',
|
||||
fallback: '발송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -222,7 +219,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.code_mismatch',
|
||||
fallback: '인증코드가 일치하지 않습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -230,7 +226,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.verify_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -251,7 +246,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.send_failed',
|
||||
fallback: '발송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -280,7 +274,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.code_mismatch',
|
||||
fallback: '인증코드가 일치하지 않습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -288,7 +281,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.verify_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
@@ -300,7 +292,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
setState(
|
||||
() => _confirmPasswordError = tr(
|
||||
'msg.userfront.signup.password.mismatch',
|
||||
fallback: '비밀번호가 일치하지 않습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -332,32 +323,26 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
if (eStr.contains('uppercase')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.uppercase_required',
|
||||
fallback: '대문자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('lowercase')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.lowercase_required',
|
||||
fallback: '소문자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('digit') || eStr.contains('number')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.number_required',
|
||||
fallback: '숫자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('symbol') || eStr.contains('special')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.symbol_required',
|
||||
fallback: '특수문자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.length_required',
|
||||
fallback: '비밀번호는 최소 12자 이상이어야 합니다.',
|
||||
);
|
||||
} else {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.failed',
|
||||
fallback: '가입 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
);
|
||||
}
|
||||
@@ -373,16 +358,16 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('msg.userfront.signup.success.title', fallback: '회원가입 완료'),
|
||||
tr('msg.userfront.signup.success.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr('msg.userfront.signup.success.body', fallback: '성공적으로 가입되었습니다.'),
|
||||
tr('msg.userfront.signup.success.body'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.go('/signin'),
|
||||
child: Text(
|
||||
tr('ui.userfront.signup.success.action', fallback: '로그인하기'),
|
||||
tr('ui.userfront.signup.success.action'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -399,22 +384,22 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
children: [
|
||||
_stepCircle(
|
||||
1,
|
||||
tr('ui.userfront.signup.steps.agreement', fallback: '약관동의'),
|
||||
tr('ui.userfront.signup.steps.agreement'),
|
||||
),
|
||||
_stepLine(1),
|
||||
_stepCircle(
|
||||
2,
|
||||
tr('ui.userfront.signup.steps.verify', fallback: '본인인증'),
|
||||
tr('ui.userfront.signup.steps.verify'),
|
||||
),
|
||||
_stepLine(2),
|
||||
_stepCircle(
|
||||
3,
|
||||
tr('ui.userfront.signup.steps.profile', fallback: '정보입력'),
|
||||
tr('ui.userfront.signup.steps.profile'),
|
||||
),
|
||||
_stepLine(3),
|
||||
_stepCircle(
|
||||
4,
|
||||
tr('ui.userfront.signup.steps.password', fallback: '비밀번호'),
|
||||
tr('ui.userfront.signup.steps.password'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -471,7 +456,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.agreement.title',
|
||||
fallback: '서비스 이용을 위해\n약관에 동의해주세요',
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
@@ -489,7 +473,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
title: Text(
|
||||
tr('ui.userfront.signup.agreement.all', fallback: '모두 동의합니다'),
|
||||
tr('ui.userfront.signup.agreement.all'),
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
value: _termsAccepted && _privacyAccepted,
|
||||
@@ -507,7 +491,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_agreementSection(
|
||||
title: tr(
|
||||
'ui.userfront.signup.agreement.tos_title',
|
||||
fallback: '바론 소프트웨어 이용약관 (필수)',
|
||||
),
|
||||
content: _tosText,
|
||||
value: _termsAccepted,
|
||||
@@ -517,7 +500,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_agreementSection(
|
||||
title: tr(
|
||||
'ui.userfront.signup.agreement.privacy_title',
|
||||
fallback: '개인정보 수집 및 이용 동의 (필수)',
|
||||
),
|
||||
content: _privacyText,
|
||||
value: _privacyAccepted,
|
||||
@@ -765,7 +747,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.auth.title',
|
||||
fallback: '본인 확인을 위해\n인증을 진행해주세요',
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -785,7 +766,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.auth.affiliate_notice',
|
||||
fallback: '가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
@@ -799,7 +779,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr('ui.userfront.signup.auth.email.title', fallback: '이메일 인증'),
|
||||
tr('ui.userfront.signup.auth.email.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -812,7 +792,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.email.label',
|
||||
fallback: '이메일 주소',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _emailError,
|
||||
@@ -835,10 +814,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
child: Text(
|
||||
_emailSeconds > 0
|
||||
? tr('ui.common.resend', fallback: '재발송')
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.signup.auth.request_code',
|
||||
fallback: '인증요청',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -852,7 +830,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.code_label',
|
||||
fallback: '인증코드 6자리',
|
||||
),
|
||||
suffixText: _formatTime(_emailSeconds),
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -873,7 +850,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.email.verified',
|
||||
fallback: '✅ 이메일 인증 완료',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
@@ -884,7 +860,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr('ui.userfront.signup.phone.title', fallback: '휴대폰 인증'),
|
||||
tr('ui.userfront.signup.phone.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -896,7 +872,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.phone.label',
|
||||
fallback: '휴대폰 번호 (-없이)',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _phoneError,
|
||||
@@ -919,10 +894,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
child: Text(
|
||||
_phoneSeconds > 0
|
||||
? tr('ui.common.resend', fallback: '재발송')
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.signup.auth.request_code',
|
||||
fallback: '인증요청',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -936,7 +910,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.code_label',
|
||||
fallback: '인증코드 6자리',
|
||||
),
|
||||
suffixText: _formatTime(_phoneSeconds),
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -957,7 +930,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.phone.verified',
|
||||
fallback: '✅ 휴대폰 인증 완료',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
@@ -977,7 +949,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.profile.title',
|
||||
fallback: '회원님의\n소속 정보를 알려주세요',
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -986,7 +957,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
controller: _nameController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: tr('ui.userfront.signup.profile.name', fallback: '이름'),
|
||||
labelText: tr('ui.userfront.signup.profile.name'),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
@@ -1002,13 +973,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.affiliation_type',
|
||||
fallback: '소속 유형',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: _isAffiliateEmail
|
||||
? tr(
|
||||
'msg.userfront.signup.profile.affiliate_hint',
|
||||
fallback: '가족사 이메일 사용 시 자동으로 선택됩니다.',
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@@ -1016,13 +985,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
DropdownMenuItem(
|
||||
value: 'GENERAL',
|
||||
child: Text(
|
||||
tr('domain.affiliation.general', fallback: '일반 사용자'),
|
||||
tr('domain.affiliation.general'),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'AFFILIATE',
|
||||
child: Text(
|
||||
tr('domain.affiliation.affiliate', fallback: '가족사 임직원'),
|
||||
tr('domain.affiliation.affiliate'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1052,18 +1021,17 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.company',
|
||||
fallback: '가족사 선택',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'HANMAC',
|
||||
child: Text(tr('domain.company.hanmac', fallback: '한맥')),
|
||||
child: Text(tr('domain.company.hanmac')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'SAMAN',
|
||||
child: Text(tr('domain.company.saman', fallback: '삼안')),
|
||||
child: Text(tr('domain.company.saman')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'PTC',
|
||||
@@ -1071,15 +1039,15 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'JANGHEON',
|
||||
child: Text(tr('domain.company.jangheon', fallback: '장헌')),
|
||||
child: Text(tr('domain.company.jangheon')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'BARON',
|
||||
child: Text(tr('domain.company.baron', fallback: '바론')),
|
||||
child: Text(tr('domain.company.baron')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'HALLA',
|
||||
child: Text(tr('domain.company.halla', fallback: '한라')),
|
||||
child: Text(tr('domain.company.halla')),
|
||||
),
|
||||
],
|
||||
onChanged: _isAffiliateEmail
|
||||
@@ -1095,10 +1063,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: _affiliationType == 'AFFILIATE'
|
||||
? tr('ui.userfront.signup.profile.department', fallback: '부서명')
|
||||
? tr('ui.userfront.signup.profile.department')
|
||||
: tr(
|
||||
'ui.userfront.signup.profile.department_optional',
|
||||
fallback: '소속 정보 (선택)',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
@@ -1111,7 +1078,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
if (_isPolicyLoading) {
|
||||
return tr(
|
||||
'msg.userfront.signup.policy.loading',
|
||||
fallback: '비밀번호 정책을 불러오는 중입니다...',
|
||||
);
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
@@ -1124,7 +1090,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final parts = <String>[
|
||||
tr(
|
||||
'msg.userfront.signup.policy.min_length',
|
||||
fallback: '최소 {{count}}자 이상',
|
||||
params: {'count': minLength.toString()},
|
||||
),
|
||||
];
|
||||
@@ -1132,27 +1097,25 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.signup.policy.min_types',
|
||||
fallback: '영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상',
|
||||
params: {'count': minTypes.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (requiresUpper) {
|
||||
parts.add(tr('msg.userfront.signup.policy.uppercase', fallback: '대문자'));
|
||||
parts.add(tr('msg.userfront.signup.policy.uppercase'));
|
||||
}
|
||||
if (requiresLower) {
|
||||
parts.add(tr('msg.userfront.signup.policy.lowercase', fallback: '소문자'));
|
||||
parts.add(tr('msg.userfront.signup.policy.lowercase'));
|
||||
}
|
||||
if (requiresNumber) {
|
||||
parts.add(tr('msg.userfront.signup.policy.number', fallback: '숫자'));
|
||||
parts.add(tr('msg.userfront.signup.policy.number'));
|
||||
}
|
||||
if (requiresSymbol) {
|
||||
parts.add(tr('msg.userfront.signup.policy.symbol', fallback: '특수문자'));
|
||||
parts.add(tr('msg.userfront.signup.policy.symbol'));
|
||||
}
|
||||
|
||||
return tr(
|
||||
'msg.userfront.signup.policy.summary',
|
||||
fallback: '보안 정책: {{rules}}',
|
||||
params: {'rules': parts.join(', ')},
|
||||
);
|
||||
}
|
||||
@@ -1186,7 +1149,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.password.title',
|
||||
fallback: '마지막으로\n비밀번호를 설정해주세요',
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -1223,7 +1185,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.password.label',
|
||||
fallback: '비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _passwordError,
|
||||
@@ -1236,7 +1197,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.min_length',
|
||||
fallback: '{{count}}자 이상',
|
||||
params: {'count': minLength.toString()},
|
||||
),
|
||||
hasLength,
|
||||
@@ -1245,7 +1205,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.min_types',
|
||||
fallback: '문자 유형 {{count}}가지 이상',
|
||||
params: {'count': minTypes.toString()},
|
||||
),
|
||||
hasTypeCount,
|
||||
@@ -1254,7 +1213,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.uppercase',
|
||||
fallback: '대문자',
|
||||
),
|
||||
hasUpper,
|
||||
),
|
||||
@@ -1262,20 +1220,18 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.lowercase',
|
||||
fallback: '소문자',
|
||||
),
|
||||
hasLower,
|
||||
),
|
||||
if (requiresNumber)
|
||||
_cryptoCheck(
|
||||
tr('msg.userfront.signup.password.rule.number', fallback: '숫자'),
|
||||
tr('msg.userfront.signup.password.rule.number'),
|
||||
hasDigit,
|
||||
),
|
||||
if (requiresSymbol)
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.symbol',
|
||||
fallback: '특수문자',
|
||||
),
|
||||
hasSpecial,
|
||||
),
|
||||
@@ -1290,7 +1246,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_confirmPasswordError = (val != _passwordController.text)
|
||||
? tr(
|
||||
'msg.userfront.signup.password.mismatch',
|
||||
fallback: '비밀번호가 일치하지 않습니다.',
|
||||
)
|
||||
: null;
|
||||
});
|
||||
@@ -1298,7 +1253,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.password.confirm_label',
|
||||
fallback: '비밀번호 확인',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _confirmPasswordError,
|
||||
@@ -1352,7 +1306,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.signup.title', fallback: '회원가입'),
|
||||
tr('ui.userfront.signup.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -1394,7 +1348,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
side: const BorderSide(color: Colors.black),
|
||||
),
|
||||
child: Text(
|
||||
tr('ui.common.prev', fallback: '이전'),
|
||||
tr('ui.common.prev'),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
@@ -1425,11 +1379,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_currentStep < 4
|
||||
? tr(
|
||||
'ui.userfront.signup.next_step',
|
||||
fallback: '다음 단계',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.signup.complete',
|
||||
fallback: '가입 완료',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -174,7 +174,6 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
|
||||
isLoadingMore: false,
|
||||
error: tr(
|
||||
'msg.userfront.dashboard.timeline.load_error',
|
||||
fallback: '접속이력을 불러오지 못했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import '../domain/providers/linked_rps_provider.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/services/http_client.dart';
|
||||
import '../../../../core/i18n/locale_utils.dart';
|
||||
import '../../../../core/widgets/language_selector.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
import '../domain/dashboard_providers.dart';
|
||||
@@ -35,6 +37,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
bool _auditLoading = false;
|
||||
bool _auditLoadingMore = false;
|
||||
bool _isRevoking = false;
|
||||
bool _redirectingToSignin = false;
|
||||
|
||||
bool _showAllActivities = false;
|
||||
final Set<String> _revokedClientIds = {};
|
||||
@@ -43,7 +46,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageScrollController.addListener(_onPageScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
}
|
||||
_loadAuditLogs(reset: true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -63,19 +72,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('ui.userfront.dashboard.revoke.title', fallback: '연동 해지'),
|
||||
tr('ui.userfront.dashboard.revoke.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.confirm',
|
||||
fallback: '{{app}} 앱과의 연동을 해지하시겠습니까?\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.',
|
||||
params: {'app': appName},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
@@ -83,7 +91,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.revoke.confirm_button',
|
||||
fallback: '해지하기',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -102,7 +109,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.success',
|
||||
fallback: '{{app}} 연동이 해지되었습니다.',
|
||||
params: {'app': appName},
|
||||
),
|
||||
),
|
||||
@@ -120,7 +126,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.error',
|
||||
fallback: '해지 실패: {{error}}',
|
||||
params: {'error': '$e'},
|
||||
),
|
||||
),
|
||||
@@ -163,7 +168,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.scopes.title',
|
||||
fallback: '권한 (Scopes)',
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -172,7 +176,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.scopes.empty',
|
||||
fallback: '요청된 권한이 없습니다.',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
)
|
||||
@@ -198,7 +201,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.status_history',
|
||||
fallback: '상태 이력',
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -209,7 +211,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.last_auth',
|
||||
fallback: '최근 인증: {{value}}',
|
||||
params: {'value': item.lastAuthAt},
|
||||
),
|
||||
),
|
||||
@@ -217,15 +218,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final statusLabel = item.status == 'active'
|
||||
? tr('ui.common.status.active', fallback: '활성')
|
||||
? tr('ui.common.status.active')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
);
|
||||
return Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.current_status',
|
||||
fallback: '현재 상태: {{status}}',
|
||||
params: {'status': statusLabel},
|
||||
),
|
||||
style: TextStyle(
|
||||
@@ -244,7 +243,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(tr('ui.common.close', fallback: '닫기')),
|
||||
child: Text(tr('ui.common.close')),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -255,12 +254,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
Widget _buildSideMenu(BuildContext context, {required bool closeOnTap}) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
title: Text(tr('ui.userfront.nav.dashboard')),
|
||||
selected: true,
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
@@ -271,7 +273,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -281,7 +283,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -292,7 +294,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
title: Text(tr('ui.userfront.nav.logout')),
|
||||
onTap: () async {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -302,10 +304,21 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: LanguageSelector(compact: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshAll() async {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
}
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
setState(() {
|
||||
_revokedClientIds.clear();
|
||||
@@ -367,6 +380,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadAuditLogs({bool reset = false}) async {
|
||||
if (!_isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
if (_auditLoading || _auditLoadingMore) {
|
||||
return;
|
||||
}
|
||||
@@ -443,15 +459,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
String _authMethodLabel() {
|
||||
if (AuthTokenStore.usesCookie()) {
|
||||
return tr('ui.userfront.auth_method.ory', fallback: 'Ory 세션');
|
||||
return tr('ui.userfront.auth_method.ory');
|
||||
}
|
||||
final provider = AuthTokenStore.getProvider();
|
||||
if (provider == null || provider.isEmpty) {
|
||||
return tr('ui.userfront.auth_method.session', fallback: '세션');
|
||||
return tr('ui.userfront.auth_method.session');
|
||||
}
|
||||
final lower = provider.toLowerCase();
|
||||
if (lower.contains('ory')) {
|
||||
return tr('ui.userfront.auth_method.ory', fallback: 'Ory 세션');
|
||||
return tr('ui.userfront.auth_method.ory');
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
@@ -494,12 +510,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltip = [
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_device',
|
||||
fallback: '승인 기기: {{device}}',
|
||||
params: {'device': deviceLabel},
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_ip',
|
||||
fallback: '승인 IP: {{ip}}',
|
||||
params: {'ip': approvedIp.isEmpty ? '-' : approvedIp},
|
||||
),
|
||||
].join('\n');
|
||||
@@ -522,21 +536,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltipLabel = isOidc
|
||||
? tr(
|
||||
'ui.userfront.dashboard.approved_session.userfront',
|
||||
fallback: '승인한 Userfront 세션 ID',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.approved_session.default',
|
||||
fallback: '승인한 세션 ID',
|
||||
);
|
||||
final tooltip = approvedSessionId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.approved_session.none',
|
||||
fallback: '{{label}} 없음',
|
||||
params: {'label': tooltipLabel},
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.dashboard.approved_session.copy_click',
|
||||
fallback: '{{label}}: {{id}}\\n클릭하면 복사됩니다.',
|
||||
params: {'label': tooltipLabel, 'id': approvedSessionId},
|
||||
);
|
||||
return InkWell(
|
||||
@@ -550,7 +560,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.session_id_copied',
|
||||
fallback: '세션 ID가 복사되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -584,7 +593,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return _selectableText(
|
||||
tr(
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {'method': authMethod},
|
||||
),
|
||||
);
|
||||
@@ -593,12 +601,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltip = [
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_device',
|
||||
fallback: '승인 기기: {{device}}',
|
||||
params: {'device': deviceLabel},
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_ip',
|
||||
fallback: '승인 IP: {{ip}}',
|
||||
params: {'ip': approvedIp.isEmpty ? '-' : approvedIp},
|
||||
),
|
||||
].join('\n');
|
||||
@@ -607,7 +613,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: _selectableText(
|
||||
tr(
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {'method': authMethod},
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -625,11 +630,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltipLabel = isOidc
|
||||
? tr(
|
||||
'ui.userfront.dashboard.approved_session.userfront',
|
||||
fallback: '승인한 Userfront 세션 ID',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.approved_session.default',
|
||||
fallback: '승인한 세션 ID',
|
||||
);
|
||||
return InkWell(
|
||||
onTap: approvedSessionId.isEmpty
|
||||
@@ -642,7 +645,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.session_id_copied',
|
||||
fallback: '세션 ID가 복사되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -653,18 +655,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
message: approvedSessionId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.approved_session.none',
|
||||
fallback: '{{label}} 없음',
|
||||
params: {'label': tooltipLabel},
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.dashboard.approved_session.copy_tap',
|
||||
fallback: '{{label}}: {{id}}\\n탭하면 복사됩니다.',
|
||||
params: {'label': tooltipLabel, 'id': approvedSessionId},
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {
|
||||
'method': isOidc
|
||||
? authMethod
|
||||
@@ -695,7 +694,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltip = clientId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.client_id_missing',
|
||||
fallback: 'Client ID 없음',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.dashboard.client_id',
|
||||
@@ -717,10 +715,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
String _appLabelForPath(String path) {
|
||||
if (path.startsWith('/api/v1/auth')) {
|
||||
return tr('ui.userfront.app_label.baron', fallback: 'Baron 로그인');
|
||||
return tr('ui.userfront.app_label.baron');
|
||||
}
|
||||
if (path.startsWith('/api/v1/user')) {
|
||||
return tr('ui.userfront.app_label.baron', fallback: 'Baron 로그인');
|
||||
return tr('ui.userfront.app_label.baron');
|
||||
}
|
||||
if (path.startsWith('/api/v1/dev')) {
|
||||
return tr('ui.userfront.app_label.dev_console', fallback: 'Dev Console');
|
||||
@@ -731,11 +729,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
fallback: 'Admin Console',
|
||||
);
|
||||
}
|
||||
return tr('ui.userfront.app_label.baron', fallback: 'Baron 로그인');
|
||||
return tr('ui.userfront.app_label.baron');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint;
|
||||
final profileState = ref.watch(profileProvider);
|
||||
final profile = profileState.value;
|
||||
@@ -747,14 +749,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
tr('ui.userfront.profile.user_fallback', fallback: 'User');
|
||||
final department = profile?.department.isNotEmpty == true
|
||||
? profile!.department
|
||||
: tr('ui.userfront.profile.department_empty', fallback: '소속 정보 없음');
|
||||
: tr('ui.userfront.profile.department_empty');
|
||||
final sessionIssuedAt = _getJwtIssuedAt();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
tr('ui.userfront.app_title'),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -763,17 +765,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_outline),
|
||||
tooltip: tr('ui.userfront.nav.profile', fallback: '내 정보'),
|
||||
tooltip: tr('ui.userfront.nav.profile'),
|
||||
onPressed: () => context.push('/profile'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan'),
|
||||
onPressed: _onScanQR,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: tr('ui.userfront.nav.logout', fallback: '로그아웃'),
|
||||
tooltip: tr('ui.userfront.nav.logout'),
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
@@ -814,21 +816,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_buildSectionTitle(
|
||||
tr(
|
||||
'ui.userfront.sections.apps',
|
||||
fallback: '나의 App 현황',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.sections.apps_subtitle',
|
||||
fallback: '현재 연결된 앱과 최근 인증 상태입니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.audit', fallback: '접속이력'),
|
||||
tr('ui.userfront.sections.audit'),
|
||||
tr(
|
||||
'msg.userfront.sections.audit_subtitle',
|
||||
fallback: 'Baron 로그인 기준의 최근 접근 기록입니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -853,14 +852,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
) {
|
||||
final sessionLabel = issuedAt != null
|
||||
? _formatDateTime(issuedAt)
|
||||
: tr('ui.userfront.session.unknown', fallback: '알 수 없음');
|
||||
: tr('ui.userfront.session.unknown');
|
||||
final infoColumn = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.greeting',
|
||||
fallback: '안녕하세요, {{name}}님',
|
||||
params: {'name': userName},
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -881,7 +879,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
Icons.verified_user,
|
||||
tr('ui.userfront.session.active', fallback: '세션 활성'),
|
||||
tr('ui.userfront.session.active'),
|
||||
),
|
||||
_buildInfoChip(Icons.lock_outline, _authMethodLabel()),
|
||||
_buildInfoChip(Icons.access_time, sessionLabel),
|
||||
@@ -967,7 +965,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.empty',
|
||||
fallback: '연동된 앱이 없습니다.',
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -979,7 +976,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.empty_detail',
|
||||
fallback: '앱을 연동하면 최근 활동과 상태가 표시됩니다.',
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
@@ -998,14 +994,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.error',
|
||||
fallback: '연동 정보를 불러오지 못했습니다.',
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => ref.read(linkedRpsProvider.notifier).refresh(),
|
||||
child: Text(tr('ui.common.retry', fallback: '다시 시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1023,7 +1018,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
final lastAuthLabel = rp.lastAuthenticatedAt != null
|
||||
? _formatDateTime(rp.lastAuthenticatedAt!)
|
||||
: tr('ui.userfront.dashboard.activity.linked', fallback: '연동됨');
|
||||
: tr('ui.userfront.dashboard.activity.linked');
|
||||
|
||||
final statusCode = isRevoked ? 'revoked' : 'active';
|
||||
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
||||
@@ -1128,8 +1123,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
label: Text(
|
||||
_showAllActivities
|
||||
? tr('ui.common.collapse', fallback: '접기')
|
||||
: tr('ui.common.show_more', fallback: '+ 더보기'),
|
||||
? tr('ui.common.collapse')
|
||||
: tr('ui.common.show_more'),
|
||||
style: TextStyle(
|
||||
color: _showAllActivities
|
||||
? Colors.grey
|
||||
@@ -1193,19 +1188,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
color: statusColor.withValues(alpha: 31),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
item.status == 'active'
|
||||
? tr('ui.common.status.active', fallback: '활성')
|
||||
? tr('ui.common.status.active')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -1214,7 +1208,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr('ui.userfront.dashboard.last_auth_label', fallback: '최근 인증'),
|
||||
tr('ui.userfront.dashboard.last_auth_label'),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -1238,7 +1232,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
child: Text(
|
||||
tr('ui.common.details', fallback: '상세정보'),
|
||||
tr('ui.common.details'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
@@ -1272,11 +1266,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
item.isRevoked
|
||||
? tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.revoke.title',
|
||||
fallback: '연동 해지',
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
@@ -1314,7 +1306,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.link_open_error',
|
||||
fallback: '해당 링크를 열 수 없습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1326,7 +1317,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.link_missing',
|
||||
fallback: '이동할 페이지 주소(Client URI)가 설정되지 않았습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1357,14 +1347,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.audit_load_error',
|
||||
fallback: '접속이력을 불러오지 못했습니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).refresh(),
|
||||
child: Text(tr('ui.common.retry', fallback: '다시 시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1378,7 +1367,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.audit_empty',
|
||||
fallback: '최근 접속 이력이 없습니다.',
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
@@ -1429,14 +1417,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.date', fallback: '접속일자'),
|
||||
tr('ui.userfront.audit.table.date'),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.app',
|
||||
fallback: '애플리케이션',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1449,7 +1436,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.device',
|
||||
fallback: '접속환경',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1457,7 +1443,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.auth_method',
|
||||
fallback: '인증수단',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1465,20 +1450,19 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.result',
|
||||
fallback: '인증결과',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.status', fallback: '현황'),
|
||||
tr('ui.userfront.audit.table.status'),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: state.items.map((log) {
|
||||
final statusLabel = log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패');
|
||||
? tr('ui.common.status.success')
|
||||
: tr('ui.common.status.failure');
|
||||
final statusColor = log.status == 'success'
|
||||
? Colors.green
|
||||
: Colors.redAccent;
|
||||
@@ -1523,7 +1507,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'ui.userfront.audit.table.pending',
|
||||
fallback: '(준비중)',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
@@ -1571,8 +1554,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
_selectableText(
|
||||
log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패'),
|
||||
? tr('ui.common.status.success')
|
||||
: tr('ui.common.status.failure'),
|
||||
style: TextStyle(
|
||||
color: log.status == 'success'
|
||||
? Colors.green
|
||||
@@ -1597,14 +1580,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.date',
|
||||
fallback: '접속일자: {{value}}',
|
||||
params: {'value': _formatDateTime(log.timestamp)},
|
||||
),
|
||||
),
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.ip',
|
||||
fallback: '접속 IP: {{value}}',
|
||||
params: {
|
||||
'value': log.ipAddress.isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
@@ -1615,7 +1596,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.device',
|
||||
fallback: '접속환경: {{value}}',
|
||||
params: {
|
||||
'value': _deviceLabelFromUserAgent(log.userAgent),
|
||||
},
|
||||
@@ -1630,16 +1610,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.result',
|
||||
fallback: '인증결과: {{value}}',
|
||||
params: {
|
||||
'value': log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패'),
|
||||
? tr('ui.common.status.success')
|
||||
: tr('ui.common.status.failure'),
|
||||
},
|
||||
),
|
||||
),
|
||||
_selectableText(
|
||||
tr('msg.userfront.audit.status', fallback: '현황: (준비중)'),
|
||||
tr('msg.userfront.audit.status'),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
@@ -1667,13 +1646,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.audit.load_more_error',
|
||||
fallback: '더 불러오지 못했습니다.',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).loadMore(),
|
||||
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1683,13 +1661,34 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
tr('msg.userfront.audit.end', fallback: '더 이상 항목이 없습니다.'),
|
||||
tr('msg.userfront.audit.end'),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
bool _isLoggedIn() {
|
||||
return AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
|
||||
}
|
||||
|
||||
void _redirectToSignin() {
|
||||
if (!mounted || _redirectingToSignin) {
|
||||
return;
|
||||
}
|
||||
_redirectingToSignin = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final uri = GoRouterState.of(context).uri;
|
||||
final localeCode =
|
||||
extractLocaleFromPath(uri) ?? resolvePreferredLocaleCode();
|
||||
context.go('/$localeCode/signin');
|
||||
_redirectingToSignin = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityItem {
|
||||
|
||||
@@ -26,7 +26,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.load_failed',
|
||||
fallback: '프로필을 불러오지 못했습니다: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -61,7 +60,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +85,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.update_failed',
|
||||
fallback: '프로필 업데이트에 실패했습니다: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -98,7 +96,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,7 +117,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.send_code_failed',
|
||||
fallback: '인증번호 전송 실패: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -134,7 +131,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +155,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.password_change_failed',
|
||||
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -170,7 +166,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +187,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.verify_code_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:userfront/i18n.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
import '../../../../core/widgets/language_selector.dart';
|
||||
import '../../data/models/user_profile_model.dart';
|
||||
import '../../domain/notifiers/profile_notifier.dart';
|
||||
|
||||
@@ -235,7 +236,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.code_sent',
|
||||
fallback: '인증번호가 전송되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -249,7 +249,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.send_failed',
|
||||
fallback: '전송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
),
|
||||
@@ -275,7 +274,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr('msg.userfront.profile.phone.verified', fallback: '인증되었습니다.'),
|
||||
tr('msg.userfront.profile.phone.verified'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -291,7 +290,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.verify_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
),
|
||||
@@ -311,7 +309,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.current_required',
|
||||
fallback: '현재 비밀번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -320,7 +317,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.new_required',
|
||||
fallback: '새 비밀번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -329,7 +325,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.mismatch',
|
||||
fallback: '새 비밀번호가 일치하지 않습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -354,7 +349,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(() {
|
||||
_passwordSuccess = tr(
|
||||
'msg.userfront.profile.password.changed',
|
||||
fallback: '비밀번호가 변경되었습니다.',
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -362,7 +356,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(() {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.profile.password.change_failed',
|
||||
fallback: '비밀번호 변경 실패: {{error}}',
|
||||
params: {'error': message},
|
||||
);
|
||||
});
|
||||
@@ -440,7 +433,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr('msg.userfront.profile.name_required', fallback: '이름을 입력해주세요.'),
|
||||
tr('msg.userfront.profile.name_required'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -452,7 +445,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.department_required',
|
||||
fallback: '소속을 입력해주세요.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -466,7 +458,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone_required',
|
||||
fallback: '휴대폰 번호를 입력해주세요.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -479,7 +470,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone_verify_required',
|
||||
fallback: '휴대폰 번호 인증이 필요합니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -525,7 +515,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.update_success',
|
||||
fallback: '정보가 수정되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -538,7 +527,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.update_failed',
|
||||
fallback: '수정 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
),
|
||||
@@ -551,32 +539,42 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
}
|
||||
|
||||
Widget _buildSideMenu(BuildContext context) {
|
||||
return ListView(
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
title: Text(tr('ui.userfront.nav.dashboard')),
|
||||
onTap: () => context.go('/'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
selected: true,
|
||||
onTap: () => context.go('/profile'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan')),
|
||||
onTap: () => context.go('/scan'),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
title: Text(tr('ui.userfront.nav.logout')),
|
||||
onTap: _logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: LanguageSelector(compact: true),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -626,13 +624,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
|
||||
Widget _buildHeaderCard(UserProfile profile) {
|
||||
final name = profile.name.isEmpty
|
||||
? tr('msg.userfront.profile.name_missing', fallback: '이름 없음')
|
||||
? tr('msg.userfront.profile.name_missing')
|
||||
: profile.name;
|
||||
final email = profile.email.isEmpty
|
||||
? tr('msg.userfront.profile.email_missing', fallback: '이메일 없음')
|
||||
? tr('msg.userfront.profile.email_missing')
|
||||
: profile.email;
|
||||
final department = profile.department.isEmpty
|
||||
? tr('msg.userfront.profile.department_missing', fallback: '소속 정보 없음')
|
||||
? tr('msg.userfront.profile.department_missing')
|
||||
: profile.department;
|
||||
|
||||
return Container(
|
||||
@@ -661,7 +659,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.greeting',
|
||||
fallback: '안녕하세요, {{name}}님',
|
||||
params: {'name': name},
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -682,7 +679,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
Icons.badge_outlined,
|
||||
tr('ui.userfront.profile.manage', fallback: '프로필 관리'),
|
||||
tr('ui.userfront.profile.manage'),
|
||||
),
|
||||
_buildInfoChip(
|
||||
Icons.apartment,
|
||||
@@ -725,7 +722,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
title: Text(label),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: Text(
|
||||
tr('ui.common.read_only', fallback: '읽기 전용'),
|
||||
tr('ui.common.read_only'),
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
),
|
||||
);
|
||||
@@ -749,7 +746,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing(field, profile),
|
||||
child: Text(tr('ui.common.edit', fallback: '수정')),
|
||||
child: Text(tr('ui.common.edit')),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -777,7 +774,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -792,11 +789,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (!isEditing) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(tr('ui.userfront.profile.phone.title', fallback: '전화번호')),
|
||||
title: Text(tr('ui.userfront.profile.phone.title')),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
|
||||
child: Text(tr('ui.common.edit', fallback: '수정')),
|
||||
child: Text(tr('ui.common.edit')),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -805,7 +802,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.profile.phone.title', fallback: '전화번호'),
|
||||
tr('ui.userfront.profile.phone.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -835,17 +832,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
onPressed: _isVerifying ? null : _sendCode,
|
||||
child: Text(
|
||||
_isCodeSent
|
||||
? tr('ui.common.resend', fallback: '재전송')
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.profile.phone.request_code',
|
||||
fallback: '인증요청',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -865,7 +861,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: tr(
|
||||
'ui.userfront.profile.phone.code_hint',
|
||||
fallback: '인증번호 6자리',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -873,7 +868,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : () => _verifyCode(profile),
|
||||
child: Text(tr('ui.common.confirm', fallback: '확인')),
|
||||
child: Text(tr('ui.common.confirm')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -884,7 +879,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.verify_notice',
|
||||
fallback: '휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
|
||||
),
|
||||
style: const TextStyle(color: Colors.orange, fontSize: 12),
|
||||
),
|
||||
@@ -899,14 +893,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.profile.password.title', fallback: '비밀번호 변경'),
|
||||
tr('ui.userfront.profile.password.title'),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.password.subtitle',
|
||||
fallback: '현재 비밀번호 확인 후 새 비밀번호로 변경합니다.',
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||
),
|
||||
@@ -917,7 +910,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.current',
|
||||
fallback: '현재 비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@@ -939,7 +931,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.new',
|
||||
fallback: '새 비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@@ -959,7 +950,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.confirm',
|
||||
fallback: '새 비밀번호 확인',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@@ -999,7 +989,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
: Text(
|
||||
tr(
|
||||
'ui.userfront.profile.password.change',
|
||||
fallback: '비밀번호 변경',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1009,7 +998,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.profile.password.forgot',
|
||||
fallback: '비밀번호를 잊으셨나요?',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1035,10 +1023,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildHeaderCard(profile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.basic', fallback: '기본 정보'),
|
||||
tr('ui.userfront.profile.section.basic'),
|
||||
tr(
|
||||
'msg.userfront.profile.section.basic',
|
||||
fallback: '계정 기본 정보를 관리합니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1049,7 +1036,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
field: 'name',
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.name',
|
||||
fallback: '이름',
|
||||
),
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
@@ -1060,7 +1046,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.email',
|
||||
fallback: '이메일',
|
||||
),
|
||||
profile.email,
|
||||
),
|
||||
@@ -1073,11 +1058,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildSectionTitle(
|
||||
tr(
|
||||
'ui.userfront.profile.section.organization',
|
||||
fallback: '조직 정보',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.profile.section.organization',
|
||||
fallback: '소속 및 구분 정보입니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1088,7 +1071,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
field: 'department',
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.department',
|
||||
fallback: '소속',
|
||||
),
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
@@ -1099,7 +1081,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.affiliation',
|
||||
fallback: '구분',
|
||||
),
|
||||
profile.affiliationType,
|
||||
),
|
||||
@@ -1108,7 +1089,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.tenant',
|
||||
fallback: '소속 테넌트',
|
||||
),
|
||||
profile.tenant!.name,
|
||||
),
|
||||
@@ -1118,7 +1098,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.company_code',
|
||||
fallback: '회사코드',
|
||||
),
|
||||
profile.companyCode,
|
||||
),
|
||||
@@ -1128,10 +1107,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.security', fallback: '보안'),
|
||||
tr('ui.userfront.profile.section.security'),
|
||||
tr(
|
||||
'msg.userfront.profile.section.security',
|
||||
fallback: '비밀번호를 안전하게 관리합니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1160,7 +1138,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (profile == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
),
|
||||
body: profileState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
@@ -1171,14 +1149,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.load_failed',
|
||||
fallback: '정보를 불러올 수 없습니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1195,7 +1172,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
tr('ui.userfront.app_title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -1204,17 +1181,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
|
||||
tooltip: tr('ui.userfront.nav.dashboard'),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan'),
|
||||
onPressed: () => context.push('/scan'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: tr('ui.userfront.nav.logout', fallback: '로그아웃'),
|
||||
tooltip: tr('ui.userfront.nav.logout'),
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
|
||||
+14
-32
@@ -1,36 +1,18 @@
|
||||
import 'dart:ui';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'i18n_data.dart';
|
||||
|
||||
const _defaultLocale = 'ko';
|
||||
const _supportedLocales = ['ko', 'en'];
|
||||
|
||||
String _resolveLocale() {
|
||||
final locale = PlatformDispatcher.instance.locale;
|
||||
final code = locale.languageCode.toLowerCase();
|
||||
if (_supportedLocales.contains(code)) {
|
||||
return code;
|
||||
}
|
||||
return _defaultLocale;
|
||||
}
|
||||
|
||||
String _formatTemplate(String template, Map<String, String>? params) {
|
||||
if (params == null || params.isEmpty) {
|
||||
return template;
|
||||
}
|
||||
var result = template;
|
||||
params.forEach((key, value) {
|
||||
result = result.replaceAll('{{$key}}', value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
final _koreanPattern = RegExp(r'[가-힣]');
|
||||
|
||||
String tr(String key, {String? fallback, Map<String, String>? params}) {
|
||||
final locale = _resolveLocale();
|
||||
final map = locale == 'en' ? enStrings : koStrings;
|
||||
final value = map[key];
|
||||
final template = (value != null && value.isNotEmpty)
|
||||
? value
|
||||
: (fallback ?? key);
|
||||
return _formatTemplate(template, params);
|
||||
try {
|
||||
if (fallback != null && _koreanPattern.hasMatch(fallback)) {
|
||||
fallback = null;
|
||||
}
|
||||
final translated = key.tr(namedArgs: params);
|
||||
if (translated == key && fallback != null && fallback.isNotEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
return translated;
|
||||
} catch (_) {
|
||||
return fallback ?? key;
|
||||
}
|
||||
}
|
||||
+83
-34
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
@@ -19,6 +20,9 @@ import 'core/services/auth_proxy_service.dart';
|
||||
import 'core/services/auth_token_store.dart';
|
||||
import 'core/services/logger_service.dart';
|
||||
import 'core/notifiers/auth_notifier.dart';
|
||||
import 'core/i18n/locale_gate.dart';
|
||||
import 'core/i18n/locale_utils.dart';
|
||||
import 'core/i18n/toml_asset_loader.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'features/auth/presentation/consent_screen.dart';
|
||||
import 'i18n.dart';
|
||||
@@ -40,6 +44,7 @@ Future<void> _loadBundledFonts() async {
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
usePathUrlStrategy();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
||||
// 1. Global Error Handling
|
||||
FlutterError.onError = (details) {
|
||||
@@ -70,7 +75,22 @@ void main() async {
|
||||
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
|
||||
await _loadBundledFonts();
|
||||
|
||||
runApp(const ProviderScope(child: BaronSSOApp()));
|
||||
runApp(
|
||||
// URL(/en, /ko)이 있으면 우선 적용해서 첫 렌더부터 올바른 언어로 시작합니다.
|
||||
() {
|
||||
final initialLocaleCode =
|
||||
extractLocaleFromPath(Uri.base) ?? resolvePreferredLocaleCode();
|
||||
return EasyLocalization(
|
||||
supportedLocales: const [Locale('en'), Locale('ko')],
|
||||
fallbackLocale: const Locale('en'),
|
||||
startLocale: Locale(initialLocaleCode),
|
||||
saveLocale: false,
|
||||
path: 'assets/translations',
|
||||
assetLoader: const TomlAssetLoader(),
|
||||
child: const ProviderScope(child: BaronSSOApp()),
|
||||
);
|
||||
}(),
|
||||
);
|
||||
}
|
||||
|
||||
// Router Configuration
|
||||
@@ -80,19 +100,30 @@ final _router = GoRouter(
|
||||
initialLocation: '/',
|
||||
debugLogDiagnostics: !kReleaseMode,
|
||||
refreshListenable: AuthNotifier.instance,
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
final localeCode =
|
||||
extractLocaleFromPath(state.uri) ?? resolvePreferredLocaleCode();
|
||||
return LocaleGate(localeCode: localeCode, child: child);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
path: '/:locale',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to root (DashboardScreen)");
|
||||
return const DashboardScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(path: '/profile', builder: (context, state) => const ProfilePage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/signin',
|
||||
path: 'profile',
|
||||
builder: (context, state) => const ProfilePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'signin',
|
||||
builder: (context, state) {
|
||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||
final loginChallenge =
|
||||
state.uri.queryParameters['login_challenge'];
|
||||
final redirectUrl =
|
||||
state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
@@ -107,7 +138,7 @@ final _router = GoRouter(
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
path: 'login',
|
||||
builder: (context, state) {
|
||||
final redirectUrl =
|
||||
state.uri.queryParameters['redirect_uri'] ??
|
||||
@@ -117,13 +148,16 @@ final _router = GoRouter(
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/consent',
|
||||
path: 'consent',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
final consentChallenge = state.uri.queryParameters['consent_challenge'];
|
||||
final consentChallenge =
|
||||
state.uri.queryParameters['consent_challenge'];
|
||||
if (consentChallenge == null) {
|
||||
_routerLogger.warning("Consent screen loaded without a challenge.");
|
||||
return const Scaffold(
|
||||
body: Center(child: Text('Error: Consent challenge is missing.')),
|
||||
body: Center(
|
||||
child: Text('Error: Consent challenge is missing.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
_routerLogger.info("Navigating to /consent with challenge.");
|
||||
@@ -131,43 +165,46 @@ final _router = GoRouter(
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/signup',
|
||||
path: 'signup',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /signup");
|
||||
return const SignupScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/registration',
|
||||
path: 'registration',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /registration");
|
||||
return const SignupScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verify',
|
||||
path: 'verify',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /verify (query)");
|
||||
return LoginScreen(key: state.pageKey);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verify/:token',
|
||||
path: 'verify/:token',
|
||||
builder: (context, state) {
|
||||
final token = state.pathParameters['token'];
|
||||
_routerLogger.info("Navigating to /verify with token: $token");
|
||||
return LoginScreen(key: state.pageKey, verificationToken: token);
|
||||
return LoginScreen(
|
||||
key: state.pageKey,
|
||||
verificationToken: token,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verification',
|
||||
path: 'verification',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /verification");
|
||||
return LoginScreen(key: state.pageKey);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/l/:shortCode',
|
||||
path: 'l/:shortCode',
|
||||
builder: (context, state) {
|
||||
final shortCode = state.pathParameters['shortCode'];
|
||||
_routerLogger.info("Navigating to /l with code: $shortCode");
|
||||
@@ -175,14 +212,14 @@ final _router = GoRouter(
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/forgot-password',
|
||||
path: 'forgot-password',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /forgot-password");
|
||||
return const ForgotPasswordScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/recovery',
|
||||
path: 'recovery',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /recovery");
|
||||
return const ForgotPasswordScreen();
|
||||
@@ -190,41 +227,39 @@ final _router = GoRouter(
|
||||
),
|
||||
GoRoute(
|
||||
// Supports both /reset-password and /reset-password?token=...
|
||||
path: '/reset-password',
|
||||
path: 'reset-password',
|
||||
builder: (context, state) {
|
||||
// For deep linking, you might pass the token in the path, e.g., /reset-password/:token
|
||||
// final token = state.pathParameters['token'];
|
||||
_routerLogger.info("Navigating to /reset-password");
|
||||
return const ResetPasswordScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/error',
|
||||
path: 'error',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /error");
|
||||
final params = state.uri.queryParameters;
|
||||
return ErrorScreen(
|
||||
errorId: params['id'],
|
||||
errorCode: params['error'],
|
||||
description: params['error_description'] ?? params['message'],
|
||||
description:
|
||||
params['error_description'] ?? params['message'],
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
path: 'settings',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /settings (disabled)");
|
||||
return ErrorScreen(
|
||||
errorCode: 'settings_disabled',
|
||||
description: tr(
|
||||
'msg.userfront.settings.disabled',
|
||||
fallback: '현재 계정 설정 화면은 준비 중입니다.',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approve',
|
||||
path: 'approve',
|
||||
builder: (context, state) {
|
||||
final ref = state.uri.queryParameters['ref'];
|
||||
_routerLogger.info("Navigating to /approve with ref: $ref");
|
||||
@@ -232,7 +267,7 @@ final _router = GoRouter(
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/ql/:ref',
|
||||
path: 'ql/:ref',
|
||||
builder: (context, state) {
|
||||
final ref = state.pathParameters['ref'];
|
||||
_routerLogger.info("Navigating to /ql with ref: $ref");
|
||||
@@ -240,25 +275,35 @@ final _router = GoRouter(
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/scan',
|
||||
path: 'scan',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /scan");
|
||||
return const QRScanScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/users',
|
||||
path: 'admin/users',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /admin/users");
|
||||
return const UserManagementScreen();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
redirect: (context, state) {
|
||||
final requestedLocale = extractLocaleFromPath(state.uri);
|
||||
final preferredLocale = resolvePreferredLocaleCode();
|
||||
if (requestedLocale == null) {
|
||||
return buildLocalizedPath(preferredLocale, state.uri);
|
||||
}
|
||||
|
||||
final hasStoredToken = AuthTokenStore.getToken() != null;
|
||||
final hasCookieSession = AuthTokenStore.usesCookie();
|
||||
final isLoggedIn = hasStoredToken || hasCookieSession;
|
||||
final path = state.uri.path;
|
||||
final path = stripLocalePath(state.uri);
|
||||
|
||||
// Public paths that don't require login
|
||||
final isPublicPath =
|
||||
@@ -290,10 +335,11 @@ final _router = GoRouter(
|
||||
_routerLogger.info("Not logged in, redirecting to /signin");
|
||||
// Preserve OIDC challenge if present
|
||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||
final locale = requestedLocale;
|
||||
if (loginChallenge != null) {
|
||||
return '/signin?login_challenge=$loginChallenge';
|
||||
return '/$locale/signin?login_challenge=$loginChallenge';
|
||||
}
|
||||
return '/signin';
|
||||
return '/$locale/signin';
|
||||
}
|
||||
|
||||
// If logged in and trying to access login page, redirect to root (dashboard)
|
||||
@@ -313,7 +359,10 @@ class BaronSSOApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
title: tr('ui.userfront.app_title'),
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base
|
||||
|
||||
+189
-1
@@ -105,6 +105,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
easy_localization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_localization
|
||||
sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
easy_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: easy_logger
|
||||
sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -113,6 +129,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -134,6 +158,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -142,6 +171,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -168,6 +202,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -208,6 +247,19 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -336,6 +388,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -352,6 +444,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -384,6 +484,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.20"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -477,6 +633,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -509,6 +673,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.12"
|
||||
toml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toml
|
||||
sha256: "9968de24e45b632bf1a654fe1ac7b6fe5261c349243df83fd262397799c45a2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -629,6 +801,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -637,6 +817,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -646,5 +834,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.4 <4.0.0"
|
||||
dart: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
@@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.4
|
||||
sdk: ">=3.10.0-0 <4.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@@ -45,10 +45,14 @@ dependencies:
|
||||
logger: ^2.0.0
|
||||
qr_flutter: ^4.1.0
|
||||
mobile_scanner: ^7.1.4
|
||||
easy_localization: ^3.0.7
|
||||
toml: ^0.15.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
@@ -70,6 +74,8 @@ flutter:
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - .env
|
||||
assets:
|
||||
- assets/translations/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'web_storage_stub.dart'
|
||||
if (dart.library.html) 'web_storage_web.dart';
|
||||
|
||||
export 'web_storage_stub.dart'
|
||||
if (dart.library.html) 'web_storage_web.dart';
|
||||
@@ -0,0 +1,21 @@
|
||||
class WebStorage {
|
||||
bool get isWeb => false;
|
||||
|
||||
String? get(String key) => null;
|
||||
|
||||
void set(String key, String value) {}
|
||||
|
||||
String? getSession(String key) => null;
|
||||
|
||||
void setSession(String key, String value) {}
|
||||
|
||||
void removeSession(String key) {}
|
||||
|
||||
void clearSession() {}
|
||||
|
||||
void remove(String key) {}
|
||||
|
||||
void clear() {}
|
||||
}
|
||||
|
||||
final webStorage = WebStorage();
|
||||
@@ -0,0 +1,37 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:html' as html;
|
||||
|
||||
class WebStorage {
|
||||
bool get isWeb => true;
|
||||
|
||||
String? get(String key) => html.window.localStorage[key];
|
||||
|
||||
void set(String key, String value) {
|
||||
html.window.localStorage[key] = value;
|
||||
}
|
||||
|
||||
String? getSession(String key) => html.window.sessionStorage[key];
|
||||
|
||||
void setSession(String key, String value) {
|
||||
html.window.sessionStorage[key] = value;
|
||||
}
|
||||
|
||||
void removeSession(String key) {
|
||||
html.window.sessionStorage.remove(key);
|
||||
}
|
||||
|
||||
void clearSession() {
|
||||
html.window.sessionStorage.clear();
|
||||
}
|
||||
|
||||
void remove(String key) {
|
||||
html.window.localStorage.remove(key);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
html.window.localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final webStorage = WebStorage();
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage_web.dart' as locale_web;
|
||||
|
||||
import 'helpers/web_storage.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(false);
|
||||
locale_web.LocaleStorageImpl.forceSessionStorageForTests(false);
|
||||
if (webStorage.isWeb) {
|
||||
webStorage.clear();
|
||||
webStorage.clearSession();
|
||||
}
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(false);
|
||||
locale_web.LocaleStorageImpl.forceSessionStorageForTests(false);
|
||||
if (webStorage.isWeb) {
|
||||
webStorage.clear();
|
||||
webStorage.clearSession();
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'localStorage write/read (웹)',
|
||||
() {
|
||||
if (!webStorage.isWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocaleStorage.write('ko');
|
||||
expect(webStorage.get('locale'), 'ko');
|
||||
expect(LocaleStorage.read(), 'ko');
|
||||
},
|
||||
skip: !webStorage.isWeb,
|
||||
);
|
||||
|
||||
test(
|
||||
'legacy key에서 locale로 마이그레이션 (웹)',
|
||||
() {
|
||||
if (!webStorage.isWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
webStorage.set('baron_locale', 'en');
|
||||
expect(LocaleStorage.read(), 'en');
|
||||
expect(webStorage.get('locale'), 'en');
|
||||
expect(webStorage.get('baron_locale'), isNull);
|
||||
},
|
||||
skip: !webStorage.isWeb,
|
||||
);
|
||||
|
||||
test(
|
||||
'localStorage 접근이 차단되면 메모리 fallback (웹)',
|
||||
() {
|
||||
if (!webStorage.isWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(true);
|
||||
|
||||
LocaleStorage.write('en');
|
||||
expect(webStorage.get('locale'), isNull);
|
||||
expect(webStorage.getSession('locale'), isNull);
|
||||
expect(LocaleStorage.read(), 'en');
|
||||
},
|
||||
skip: !webStorage.isWeb,
|
||||
);
|
||||
|
||||
test(
|
||||
'localStorage 접근이 차단되면 sessionStorage로 fallback (웹)',
|
||||
() {
|
||||
if (!webStorage.isWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
locale_web.LocaleStorageImpl.forceSessionStorageForTests(true);
|
||||
|
||||
LocaleStorage.write('ko');
|
||||
expect(webStorage.get('locale'), isNull);
|
||||
expect(webStorage.getSession('locale'), 'ko');
|
||||
expect(LocaleStorage.read(), 'ko');
|
||||
},
|
||||
skip: !webStorage.isWeb,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('locale_utils', () {
|
||||
test('normalizeLocaleCode handles supported locales', () {
|
||||
expect(normalizeLocaleCode('ko'), 'ko');
|
||||
expect(normalizeLocaleCode('ko-KR'), 'ko');
|
||||
expect(normalizeLocaleCode('en'), 'en');
|
||||
expect(normalizeLocaleCode('en-US'), 'en');
|
||||
});
|
||||
|
||||
test('normalizeLocaleCode falls back to default', () {
|
||||
expect(normalizeLocaleCode('ja'), defaultLocaleCode);
|
||||
expect(normalizeLocaleCode(null), defaultLocaleCode);
|
||||
expect(normalizeLocaleCode(''), defaultLocaleCode);
|
||||
});
|
||||
|
||||
test('extractLocaleFromPath picks locale when present', () {
|
||||
expect(extractLocaleFromPath(Uri.parse('/ko/signin')), 'ko');
|
||||
expect(extractLocaleFromPath(Uri.parse('/en/profile')), 'en');
|
||||
expect(extractLocaleFromPath(Uri.parse('/ko')), 'ko');
|
||||
});
|
||||
|
||||
test('extractLocaleFromPath returns null when missing', () {
|
||||
expect(extractLocaleFromPath(Uri.parse('/signin')), isNull);
|
||||
expect(extractLocaleFromPath(Uri.parse('/zz/signin')), isNull);
|
||||
});
|
||||
|
||||
test('stripLocalePath removes locale segment', () {
|
||||
expect(stripLocalePath(Uri.parse('/ko/signin')), '/signin');
|
||||
expect(stripLocalePath(Uri.parse('/en/profile')), '/profile');
|
||||
expect(stripLocalePath(Uri.parse('/ko')), '/');
|
||||
expect(stripLocalePath(Uri.parse('/en/')), '/');
|
||||
});
|
||||
|
||||
test('stripLocalePath keeps path without locale', () {
|
||||
expect(stripLocalePath(Uri.parse('/signin')), '/signin');
|
||||
expect(stripLocalePath(Uri.parse('/auth/callback')), '/auth/callback');
|
||||
});
|
||||
|
||||
test('buildLocalizedPath applies locale', () {
|
||||
expect(buildLocalizedPath('ko', Uri.parse('/signin')), '/ko/signin');
|
||||
expect(buildLocalizedPath('en', Uri.parse('/signin')), '/en/signin');
|
||||
expect(buildLocalizedPath('ko', Uri.parse('/')), '/ko');
|
||||
expect(buildLocalizedPath('en', Uri.parse('/')), '/en');
|
||||
});
|
||||
|
||||
test('buildLocalizedPath preserves query parameters', () {
|
||||
final uri = Uri.parse('/signin?redirect_uri=https://example.com');
|
||||
expect(
|
||||
buildLocalizedPath('ko', uri),
|
||||
'/ko/signin?redirect_uri=https%3A%2F%2Fexample.com',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildLocalizedPath replaces existing locale', () {
|
||||
expect(buildLocalizedPath('en', Uri.parse('/ko/signin')), '/en/signin');
|
||||
expect(buildLocalizedPath('ko', Uri.parse('/en/profile')), '/ko/profile');
|
||||
});
|
||||
|
||||
test('buildLocalizedPath drops unknown 2-letter prefix', () {
|
||||
expect(buildLocalizedPath('ko', Uri.parse('/zz/signin')), '/ko/signin');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,15 +5,36 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:userfront/main.dart' show BaronSSOApp;
|
||||
|
||||
class _TestAssetLoader extends AssetLoader {
|
||||
const _TestAssetLoader();
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> load(String path, Locale locale) async {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('BaronSSOApp builds', (WidgetTester tester) async {
|
||||
// runApp에서 ProviderScope로 감싸서 쓰고 있으니 테스트도 동일하게 감쌈
|
||||
await tester.pumpWidget(const ProviderScope(child: BaronSSOApp()));
|
||||
await tester.pumpWidget(
|
||||
EasyLocalization(
|
||||
supportedLocales: const [Locale('en'), Locale('ko')],
|
||||
fallbackLocale: const Locale('en'),
|
||||
startLocale: const Locale('en'),
|
||||
path: 'assets/translations',
|
||||
assetLoader: const _TestAssetLoader(),
|
||||
child: const ProviderScope(child: BaronSSOApp()),
|
||||
),
|
||||
);
|
||||
await tester.pump(); // 한 프레임 더
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user