forked from baron/baron-sso
i18n 대대적 변경
This commit is contained in:
@@ -7,7 +7,7 @@ COPY . .
|
|||||||
# Get dependencies and build for web
|
# Get dependencies and build for web
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
RUN touch .env
|
RUN touch .env
|
||||||
RUN flutter build web --release --no-tree-shake-icons
|
RUN flutter build web --release --no-tree-shake-icons --wasm
|
||||||
|
|
||||||
# Stage 2: Serve with Nginx
|
# Stage 2: Serve with Nginx
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
@@ -33,12 +34,14 @@ class AuditService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
print("Audit log sent successfully");
|
debugPrint('Audit log sent successfully');
|
||||||
} else {
|
} else {
|
||||||
print("Failed to send audit log: ${response.statusCode} ${response.body}");
|
debugPrint(
|
||||||
|
'Failed to send audit log: ${response.statusCode} ${response.body}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error sending audit log: $e");
|
debugPrint('Error sending audit log: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
import 'http_client.dart';
|
import 'http_client.dart';
|
||||||
import 'web_window.dart';
|
import 'web_window.dart';
|
||||||
import 'auth_token_store.dart';
|
import 'auth_token_store.dart';
|
||||||
@@ -30,13 +31,26 @@ class AuthProxyService {
|
|||||||
return drySend == true;
|
return drySend == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Exception _error(String key, String fallback, {String? detail}) {
|
||||||
|
return Exception(
|
||||||
|
tr(
|
||||||
|
key,
|
||||||
|
fallback: fallback,
|
||||||
|
params: detail != null ? {'error': detail} : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> fetchPasswordPolicy() async {
|
static Future<Map<String, dynamic>> fetchPasswordPolicy() async {
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/auth/password/policy');
|
final url = Uri.parse('$_baseUrl/api/v1/auth/password/policy');
|
||||||
final response = await http.get(url);
|
final response = await http.get(url);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to fetch password policy');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.password_policy_fetch',
|
||||||
|
'비밀번호 정책을 불러오지 못했습니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +66,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
}
|
}
|
||||||
throw Exception('Failed to load profile: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.profile_load',
|
||||||
|
'프로필을 불러오지 못했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
@@ -107,7 +125,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to init login: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.login_init',
|
||||||
|
'로그인 초기화에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +150,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 400) {
|
if (response.statusCode == 400) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
}
|
}
|
||||||
throw Exception('Polling failed: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.login_poll',
|
||||||
|
'로그인 상태 확인에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> verifyMagicLink(String token, {bool verifyOnly = false}) async {
|
static Future<Map<String, dynamic>> verifyMagicLink(String token, {bool verifyOnly = false}) async {
|
||||||
@@ -146,7 +172,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Verification failed: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.verify_failed',
|
||||||
|
'검증에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +206,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Verification failed: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.verify_failed',
|
||||||
|
'검증에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +232,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Verification failed: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.verify_failed',
|
||||||
|
'검증에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +263,13 @@ class AuthProxyService {
|
|||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to login');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.login_failed',
|
||||||
|
fallback: '로그인에 실패했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static Future<Map<String, dynamic>> getConsentInfo(String consentChallenge) async {
|
static Future<Map<String, dynamic>> getConsentInfo(String consentChallenge) async {
|
||||||
@@ -239,7 +283,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to get consent info');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.consent_fetch',
|
||||||
|
fallback: '동의 정보를 가져오지 못했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +312,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to accept consent');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.consent_accept',
|
||||||
|
fallback: '동의 처리에 실패했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +338,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to reject consent');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.consent_reject',
|
||||||
|
fallback: '동의 거부에 실패했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +373,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to accept OIDC login');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.oidc_accept',
|
||||||
|
fallback: 'OIDC 로그인 승인에 실패했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
@@ -334,7 +402,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to initiate password reset');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.password_reset_init',
|
||||||
|
fallback: '비밀번호 재설정을 시작하지 못했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +435,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? 'Failed to complete password reset');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.password_reset_complete',
|
||||||
|
fallback: '비밀번호 재설정에 실패했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +457,11 @@ class AuthProxyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to send SMS: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.sms_send',
|
||||||
|
'SMS 전송에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +480,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to verify code: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.code_verify',
|
||||||
|
'인증 코드 확인에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +498,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to init QR login: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.qr_init',
|
||||||
|
'QR 로그인을 시작하지 못했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +520,11 @@ class AuthProxyService {
|
|||||||
if (response.statusCode == 400) {
|
if (response.statusCode == 400) {
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
}
|
}
|
||||||
throw Exception('QR Polling failed: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.qr_poll',
|
||||||
|
'QR 상태 확인에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> approveQrLogin(
|
static Future<void> approveQrLogin(
|
||||||
@@ -462,7 +558,11 @@ class AuthProxyService {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('QR Approval failed: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.qr_approve',
|
||||||
|
'QR 승인에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client?.close();
|
client?.close();
|
||||||
@@ -509,7 +609,11 @@ class AuthProxyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to create user: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.user_create',
|
||||||
|
'사용자 생성에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +635,11 @@ class AuthProxyService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
return data['users'] ?? [];
|
return data['users'] ?? [];
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to list users: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.user_list',
|
||||||
|
'사용자 목록 조회에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,7 +656,11 @@ class AuthProxyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to delete user: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.user_delete',
|
||||||
|
'사용자 삭제에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +678,11 @@ class AuthProxyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to update status: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.user_status_update',
|
||||||
|
'상태 업데이트에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +711,11 @@ class AuthProxyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to update user: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.user_update',
|
||||||
|
'사용자 수정에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +742,10 @@ class AuthProxyService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
return data['items'] ?? [];
|
return data['items'] ?? [];
|
||||||
} else {
|
} else {
|
||||||
throw Exception('연동된 앱 목록을 불러오지 못했습니다.');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.linked_apps_load',
|
||||||
|
'연동된 앱 목록을 불러오지 못했습니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
@@ -650,7 +773,13 @@ class AuthProxyService {
|
|||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(errorBody['error'] ?? '연동 해지에 실패했습니다.');
|
throw Exception(
|
||||||
|
errorBody['error'] ??
|
||||||
|
tr(
|
||||||
|
'err.userfront.auth_proxy.linked_app_revoke',
|
||||||
|
fallback: '연동 해지에 실패했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
@@ -688,7 +817,6 @@ class AuthProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int _clientLogFailureCount = 0;
|
static int _clientLogFailureCount = 0;
|
||||||
static DateTime? _clientLogLastFailureAt;
|
|
||||||
static DateTime? _clientLogOpenUntil;
|
static DateTime? _clientLogOpenUntil;
|
||||||
|
|
||||||
static bool _canSendClientLog() {
|
static bool _canSendClientLog() {
|
||||||
@@ -702,7 +830,6 @@ class AuthProxyService {
|
|||||||
|
|
||||||
static void _recordClientLogFailure() {
|
static void _recordClientLogFailure() {
|
||||||
_clientLogFailureCount += 1;
|
_clientLogFailureCount += 1;
|
||||||
_clientLogLastFailureAt = DateTime.now();
|
|
||||||
if (_clientLogFailureCount >= 3) {
|
if (_clientLogFailureCount >= 3) {
|
||||||
_clientLogOpenUntil = DateTime.now().add(const Duration(minutes: 1));
|
_clientLogOpenUntil = DateTime.now().add(const Duration(minutes: 1));
|
||||||
_clientLogFailureCount = 0;
|
_clientLogFailureCount = 0;
|
||||||
@@ -711,7 +838,6 @@ class AuthProxyService {
|
|||||||
|
|
||||||
static void _recordClientLogSuccess() {
|
static void _recordClientLogSuccess() {
|
||||||
_clientLogFailureCount = 0;
|
_clientLogFailureCount = 0;
|
||||||
_clientLogLastFailureAt = null;
|
|
||||||
_clientLogOpenUntil = null;
|
_clientLogOpenUntil = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +869,11 @@ class AuthProxyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to send code: ${response.body}');
|
throw _error(
|
||||||
|
'err.userfront.auth_proxy.phone_code_send',
|
||||||
|
'인증 코드 전송에 실패했습니다: {{error}}',
|
||||||
|
detail: response.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
|
|
||||||
import 'dart:html' as html;
|
import 'dart:html' as html;
|
||||||
|
|
||||||
class AuthTokenStore {
|
class AuthTokenStore {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
void implSendLoginSuccess(String token) {
|
void implSendLoginSuccess(String token) {
|
||||||
// No-op on non-web platforms
|
// No-op on non-web platforms
|
||||||
print("Not on web: Login Success with token: $token");
|
debugPrint('Not on web: Login Success with token: $token');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool implIsPopup() {
|
bool implIsPopup() {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'dart:html' as html;
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
void implSendLoginSuccess(String token) {
|
void implSendLoginSuccess(String token) {
|
||||||
final message = {'type': 'LOGIN_SUCCESS', 'token': token};
|
final message = {'type': 'LOGIN_SUCCESS', 'token': token};
|
||||||
@@ -7,9 +10,9 @@ void implSendLoginSuccess(String token) {
|
|||||||
if (html.window.opener != null) {
|
if (html.window.opener != null) {
|
||||||
try {
|
try {
|
||||||
html.window.opener!.postMessage(message, '*');
|
html.window.opener!.postMessage(message, '*');
|
||||||
print("Sent login success message to opener");
|
debugPrint('Sent login success message to opener');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Failed to postMessage: $e");
|
debugPrint('Failed to postMessage: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the popup after a short delay to ensure message sending
|
// Close the popup after a short delay to ensure message sending
|
||||||
@@ -18,7 +21,7 @@ void implSendLoginSuccess(String token) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Should not happen given isPopup check, but as fallback:
|
// Should not happen given isPopup check, but as fallback:
|
||||||
print("No opener found during popup flow.");
|
debugPrint('No opener found during popup flow.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
|
|
||||||
import 'dart:html' as html;
|
import 'dart:html' as html;
|
||||||
|
|
||||||
class WebWindow {
|
class WebWindow {
|
||||||
|
|||||||
@@ -345,10 +345,9 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
|||||||
? const Center(child: Text("No users found."))
|
? const Center(child: Text("No users found."))
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
itemCount: _users.length,
|
itemCount: _users.length,
|
||||||
separatorBuilder: (_, __) => const Divider(),
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final user = _users[index];
|
final user = _users[index];
|
||||||
final userObj = user['user'] ?? {}; // 응답 구조가 케이스마다 다를 수 있음
|
|
||||||
// 일부 응답은 최상위 또는 user 하위에 필드를 포함합니다.
|
// 일부 응답은 최상위 또는 user 하위에 필드를 포함합니다.
|
||||||
|
|
||||||
final loginIDs = (user['loginIds'] as List?) ?? [];
|
final loginIDs = (user['loginIds'] as List?) ?? [];
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ class ConsentScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ConsentScreenState extends State<ConsentScreen> {
|
class _ConsentScreenState extends State<ConsentScreen> {
|
||||||
static const _ink = Color(0xFF1A1F2C);
|
|
||||||
static const _surface = Colors.white;
|
|
||||||
static const _border = Color(0xFFE5E7EB);
|
|
||||||
static const _subtle = Color(0xFFF7F8FA);
|
|
||||||
static const _accent = Color(0xFF2563EB);
|
|
||||||
|
|
||||||
Map<String, dynamic>? _consentInfo;
|
Map<String, dynamic>? _consentInfo;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
@@ -28,7 +22,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
|||||||
final Set<String> _selectedScopes = {};
|
final Set<String> _selectedScopes = {};
|
||||||
|
|
||||||
// 권한별 설명 매핑 (동적으로 업데이트됨)
|
// 권한별 설명 매핑 (동적으로 업데이트됨)
|
||||||
Map<String, String> _scopeDescriptions = {
|
final Map<String, String> _scopeDescriptions = {
|
||||||
'openid': 'OpenID 인증 정보 (로그인 상태 확인)',
|
'openid': 'OpenID 인증 정보 (로그인 상태 확인)',
|
||||||
'profile': '기본 프로필 정보 (이름, 사용자 식별자)',
|
'profile': '기본 프로필 정보 (이름, 사용자 식별자)',
|
||||||
'email': '이메일 주소 (계정 식별 및 알림 용도)',
|
'email': '이메일 주소 (계정 식별 및 알림 용도)',
|
||||||
@@ -37,7 +31,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 필수 권한 목록 (동적으로 업데이트됨)
|
// 필수 권한 목록 (동적으로 업데이트됨)
|
||||||
Set<String> _mandatoryScopes = {'openid'};
|
final Set<String> _mandatoryScopes = {'openid'};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -333,7 +327,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
activeColor: Theme.of(context).primaryColor,
|
activeColor: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../core/constants/error_whitelist.dart';
|
import '../../../core/constants/error_whitelist.dart';
|
||||||
import '../../../core/services/auth_proxy_service.dart';
|
import '../../../core/services/auth_proxy_service.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
class ErrorScreen extends StatelessWidget {
|
class ErrorScreen extends StatelessWidget {
|
||||||
final String? errorId;
|
final String? errorId;
|
||||||
@@ -23,19 +24,38 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
final isProd = isProdOverride ?? AuthProxyService.isProdEnv;
|
final isProd = isProdOverride ?? AuthProxyService.isProdEnv;
|
||||||
final normalizedCode = (errorCode ?? '').trim();
|
final normalizedCode = (errorCode ?? '').trim();
|
||||||
final hasCode = normalizedCode.isNotEmpty;
|
final hasCode = normalizedCode.isNotEmpty;
|
||||||
final whitelistMessage = errorWhitelistMessages[normalizedCode];
|
final whitelistFallback = errorWhitelistMessages[normalizedCode];
|
||||||
final isWhitelisted = whitelistMessage != null;
|
final isWhitelisted = whitelistFallback != null;
|
||||||
final errorType = isProd
|
final errorType = isProd
|
||||||
? (isWhitelisted && hasCode ? normalizedCode : 'unknown_error')
|
? (isWhitelisted && hasCode ? normalizedCode : 'unknown_error')
|
||||||
: (hasCode ? normalizedCode : 'unknown_error');
|
: (hasCode ? normalizedCode : 'unknown_error');
|
||||||
final title = isProd
|
final title = isProd
|
||||||
? '인증 과정에서 오류가 발생했습니다'
|
? tr('msg.userfront.error.title', fallback: '인증 과정에서 오류가 발생했습니다')
|
||||||
: (hasCode ? '오류: $normalizedCode' : '오류가 발생했습니다');
|
: (hasCode
|
||||||
|
? tr(
|
||||||
|
'msg.userfront.error.title_with_code',
|
||||||
|
fallback: '오류: {{code}}',
|
||||||
|
params: {'code': normalizedCode},
|
||||||
|
)
|
||||||
|
: tr('msg.userfront.error.title_generic', fallback: '오류가 발생했습니다'));
|
||||||
final detail = isProd
|
final detail = isProd
|
||||||
? (isWhitelisted ? whitelistMessage! : '에러가 계속되면 관리자에게 문의해주세요')
|
? (isWhitelisted
|
||||||
|
? tr(
|
||||||
|
'msg.userfront.error.whitelist.$normalizedCode',
|
||||||
|
fallback: whitelistFallback,
|
||||||
|
)
|
||||||
|
: tr(
|
||||||
|
'msg.userfront.error.detail_contact',
|
||||||
|
fallback: '에러가 계속되면 관리자에게 문의해주세요',
|
||||||
|
))
|
||||||
: ((description?.isNotEmpty == true)
|
: ((description?.isNotEmpty == true)
|
||||||
? description!
|
? description!
|
||||||
: (hasCode ? '오류가 발생했습니다.' : '요청을 처리하는 중 문제가 발생했습니다.'));
|
: (hasCode
|
||||||
|
? tr('msg.userfront.error.detail_generic', fallback: '오류가 발생했습니다.')
|
||||||
|
: tr(
|
||||||
|
'msg.userfront.error.detail_request',
|
||||||
|
fallback: '요청을 처리하는 중 문제가 발생했습니다.',
|
||||||
|
)));
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF7F8FA),
|
backgroundColor: const Color(0xFFF7F8FA),
|
||||||
@@ -72,7 +92,11 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'오류 종류: $errorType',
|
tr(
|
||||||
|
'msg.userfront.error.type',
|
||||||
|
fallback: '오류 종류: {{type}}',
|
||||||
|
params: {'type': errorType},
|
||||||
|
),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: const Color(0xFF6B7280),
|
color: const Color(0xFF6B7280),
|
||||||
),
|
),
|
||||||
@@ -80,7 +104,11 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
if (errorId != null && errorId!.isNotEmpty) ...[
|
if (errorId != null && errorId!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'오류 ID: $errorId',
|
tr(
|
||||||
|
'msg.userfront.error.id',
|
||||||
|
fallback: '오류 ID: {{id}}',
|
||||||
|
params: {'id': errorId!},
|
||||||
|
),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: const Color(0xFF6B7280),
|
color: const Color(0xFF6B7280),
|
||||||
),
|
),
|
||||||
@@ -101,7 +129,9 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('로그인으로 이동'),
|
child: Text(
|
||||||
|
tr('ui.userfront.error.go_login', fallback: '로그인으로 이동'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () => context.go('/'),
|
onPressed: () => context.go('/'),
|
||||||
@@ -113,7 +143,9 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('홈으로 이동'),
|
child: Text(
|
||||||
|
tr('ui.userfront.error.go_home', fallback: '홈으로 이동'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../core/services/auth_proxy_service.dart';
|
import '../../../core/services/auth_proxy_service.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
class ForgotPasswordScreen extends StatefulWidget {
|
class ForgotPasswordScreen extends StatefulWidget {
|
||||||
const ForgotPasswordScreen({super.key});
|
const ForgotPasswordScreen({super.key});
|
||||||
@@ -22,7 +23,12 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
Future<void> _handlePasswordReset() async {
|
Future<void> _handlePasswordReset() async {
|
||||||
final input = _loginIdController.text.trim();
|
final input = _loginIdController.text.trim();
|
||||||
if (input.isEmpty) {
|
if (input.isEmpty) {
|
||||||
_showError("이메일 또는 휴대폰 번호를 입력해주세요.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.forgot.input_required',
|
||||||
|
fallback: '이메일 또는 휴대폰 번호를 입력해주세요.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +47,13 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
await AuthProxyService.initiatePasswordReset(loginId, drySend: _drySendEnabled);
|
await AuthProxyService.initiatePasswordReset(loginId, drySend: _drySendEnabled);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text("비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요."),
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.forgot.sent',
|
||||||
|
fallback: '비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -50,7 +61,13 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError("전송에 실패했습니다: ${e.toString()}");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.forgot.error',
|
||||||
|
fallback: '전송에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -77,7 +94,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("비밀번호 재설정"),
|
title: Text(tr('ui.userfront.forgot.title', fallback: '비밀번호 재설정')),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
@@ -89,7 +106,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"비밀번호를 잊으셨나요?",
|
tr('ui.userfront.forgot.heading', fallback: '비밀번호를 잊으셨나요?'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -106,13 +123,16 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
border: Border.all(color: const Color(0xFFFFC107)),
|
border: Border.all(color: const Color(0xFFFFC107)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
const Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
"drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.",
|
tr(
|
||||||
style: TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
|
'msg.userfront.forgot.dry_send',
|
||||||
|
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -120,18 +140,25 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
"계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.",
|
tr(
|
||||||
|
'msg.userfront.forgot.description',
|
||||||
|
fallback:
|
||||||
|
'계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.',
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _loginIdController,
|
controller: _loginIdController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "이메일 또는 휴대폰 번호",
|
labelText: tr(
|
||||||
border: OutlineInputBorder(),
|
'ui.userfront.forgot.input_label',
|
||||||
prefixIcon: Icon(Icons.person_outline),
|
fallback: '이메일 또는 휴대폰 번호',
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _handlePasswordReset(),
|
onSubmitted: (_) => _handlePasswordReset(),
|
||||||
),
|
),
|
||||||
@@ -147,7 +174,12 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
width: 20,
|
width: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
)
|
)
|
||||||
: const Text("재설정 링크 전송"),
|
: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.forgot.submit',
|
||||||
|
fallback: '재설정 링크 전송',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
import '../../../core/services/web_auth_integration.dart';
|
import '../../../core/services/web_auth_integration.dart';
|
||||||
import '../../../core/services/auth_proxy_service.dart';
|
import '../../../core/services/auth_proxy_service.dart';
|
||||||
import '../../../core/services/auth_token_store.dart';
|
import '../../../core/services/auth_token_store.dart';
|
||||||
@@ -51,10 +52,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
bool _verificationOnly = false;
|
bool _verificationOnly = false;
|
||||||
bool _verificationApproved = false;
|
bool _verificationApproved = false;
|
||||||
String _verificationMessage = '';
|
String _verificationMessage = '';
|
||||||
String _verificationTitle = '승인 완료';
|
String _verificationTitle = tr(
|
||||||
String _verificationPageTitle = '로그인 승인';
|
'ui.userfront.login.verification.title',
|
||||||
String _verificationActionLabel = '확인';
|
fallback: '승인 완료',
|
||||||
String _verificationActionPath = '/';
|
);
|
||||||
|
String _verificationPageTitle = tr(
|
||||||
|
'ui.userfront.login.verification.page_title',
|
||||||
|
fallback: '로그인 승인',
|
||||||
|
);
|
||||||
|
String _verificationActionLabel = tr(
|
||||||
|
'ui.userfront.login.verification.action_label',
|
||||||
|
fallback: '확인',
|
||||||
|
);
|
||||||
Timer? _verificationRedirectTimer;
|
Timer? _verificationRedirectTimer;
|
||||||
bool _noticeHandled = false;
|
bool _noticeHandled = false;
|
||||||
bool _drySendEnabled = false;
|
bool _drySendEnabled = false;
|
||||||
@@ -92,7 +101,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
if (!_noticeHandled && notice == 'qr_login_required') {
|
if (!_noticeHandled && notice == 'qr_login_required') {
|
||||||
_noticeHandled = true;
|
_noticeHandled = true;
|
||||||
_showInfo('로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다');
|
_showInfo(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.qr_login_required',
|
||||||
|
fallback: '로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_verificationOnly) {
|
if (!_verificationOnly) {
|
||||||
@@ -125,7 +139,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
_showError("로그인 확인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.cookie_check_failed',
|
||||||
|
fallback: '로그인 확인 실패: {{error}}',
|
||||||
|
params: {
|
||||||
|
'error': e.toString().replaceFirst('Exception: ', ''),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +316,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
_startCountdown();
|
_startCountdown();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError("Failed to init QR: $e");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.qr_init_failed',
|
||||||
|
fallback: 'QR 초기화에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
if (mounted) setState(() => _isQrLoading = false);
|
if (mounted) setState(() => _isQrLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,7 +374,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (res['error'] == 'expired_token') {
|
if (res['error'] == 'expired_token') {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
_qrCountdownTimer?.cancel();
|
_qrCountdownTimer?.cancel();
|
||||||
_showError("QR 세션이 만료되었습니다.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.qr_expired',
|
||||||
|
fallback: 'QR 세션이 만료되었습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +390,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (token is String && token.isNotEmpty) {
|
if (token is String && token.isNotEmpty) {
|
||||||
_completeLoginFromToken(token);
|
_completeLoginFromToken(token);
|
||||||
} else {
|
} else {
|
||||||
_showError("로그인 토큰을 확인할 수 없습니다.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.token_missing',
|
||||||
|
fallback: '로그인 토큰을 확인할 수 없습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -423,21 +461,35 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
void _markVerificationApproved(
|
void _markVerificationApproved(
|
||||||
String message, {
|
String message, {
|
||||||
String title = '승인 완료',
|
String? title,
|
||||||
String pageTitle = '로그인 승인',
|
String? pageTitle,
|
||||||
String actionLabel = '확인',
|
String? actionLabel,
|
||||||
String actionPath = '/',
|
String actionPath = '/',
|
||||||
bool autoRedirect = false,
|
bool autoRedirect = false,
|
||||||
Duration redirectDelay = const Duration(seconds: 2),
|
Duration redirectDelay = const Duration(seconds: 2),
|
||||||
}) {
|
}) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final resolvedTitle = title ??
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.verification.title',
|
||||||
|
fallback: '승인 완료',
|
||||||
|
);
|
||||||
|
final resolvedPageTitle = pageTitle ??
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.verification.page_title',
|
||||||
|
fallback: '로그인 승인',
|
||||||
|
);
|
||||||
|
final resolvedActionLabel = actionLabel ??
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.verification.action_label',
|
||||||
|
fallback: '확인',
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_verificationApproved = true;
|
_verificationApproved = true;
|
||||||
_verificationMessage = message;
|
_verificationMessage = message;
|
||||||
_verificationTitle = title;
|
_verificationTitle = resolvedTitle;
|
||||||
_verificationPageTitle = pageTitle;
|
_verificationPageTitle = resolvedPageTitle;
|
||||||
_verificationActionLabel = actionLabel;
|
_verificationActionLabel = resolvedActionLabel;
|
||||||
_verificationActionPath = actionPath;
|
|
||||||
});
|
});
|
||||||
_verificationRedirectTimer?.cancel();
|
_verificationRedirectTimer?.cancel();
|
||||||
if (autoRedirect) {
|
if (autoRedirect) {
|
||||||
@@ -463,7 +515,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
_verificationMessage.isEmpty ? '로그인 승인에 성공했습니다.' : _verificationMessage,
|
_verificationMessage.isEmpty
|
||||||
|
? tr(
|
||||||
|
'msg.userfront.login.verification.success',
|
||||||
|
fallback: '로그인 승인에 성공했습니다.',
|
||||||
|
)
|
||||||
|
: _verificationMessage,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(color: Colors.black54),
|
style: const TextStyle(color: Colors.black54),
|
||||||
),
|
),
|
||||||
@@ -490,6 +547,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
Future<void> _verifyToken(String token) async {
|
Future<void> _verifyToken(String token) async {
|
||||||
debugPrint("[Auth] Starting verification for token: $token");
|
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 {
|
try {
|
||||||
// Use Backend to verify the token (Backend-Driven Flow)
|
// Use Backend to verify the token (Backend-Driven Flow)
|
||||||
final res = await AuthProxyService.verifyMagicLink(
|
final res = await AuthProxyService.verifyMagicLink(
|
||||||
@@ -505,7 +570,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (status == 'approved' || (jwt == null && _verificationOnly)) {
|
if (status == 'approved' || (jwt == null && _verificationOnly)) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -515,13 +580,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (jwt is String && jwt.isNotEmpty) {
|
if (jwt is String && jwt.isNotEmpty) {
|
||||||
if (hasLocalSession) {
|
if (hasLocalSession) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
localSessionMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -529,14 +594,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
|
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError("Verification failed: $e");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.verification_failed',
|
||||||
|
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,6 +615,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
Future<void> _verifyLoginCode(String loginId, String code, {String? pendingRef}) async {
|
Future<void> _verifyLoginCode(String loginId, String code, {String? pendingRef}) async {
|
||||||
final sanitizedLoginId = loginId.replaceAll(' ', '+');
|
final sanitizedLoginId = loginId.replaceAll(' ', '+');
|
||||||
debugPrint("[Auth] Starting code verification for loginId: $sanitizedLoginId");
|
debugPrint("[Auth] Starting code verification for loginId: $sanitizedLoginId");
|
||||||
|
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 {
|
try {
|
||||||
final res = await AuthProxyService.verifyLoginCode(
|
final res = await AuthProxyService.verifyLoginCode(
|
||||||
sanitizedLoginId,
|
sanitizedLoginId,
|
||||||
@@ -560,7 +643,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (jwt == null && status == 'approved') {
|
if (jwt == null && status == 'approved') {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -570,22 +653,32 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (jwt is String && jwt.isNotEmpty) {
|
if (jwt is String && jwt.isNotEmpty) {
|
||||||
if (hasLocalSession) {
|
if (hasLocalSession) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
localSessionMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_verificationOnly) {
|
if (_verificationOnly) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_markVerificationApproved("링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.",
|
_markVerificationApproved(
|
||||||
title: '링크 로그인 완료',
|
linkLoginMessage,
|
||||||
pageTitle: '링크 로그인',
|
title: tr(
|
||||||
actionLabel: '로그인 화면으로 이동',
|
'ui.userfront.login.link.title',
|
||||||
|
fallback: '링크 로그인 완료',
|
||||||
|
),
|
||||||
|
pageTitle: tr(
|
||||||
|
'ui.userfront.login.link.page_title',
|
||||||
|
fallback: '링크 로그인',
|
||||||
|
),
|
||||||
|
actionLabel: tr(
|
||||||
|
'ui.userfront.login.link.action_label',
|
||||||
|
fallback: '로그인 화면으로 이동',
|
||||||
|
),
|
||||||
actionPath: '/signin',
|
actionPath: '/signin',
|
||||||
autoRedirect: true,
|
autoRedirect: true,
|
||||||
);
|
);
|
||||||
@@ -594,14 +687,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
if (_verificationOnly && mounted) {
|
if (_verificationOnly && mounted) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
|
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError("Verification failed: $e");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.verification_failed',
|
||||||
|
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,6 +709,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
final sanitized = shortCode.trim().toUpperCase();
|
final sanitized = shortCode.trim().toUpperCase();
|
||||||
if (sanitized.isEmpty) return;
|
if (sanitized.isEmpty) return;
|
||||||
debugPrint("[Auth] Starting short code verification for code: $sanitized");
|
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 {
|
try {
|
||||||
final res = await AuthProxyService.verifyLoginShortCode(
|
final res = await AuthProxyService.verifyLoginShortCode(
|
||||||
sanitized,
|
sanitized,
|
||||||
@@ -624,7 +731,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (jwt == null && status == 'approved') {
|
if (jwt == null && status == 'approved') {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -634,14 +741,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (jwt is String && jwt.isNotEmpty) {
|
if (jwt is String && jwt.isNotEmpty) {
|
||||||
if (hasLocalSession) {
|
if (hasLocalSession) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
localSessionMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_verificationOnly) {
|
if (_verificationOnly) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -652,14 +759,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
if (_verificationOnly && mounted) {
|
if (_verificationOnly && mounted) {
|
||||||
_markVerificationApproved(
|
_markVerificationApproved(
|
||||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
approvedMessage,
|
||||||
actionPath: actionPath,
|
actionPath: actionPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[Auth] Short code verification FAILED. Error: $e");
|
debugPrint("[Auth] Short code verification FAILED. Error: $e");
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError("Verification failed: $e");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.verification_failed',
|
||||||
|
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,7 +795,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
final input = _passwordLoginIdController.text.trim();
|
final input = _passwordLoginIdController.text.trim();
|
||||||
final password = _passwordController.text.trim();
|
final password = _passwordController.text.trim();
|
||||||
if (input.isEmpty || password.isEmpty) {
|
if (input.isEmpty || password.isEmpty) {
|
||||||
_showError("이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.password.missing_credentials',
|
||||||
|
fallback: '이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +839,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (e.toString().contains("User not registered")) {
|
if (e.toString().contains("User not registered")) {
|
||||||
_showUnregisteredDialog();
|
_showUnregisteredDialog();
|
||||||
} else {
|
} else {
|
||||||
_showError("로그인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.password.failed',
|
||||||
|
fallback: '로그인 실패: {{error}}',
|
||||||
|
params: {
|
||||||
|
'error': e.toString().replaceFirst('Exception: ', ''),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -746,7 +872,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (e.toString().contains("User not registered")) {
|
if (e.toString().contains("User not registered")) {
|
||||||
_showUnregisteredDialog();
|
_showUnregisteredDialog();
|
||||||
} else {
|
} else {
|
||||||
_showError("오류: $e");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link_failed',
|
||||||
|
fallback: '오류: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -782,9 +914,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
});
|
});
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
_showInfo(isEmail
|
_showInfo(
|
||||||
? "입력하신 이메일로 로그인 링크를 보냈습니다."
|
isEmail
|
||||||
: "입력하신 번호로 로그인 링크를 보냈습니다.");
|
? tr(
|
||||||
|
'msg.userfront.login.link_sent_email',
|
||||||
|
fallback: '입력하신 이메일로 로그인 링크를 보냈습니다.',
|
||||||
|
)
|
||||||
|
: tr(
|
||||||
|
'msg.userfront.login.link_sent_phone',
|
||||||
|
fallback: '입력하신 번호로 로그인 링크를 보냈습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final initialInterval = (interval is int && interval > 0)
|
final initialInterval = (interval is int && interval > 0)
|
||||||
? Duration(seconds: interval)
|
? Duration(seconds: interval)
|
||||||
@@ -806,7 +946,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (e.toString().contains("User not registered")) {
|
if (e.toString().contains("User not registered")) {
|
||||||
_showUnregisteredDialog();
|
_showUnregisteredDialog();
|
||||||
} else {
|
} else {
|
||||||
_showError("전송 실패: $e");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link_send_failed',
|
||||||
|
fallback: '전송 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -842,7 +988,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (result['error'] == 'expired_token') {
|
if (result['error'] == 'expired_token') {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_showError("Login timed out.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link_timeout',
|
||||||
|
fallback: '로그인 요청 시간이 초과되었습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -862,7 +1013,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (mounted && Navigator.canPop(context)) {
|
if (mounted && Navigator.canPop(context)) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
_showError("로그인 토큰을 확인할 수 없습니다.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.token_missing',
|
||||||
|
fallback: '로그인 토큰을 확인할 수 없습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -873,7 +1029,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
|
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_showError("Login timed out.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link_timeout',
|
||||||
|
fallback: '로그인 요청 시간이 초과되었습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,7 +1111,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError("OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.oidc_failed',
|
||||||
|
fallback: 'OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -978,12 +1144,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text("미등록 회원"),
|
title: Text(
|
||||||
content: const Text("가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요."),
|
tr('ui.userfront.login.unregistered.title', fallback: '미등록 회원'),
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.unregistered.body',
|
||||||
|
fallback: '가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text("취소"),
|
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -991,7 +1164,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
_resetLinkLoginState();
|
_resetLinkLoginState();
|
||||||
context.push('/signup');
|
context.push('/signup');
|
||||||
},
|
},
|
||||||
child: const Text("회원가입 하기"),
|
child: Text(
|
||||||
|
tr('ui.userfront.login.unregistered.action', fallback: '회원가입 하기'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1028,8 +1203,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Baron 로그인",
|
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -1045,13 +1220,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
border: Border.all(color: const Color(0xFFFFC107)),
|
border: Border.all(color: const Color(0xFFFFC107)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
const Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
"drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.",
|
tr(
|
||||||
style: TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
|
'msg.userfront.login.dry_send',
|
||||||
|
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF8A6D3B),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1062,10 +1243,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
TabBar(
|
TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
tabs: const [
|
tabs: [
|
||||||
Tab(text: "비밀번호"),
|
Tab(
|
||||||
Tab(text: "로그인 링크"),
|
text: tr(
|
||||||
Tab(text: "QR 코드"),
|
'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 코드',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -1081,10 +1277,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: _passwordLoginIdController,
|
controller: _passwordLoginIdController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "이메일 또는 휴대폰 번호",
|
labelText: tr(
|
||||||
border: OutlineInputBorder(),
|
'ui.userfront.login.field.login_id',
|
||||||
prefixIcon: Icon(Icons.person_outline),
|
fallback: '이메일 또는 휴대폰 번호',
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _handlePasswordLogin(),
|
onSubmitted: (_) => _handlePasswordLogin(),
|
||||||
),
|
),
|
||||||
@@ -1092,10 +1291,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
TextField(
|
TextField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "비밀번호",
|
labelText: tr(
|
||||||
border: OutlineInputBorder(),
|
'ui.userfront.login.field.password',
|
||||||
prefixIcon: Icon(Icons.lock_outline),
|
fallback: '비밀번호',
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _handlePasswordLogin(),
|
onSubmitted: (_) => _handlePasswordLogin(),
|
||||||
),
|
),
|
||||||
@@ -1105,7 +1307,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(50),
|
minimumSize: const Size.fromHeight(50),
|
||||||
),
|
),
|
||||||
child: const Text("로그인"),
|
child: Text(
|
||||||
|
tr('ui.userfront.login.action.submit', fallback: '로그인'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1118,11 +1322,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (_linkPendingRef == null) ...[
|
if (_linkPendingRef == null) ...[
|
||||||
TextField(
|
TextField(
|
||||||
controller: _linkIdController,
|
controller: _linkIdController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "이메일 또는 휴대폰 번호",
|
labelText: tr(
|
||||||
hintText: "",
|
'ui.userfront.login.field.login_id',
|
||||||
border: OutlineInputBorder(),
|
fallback: '이메일 또는 휴대폰 번호',
|
||||||
prefixIcon: Icon(Icons.person_outline),
|
),
|
||||||
|
hintText: '',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _handleLinkLogin(),
|
onSubmitted: (_) => _handleLinkLogin(),
|
||||||
),
|
),
|
||||||
@@ -1132,19 +1339,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(50),
|
minimumSize: const Size.fromHeight(50),
|
||||||
),
|
),
|
||||||
child: const Text("로그인 링크 전송"),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.link.send',
|
||||||
|
fallback: '로그인 링크 전송',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
Text(
|
||||||
"입력하신 정보로 로그인 링크를 전송합니다.",
|
tr(
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
'msg.userfront.login.link.helper',
|
||||||
|
fallback: '입력하신 정보로 로그인 링크를 전송합니다.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (_linkPendingRef != null) ...[
|
if (_linkPendingRef != null) ...[
|
||||||
const Text(
|
Text(
|
||||||
"링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.",
|
tr(
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
'msg.userfront.login.link.short_code_help',
|
||||||
|
fallback: '링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -1155,11 +1373,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _shortCodePrefixController,
|
controller: _shortCodePrefixController,
|
||||||
textCapitalization: TextCapitalization.characters,
|
textCapitalization: TextCapitalization.characters,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "영문 2자리",
|
labelText: tr(
|
||||||
border: OutlineInputBorder(),
|
'ui.userfront.login.short_code.prefix',
|
||||||
hintText: "AB",
|
fallback: '영문 2자리',
|
||||||
hintStyle: TextStyle(color: Colors.grey),
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'AB',
|
||||||
|
hintStyle: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
maxLength: 2,
|
maxLength: 2,
|
||||||
),
|
),
|
||||||
@@ -1171,12 +1392,21 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
controller: _shortCodeDigitsController,
|
controller: _shortCodeDigitsController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "숫자 6자리",
|
labelText: tr(
|
||||||
|
'ui.userfront.login.short_code.digits',
|
||||||
|
fallback: '숫자 6자리',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: "345678",
|
hintText: '345678',
|
||||||
hintStyle: const TextStyle(color: Colors.grey),
|
hintStyle: const TextStyle(color: Colors.grey),
|
||||||
suffixText: _linkExpireSeconds > 0
|
suffixText: _linkExpireSeconds > 0
|
||||||
? "유효시간 ${_formatTime(_linkExpireSeconds)}"
|
? tr(
|
||||||
|
'ui.userfront.login.short_code.expire_time',
|
||||||
|
fallback: '유효시간 {{time}}',
|
||||||
|
params: {
|
||||||
|
'time': _formatTime(_linkExpireSeconds),
|
||||||
|
},
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
maxLength: 6,
|
maxLength: 6,
|
||||||
@@ -1190,7 +1420,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
final prefix = _shortCodePrefixController.text.trim().toUpperCase();
|
final prefix = _shortCodePrefixController.text.trim().toUpperCase();
|
||||||
final digits = _shortCodeDigitsController.text.trim();
|
final digits = _shortCodeDigitsController.text.trim();
|
||||||
if (prefix.length != 2 || digits.length != 6) {
|
if (prefix.length != 2 || digits.length != 6) {
|
||||||
_showError("문자 2개와 숫자 6자리를 입력해 주세요.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.short_code.invalid',
|
||||||
|
fallback: '문자 2개와 숫자 6자리를 입력해 주세요.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_verifyShortCode(prefix + digits);
|
_verifyShortCode(prefix + digits);
|
||||||
@@ -1198,18 +1433,36 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(45),
|
minimumSize: const Size.fromHeight(45),
|
||||||
),
|
),
|
||||||
child: const Text("코드로 로그인"),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.short_code.submit',
|
||||||
|
fallback: '코드로 로그인',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_linkResendSeconds > 0) {
|
if (_linkResendSeconds > 0) {
|
||||||
_showInfo("재발송은 ${_formatTime(_linkResendSeconds)} 후 가능합니다.");
|
_showInfo(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link.resend_wait',
|
||||||
|
fallback: '재발송은 {{time}} 후 가능합니다.',
|
||||||
|
params: {
|
||||||
|
'time': _formatTime(_linkResendSeconds),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final loginId = _lastLinkLoginId ?? _linkIdController.text.trim();
|
final loginId = _lastLinkLoginId ?? _linkIdController.text.trim();
|
||||||
if (loginId.isEmpty) {
|
if (loginId.isEmpty) {
|
||||||
_showError("이메일 또는 휴대폰 번호를 입력해 주세요.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link.missing_login_id',
|
||||||
|
fallback: '이메일 또는 휴대폰 번호를 입력해 주세요.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_startEnchantedFlow(
|
_startEnchantedFlow(
|
||||||
@@ -1220,8 +1473,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
_linkResendSeconds > 0
|
_linkResendSeconds > 0
|
||||||
? "재발송 (${_formatTime(_linkResendSeconds)})"
|
? tr(
|
||||||
: "재발송",
|
'ui.userfront.login.link.resend_with_time',
|
||||||
|
fallback: '재발송 ({{time}})',
|
||||||
|
params: {
|
||||||
|
'time': _formatTime(_linkResendSeconds),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: tr(
|
||||||
|
'ui.common.resend',
|
||||||
|
fallback: '재발송',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!_lastLinkIsEmail) ...[
|
if (!_lastLinkIsEmail) ...[
|
||||||
@@ -1229,12 +1491,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_linkResendSeconds > 0) {
|
if (_linkResendSeconds > 0) {
|
||||||
_showInfo("재발송은 ${_formatTime(_linkResendSeconds)} 후 가능합니다.");
|
_showInfo(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link.resend_wait',
|
||||||
|
fallback: '재발송은 {{time}} 후 가능합니다.',
|
||||||
|
params: {
|
||||||
|
'time': _formatTime(_linkResendSeconds),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final loginId = _lastLinkLoginId ?? _linkIdController.text.trim();
|
final loginId = _lastLinkLoginId ?? _linkIdController.text.trim();
|
||||||
if (loginId.isEmpty) {
|
if (loginId.isEmpty) {
|
||||||
_showError("휴대폰 번호를 입력해 주세요.");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.link.missing_phone',
|
||||||
|
fallback: '휴대폰 번호를 입력해 주세요.',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_startEnchantedFlow(
|
_startEnchantedFlow(
|
||||||
@@ -1243,7 +1518,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
codeOnly: true,
|
codeOnly: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text("코드만 받기(${_formatTime(_linkResendSeconds)})"),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.link.code_only',
|
||||||
|
fallback: '코드만 받기({{time}})',
|
||||||
|
params: {
|
||||||
|
'time': _formatTime(_linkResendSeconds),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1276,8 +1559,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
_qrRemainingSeconds > 0
|
_qrRemainingSeconds > 0
|
||||||
? "남은 시간: ${_formatTime(_qrRemainingSeconds)}"
|
? tr(
|
||||||
: "QR 코드 만료됨",
|
'ui.userfront.login.qr.remaining',
|
||||||
|
fallback: '남은 시간: {{time}}',
|
||||||
|
params: {
|
||||||
|
'time': _formatTime(_qrRemainingSeconds),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: tr(
|
||||||
|
'ui.userfront.login.qr.expired',
|
||||||
|
fallback: 'QR 코드 만료됨',
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _qrRemainingSeconds > 30 ? Colors.blue : Colors.red,
|
color: _qrRemainingSeconds > 30 ? Colors.blue : Colors.red,
|
||||||
@@ -1285,19 +1577,33 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
Text(
|
||||||
"모바일 앱으로 스캔하세요",
|
tr(
|
||||||
|
'msg.userfront.login.qr.scan_hint',
|
||||||
|
fallback: '모바일 앱으로 스캔하세요',
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _startQrFlow,
|
onPressed: _startQrFlow,
|
||||||
child: const Text("QR 코드 새로고침")
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.qr.refresh',
|
||||||
|
fallback: 'QR 코드 새로고침',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Text("QR 코드를 불러오지 못했습니다.", textAlign: TextAlign.center),
|
Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.qr.load_failed',
|
||||||
|
fallback: 'QR 코드를 불러오지 못했습니다.',
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1308,15 +1614,31 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.push('/forgot-password'),
|
onPressed: () => context.push('/forgot-password'),
|
||||||
child: const Text("비밀번호를 잊으셨나요?"),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.forgot_password',
|
||||||
|
fallback: '비밀번호를 잊으셨나요?',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text("계정이 없으신가요?", style: TextStyle(color: Colors.grey, fontSize: 14)),
|
Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.login.no_account',
|
||||||
|
fallback: '계정이 없으신가요?',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.push('/signup'),
|
onPressed: () => context.push('/signup'),
|
||||||
child: const Text("회원가입"),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login.signup',
|
||||||
|
fallback: '회원가입',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
class LoginSuccessScreen extends StatelessWidget {
|
class LoginSuccessScreen extends StatelessWidget {
|
||||||
const LoginSuccessScreen({super.key});
|
const LoginSuccessScreen({super.key});
|
||||||
@@ -16,17 +17,17 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
const Icon(Icons.check_circle_outline, size: 80, color: Colors.green),
|
const Icon(Icons.check_circle_outline, size: 80, color: Colors.green),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
"로그인 완료",
|
tr('ui.userfront.login_success.title', fallback: '로그인 완료'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
"성공적으로 로그인되었습니다.",
|
tr('msg.userfront.login_success.subtitle', fallback: '성공적으로 로그인되었습니다.'),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 16),
|
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
@@ -36,7 +37,9 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
context.push('/scan');
|
context.push('/scan');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.camera_alt, size: 28),
|
icon: const Icon(Icons.camera_alt, size: 28),
|
||||||
label: const Text("QR 인증 (카메라 켜기)"),
|
label: Text(
|
||||||
|
tr('ui.userfront.login_success.qr', fallback: 'QR 인증 (카메라 켜기)'),
|
||||||
|
),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
|
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
|
||||||
backgroundColor: Colors.blue.shade700,
|
backgroundColor: Colors.blue.shade700,
|
||||||
@@ -51,7 +54,13 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/');
|
context.go('/');
|
||||||
},
|
},
|
||||||
child: const Text("나중에 하기 (대시보드로 이동)", style: TextStyle(color: Colors.grey)),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.login_success.later',
|
||||||
|
fallback: '나중에 하기 (대시보드로 이동)',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import '../../../core/services/auth_proxy_service.dart';
|
import '../../../core/services/auth_proxy_service.dart';
|
||||||
import '../../../core/services/auth_token_store.dart';
|
import '../../../core/services/auth_token_store.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
class QRScanScreen extends StatefulWidget {
|
class QRScanScreen extends StatefulWidget {
|
||||||
const QRScanScreen({super.key});
|
const QRScanScreen({super.key});
|
||||||
@@ -143,7 +144,10 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSuccess = true;
|
_isSuccess = true;
|
||||||
_resultMessage = 'QR 승인 완료! PC 화면에서 로그인이 진행됩니다.';
|
_resultMessage = tr(
|
||||||
|
'msg.userfront.qr.approve_success',
|
||||||
|
fallback: 'QR 승인 완료! PC 화면에서 로그인이 진행됩니다.',
|
||||||
|
);
|
||||||
_isProcessing = false;
|
_isProcessing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,7 +156,11 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSuccess = false;
|
_isSuccess = false;
|
||||||
_resultMessage = 'QR 승인 실패: $e';
|
_resultMessage = tr(
|
||||||
|
'msg.userfront.qr.approve_error',
|
||||||
|
fallback: 'QR 승인 실패: {{error}}',
|
||||||
|
params: {'error': '$e'},
|
||||||
|
);
|
||||||
_isProcessing = false;
|
_isProcessing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -181,8 +189,13 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
_log.warning('Camera permission request failed: $e');
|
_log.warning('Camera permission request failed: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.'),
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.qr.permission_error',
|
||||||
|
fallback: '카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -198,7 +211,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
final success = _isSuccess == true;
|
final success = _isSuccess == true;
|
||||||
final icon = success ? Icons.check_circle_outline : Icons.error_outline;
|
final icon = success ? Icons.check_circle_outline : Icons.error_outline;
|
||||||
final color = success ? Colors.green : Colors.red;
|
final color = success ? Colors.green : Colors.red;
|
||||||
final title = success ? '승인 완료' : '승인 실패';
|
final title = success
|
||||||
|
? tr('ui.userfront.qr.result_success', fallback: '승인 완료')
|
||||||
|
: tr('ui.userfront.qr.result_failure', fallback: '승인 실패');
|
||||||
final message = _resultMessage ?? '';
|
final message = _resultMessage ?? '';
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
@@ -223,12 +238,12 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
if (!success)
|
if (!success)
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _resetScan,
|
onPressed: _resetScan,
|
||||||
child: const Text('다시 스캔'),
|
child: Text(tr('ui.userfront.qr.rescan', fallback: '다시 스캔')),
|
||||||
),
|
),
|
||||||
if (success)
|
if (success)
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
child: const Text('닫기'),
|
child: Text(tr('ui.common.close', fallback: '닫기')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -240,7 +255,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Scan QR Code'),
|
title: Text(tr('ui.userfront.qr.title', fallback: 'Scan QR Code')),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
@@ -263,8 +278,15 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
isPermissionDenied
|
isPermissionDenied
|
||||||
? '카메라 권한이 필요합니다.'
|
? tr(
|
||||||
: '카메라 오류: ${error.errorCode}',
|
'msg.userfront.qr.permission_required',
|
||||||
|
fallback: '카메라 권한이 필요합니다.',
|
||||||
|
)
|
||||||
|
: tr(
|
||||||
|
'msg.userfront.qr.camera_error',
|
||||||
|
fallback: '카메라 오류: {{error}}',
|
||||||
|
params: {'error': '${error.errorCode}'},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
@@ -273,8 +295,11 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
: _requestCameraPermission,
|
: _requestCameraPermission,
|
||||||
child: Text(
|
child: Text(
|
||||||
_isRequestingCamera
|
_isRequestingCamera
|
||||||
? '요청 중...'
|
? tr('ui.common.requesting', fallback: '요청 중...')
|
||||||
: '카메라 권한 요청하기',
|
: tr(
|
||||||
|
'ui.userfront.qr.request_permission',
|
||||||
|
fallback: '카메라 권한 요청하기',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../core/services/auth_proxy_service.dart';
|
import '../../../core/services/auth_proxy_service.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
class ResetPasswordScreen extends StatefulWidget {
|
class ResetPasswordScreen extends StatefulWidget {
|
||||||
final String? loginId; // Now receiving loginId
|
final String? loginId; // Now receiving loginId
|
||||||
@@ -66,7 +67,12 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
Future<void> _handlePasswordReset() async {
|
Future<void> _handlePasswordReset() async {
|
||||||
if (_formKey.currentState?.validate() != true) return;
|
if (_formKey.currentState?.validate() != true) return;
|
||||||
if ((_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)) {
|
if ((_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)) {
|
||||||
_showError("유효하지 않은 재설정 링크입니다. (loginId/token 누락)");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.reset.invalid_link',
|
||||||
|
fallback: '유효하지 않은 재설정 링크입니다. (loginId/token 누락)',
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +87,13 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text("비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요."),
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.reset.success',
|
||||||
|
fallback: '비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -90,7 +101,13 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError("비밀번호 변경에 실패했습니다: ${e.toString()}");
|
_showError(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.reset.error.generic',
|
||||||
|
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -107,7 +124,10 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
|
|
||||||
String _buildPolicyDescription() {
|
String _buildPolicyDescription() {
|
||||||
if (_isPolicyLoading) {
|
if (_isPolicyLoading) {
|
||||||
return "비밀번호 정책을 불러오는 중입니다...";
|
return tr(
|
||||||
|
'msg.userfront.reset.policy_loading',
|
||||||
|
fallback: '비밀번호 정책을 불러오는 중입니다...',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||||
@@ -116,14 +136,42 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
final requiresNumber = _policy?['number'] ?? true;
|
final requiresNumber = _policy?['number'] ?? true;
|
||||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||||
|
|
||||||
final parts = <String>["최소 ${minLength}자 이상"];
|
final parts = <String>[
|
||||||
|
tr(
|
||||||
|
'msg.userfront.reset.policy.min_length',
|
||||||
|
fallback: '최소 {{count}}자 이상',
|
||||||
|
params: {'count': '$minLength'},
|
||||||
|
),
|
||||||
|
];
|
||||||
if (minTypes > 0) {
|
if (minTypes > 0) {
|
||||||
parts.add("영문 대/소문자/숫자/특수문자 중 ${minTypes}가지 이상");
|
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개 이상'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresUpper) {
|
||||||
|
parts.add(
|
||||||
|
tr('msg.userfront.reset.policy.uppercase', fallback: '대문자 1개 이상'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresNumber) {
|
||||||
|
parts.add(
|
||||||
|
tr('msg.userfront.reset.policy.number', fallback: '숫자 1개 이상'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresSymbol) {
|
||||||
|
parts.add(
|
||||||
|
tr('msg.userfront.reset.policy.symbol', fallback: '특수문자 1개 이상'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (requiresLower) parts.add("소문자 1개 이상");
|
|
||||||
if (requiresUpper) parts.add("대문자 1개 이상");
|
|
||||||
if (requiresNumber) parts.add("숫자 1개 이상");
|
|
||||||
if (requiresSymbol) parts.add("특수문자 1개 이상");
|
|
||||||
|
|
||||||
return parts.join(", ");
|
return parts.join(", ");
|
||||||
}
|
}
|
||||||
@@ -132,7 +180,9 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("새 비밀번호 설정"),
|
title: Text(
|
||||||
|
tr('ui.userfront.reset.title', fallback: '새 비밀번호 설정'),
|
||||||
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
@@ -148,7 +198,10 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"새로운 비밀번호 설정",
|
tr(
|
||||||
|
'ui.userfront.reset.subtitle',
|
||||||
|
fallback: '새로운 비밀번호 설정',
|
||||||
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -166,7 +219,10 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: _isPasswordObscured,
|
obscureText: _isPasswordObscured,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "새 비밀번호",
|
labelText: tr(
|
||||||
|
'ui.userfront.reset.new_password',
|
||||||
|
fallback: '새 비밀번호',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
@@ -183,11 +239,18 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
validator: (value) {
|
validator: (value) {
|
||||||
final val = value ?? "";
|
final val = value ?? "";
|
||||||
if (val.isEmpty) {
|
if (val.isEmpty) {
|
||||||
return '비밀번호를 입력해주세요.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.empty_password',
|
||||||
|
fallback: '비밀번호를 입력해주세요.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||||
if (val.length < minLength) {
|
if (val.length < minLength) {
|
||||||
return '비밀번호는 최소 $minLength자 이상이어야 합니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.min_length',
|
||||||
|
fallback: '비밀번호는 최소 {{count}}자 이상이어야 합니다.',
|
||||||
|
params: {'count': '$minLength'},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final hasLower = RegExp(r'[a-z]').hasMatch(val);
|
final hasLower = RegExp(r'[a-z]').hasMatch(val);
|
||||||
final hasUpper = RegExp(r'[A-Z]').hasMatch(val);
|
final hasUpper = RegExp(r'[A-Z]').hasMatch(val);
|
||||||
@@ -201,20 +264,37 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
|
|
||||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||||
if (minTypes > 0 && typeCount < minTypes) {
|
if (minTypes > 0 && typeCount < minTypes) {
|
||||||
return '비밀번호는 영문 대/소문자/숫자/특수문자 중 $minTypes가지 이상 포함해야 합니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.min_types',
|
||||||
|
fallback:
|
||||||
|
'비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.',
|
||||||
|
params: {'count': '$minTypes'},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
||||||
return '최소 1개 이상의 소문자를 포함해야 합니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.lowercase',
|
||||||
|
fallback: '최소 1개 이상의 소문자를 포함해야 합니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
||||||
return '최소 1개 이상의 대문자를 포함해야 합니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.uppercase',
|
||||||
|
fallback: '최소 1개 이상의 대문자를 포함해야 합니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ((_policy?['number'] ?? true) && !hasNumber) {
|
if ((_policy?['number'] ?? true) && !hasNumber) {
|
||||||
return '최소 1개 이상의 숫자를 포함해야 합니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.number',
|
||||||
|
fallback: '최소 1개 이상의 숫자를 포함해야 합니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ((_policy?['nonAlphanumeric'] ?? true) && !hasSymbol) {
|
if ((_policy?['nonAlphanumeric'] ?? true) && !hasSymbol) {
|
||||||
return '최소 1개 이상의 특수문자를 포함해야 합니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.symbol',
|
||||||
|
fallback: '최소 1개 이상의 특수문자를 포함해야 합니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -224,7 +304,10 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
controller: _confirmPasswordController,
|
controller: _confirmPasswordController,
|
||||||
obscureText: _isConfirmPasswordObscured,
|
obscureText: _isConfirmPasswordObscured,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "새 비밀번호 확인",
|
labelText: tr(
|
||||||
|
'ui.userfront.reset.confirm_password',
|
||||||
|
fallback: '새 비밀번호 확인',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
@@ -240,7 +323,10 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != _passwordController.text) {
|
if (value != _passwordController.text) {
|
||||||
return '비밀번호가 일치하지 않습니다.';
|
return tr(
|
||||||
|
'msg.userfront.reset.error.mismatch',
|
||||||
|
fallback: '비밀번호가 일치하지 않습니다.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -255,9 +341,17 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Text("비밀번호 변경"),
|
: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.reset.submit',
|
||||||
|
fallback: '비밀번호 변경',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -268,20 +362,24 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInvalidTokenView() {
|
Widget _buildInvalidTokenView() {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, color: Colors.red, size: 60),
|
const Icon(Icons.error_outline, color: Colors.red, size: 60),
|
||||||
SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
"유효하지 않은 링크입니다.",
|
tr('msg.userfront.reset.invalid_title',
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
fallback: '유효하지 않은 링크입니다.'),
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
"비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요.",
|
tr(
|
||||||
|
'msg.userfront.reset.invalid_body',
|
||||||
|
fallback: '비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요.',
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
import '../../../core/services/auth_proxy_service.dart';
|
import '../../../core/services/auth_proxy_service.dart';
|
||||||
|
|
||||||
class SignupScreen extends StatefulWidget {
|
class SignupScreen extends StatefulWidget {
|
||||||
@@ -130,8 +131,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
_emailTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_emailTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_emailSeconds > 0) _emailSeconds--;
|
if (_emailSeconds > 0) {
|
||||||
else timer.cancel();
|
_emailSeconds--;
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -140,8 +144,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
_phoneTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_phoneTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_phoneSeconds > 0) _phoneSeconds--;
|
if (_phoneSeconds > 0) {
|
||||||
else timer.cancel();
|
_phoneSeconds--;
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -157,20 +164,30 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
final email = _emailController.text.trim();
|
final email = _emailController.text.trim();
|
||||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||||
if (!emailRegex.hasMatch(email)) {
|
if (!emailRegex.hasMatch(email)) {
|
||||||
setState(() => _emailError = '유효한 이메일 형식이 아닙니다.');
|
setState(() => _emailError = tr(
|
||||||
|
'msg.userfront.signup.email.invalid',
|
||||||
|
fallback: '유효한 이메일 형식이 아닙니다.',
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() { _isLoading = true; _emailError = null; });
|
setState(() { _isLoading = true; _emailError = null; });
|
||||||
try {
|
try {
|
||||||
final available = await AuthProxyService.checkEmailAvailability(email);
|
final available = await AuthProxyService.checkEmailAvailability(email);
|
||||||
if (!available) {
|
if (!available) {
|
||||||
setState(() => _emailError = '이미 가입된 이메일입니다.');
|
setState(() => _emailError = tr(
|
||||||
|
'msg.userfront.signup.email.duplicate',
|
||||||
|
fallback: '이미 가입된 이메일입니다.',
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await AuthProxyService.sendSignupCode(email, 'email');
|
await AuthProxyService.sendSignupCode(email, 'email');
|
||||||
_startTimer('email');
|
_startTimer('email');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _emailError = '발송 실패: $e');
|
setState(() => _emailError = tr(
|
||||||
|
'msg.userfront.signup.email.send_failed',
|
||||||
|
fallback: '발송 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
));
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
@@ -189,10 +206,17 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
_emailError = null;
|
_emailError = null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() => _emailError = '인증코드가 일치하지 않습니다.');
|
setState(() => _emailError = tr(
|
||||||
|
'msg.userfront.signup.email.code_mismatch',
|
||||||
|
fallback: '인증코드가 일치하지 않습니다.',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _emailError = '인증 실패: $e');
|
setState(() => _emailError = tr(
|
||||||
|
'msg.userfront.signup.email.verify_failed',
|
||||||
|
fallback: '인증 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +228,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
await AuthProxyService.sendSignupCode(phone, 'phone');
|
await AuthProxyService.sendSignupCode(phone, 'phone');
|
||||||
_startTimer('phone');
|
_startTimer('phone');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _phoneError = '발송 실패: $e');
|
setState(() => _phoneError = tr(
|
||||||
|
'msg.userfront.signup.phone.send_failed',
|
||||||
|
fallback: '발송 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
));
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
@@ -223,16 +251,26 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
_phoneError = null;
|
_phoneError = null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() => _phoneError = '인증코드가 일치하지 않습니다.');
|
setState(() => _phoneError = tr(
|
||||||
|
'msg.userfront.signup.phone.code_mismatch',
|
||||||
|
fallback: '인증코드가 일치하지 않습니다.',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _phoneError = '인증 실패: $e');
|
setState(() => _phoneError = tr(
|
||||||
|
'msg.userfront.signup.phone.verify_failed',
|
||||||
|
fallback: '인증 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleSignup() async {
|
Future<void> _handleSignup() async {
|
||||||
if (_passwordController.text != _confirmPasswordController.text) {
|
if (_passwordController.text != _confirmPasswordController.text) {
|
||||||
setState(() => _confirmPasswordError = '비밀번호가 일치하지 않습니다.');
|
setState(() => _confirmPasswordError = tr(
|
||||||
|
'msg.userfront.signup.password.mismatch',
|
||||||
|
fallback: '비밀번호가 일치하지 않습니다.',
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
@@ -257,12 +295,38 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
String eStr = e.toString().toLowerCase();
|
String eStr = e.toString().toLowerCase();
|
||||||
setState(() {
|
setState(() {
|
||||||
if (eStr.contains('uppercase')) _passwordError = '대문자가 최소 1개 이상 포함되어야 합니다.';
|
if (eStr.contains('uppercase')) {
|
||||||
else if (eStr.contains('lowercase')) _passwordError = '소문자가 최소 1개 이상 포함되어야 합니다.';
|
_passwordError = tr(
|
||||||
else if (eStr.contains('digit') || eStr.contains('number')) _passwordError = '숫자가 최소 1개 이상 포함되어야 합니다.';
|
'msg.userfront.signup.password.uppercase_required',
|
||||||
else if (eStr.contains('symbol') || eStr.contains('special')) _passwordError = '특수문자가 최소 1개 이상 포함되어야 합니다.';
|
fallback: '대문자가 최소 1개 이상 포함되어야 합니다.',
|
||||||
else if (eStr.contains('length') || eStr.contains('12 characters')) _passwordError = '비밀번호는 최소 12자 이상이어야 합니다.';
|
);
|
||||||
else _passwordError = '가입 실패: $e';
|
} 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()},
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
@@ -274,9 +338,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('회원가입 완료'),
|
title: Text(
|
||||||
content: const Text('성공적으로 가입되었습니다.'),
|
tr('msg.userfront.signup.success.title', fallback: '회원가입 완료'),
|
||||||
actions: [TextButton(onPressed: () => context.go('/signin'), child: const Text('로그인하기'))],
|
),
|
||||||
|
content: Text(
|
||||||
|
tr('msg.userfront.signup.success.body', fallback: '성공적으로 가입되었습니다.'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.go('/signin'),
|
||||||
|
child: Text(
|
||||||
|
tr('ui.userfront.signup.success.action', fallback: '로그인하기'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -288,13 +363,25 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_stepCircle(1, '약관동의'),
|
_stepCircle(
|
||||||
|
1,
|
||||||
|
tr('ui.userfront.signup.steps.agreement', fallback: '약관동의'),
|
||||||
|
),
|
||||||
_stepLine(1),
|
_stepLine(1),
|
||||||
_stepCircle(2, '본인인증'),
|
_stepCircle(
|
||||||
|
2,
|
||||||
|
tr('ui.userfront.signup.steps.verify', fallback: '본인인증'),
|
||||||
|
),
|
||||||
_stepLine(2),
|
_stepLine(2),
|
||||||
_stepCircle(3, '정보입력'),
|
_stepCircle(
|
||||||
|
3,
|
||||||
|
tr('ui.userfront.signup.steps.profile', fallback: '정보입력'),
|
||||||
|
),
|
||||||
_stepLine(3),
|
_stepLine(3),
|
||||||
_stepCircle(4, '비밀번호'),
|
_stepCircle(
|
||||||
|
4,
|
||||||
|
tr('ui.userfront.signup.steps.password', fallback: '비밀번호'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -330,9 +417,17 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('서비스 이용을 위해\n약관에 동의해주세요',
|
Text(
|
||||||
style: TextStyle(
|
tr(
|
||||||
fontSize: 20, fontWeight: FontWeight.bold, height: 1.3)),
|
'msg.userfront.signup.agreement.title',
|
||||||
|
fallback: '서비스 이용을 위해\n약관에 동의해주세요',
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 모두 동의 버튼
|
// 모두 동의 버튼
|
||||||
Container(
|
Container(
|
||||||
@@ -342,8 +437,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
border: Border.all(color: Colors.grey[200]!),
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
),
|
),
|
||||||
child: CheckboxListTile(
|
child: CheckboxListTile(
|
||||||
title: const Text('모두 동의합니다',
|
title: Text(
|
||||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
|
tr(
|
||||||
|
'ui.userfront.signup.agreement.all',
|
||||||
|
fallback: '모두 동의합니다',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
value: _termsAccepted && _privacyAccepted,
|
value: _termsAccepted && _privacyAccepted,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -357,14 +457,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_agreementSection(
|
_agreementSection(
|
||||||
title: '바론 소프트웨어 이용약관 (필수)',
|
title: tr(
|
||||||
|
'ui.userfront.signup.agreement.tos_title',
|
||||||
|
fallback: '바론 소프트웨어 이용약관 (필수)',
|
||||||
|
),
|
||||||
content: _tosText,
|
content: _tosText,
|
||||||
value: _termsAccepted,
|
value: _termsAccepted,
|
||||||
onChanged: (val) => setState(() => _termsAccepted = val!),
|
onChanged: (val) => setState(() => _termsAccepted = val!),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_agreementSection(
|
_agreementSection(
|
||||||
title: '개인정보 수집 및 이용 동의 (필수)',
|
title: tr(
|
||||||
|
'ui.userfront.signup.agreement.privacy_title',
|
||||||
|
fallback: '개인정보 수집 및 이용 동의 (필수)',
|
||||||
|
),
|
||||||
content: _privacyText,
|
content: _privacyText,
|
||||||
value: _privacyAccepted,
|
value: _privacyAccepted,
|
||||||
onChanged: (val) => setState(() => _privacyAccepted = val!),
|
onChanged: (val) => setState(() => _privacyAccepted = val!),
|
||||||
@@ -410,7 +516,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String _tosText = """
|
static String get _tosText => tr(
|
||||||
|
'msg.userfront.signup.tos_full',
|
||||||
|
fallback: """
|
||||||
바론 소프트웨어 이용약관
|
바론 소프트웨어 이용약관
|
||||||
|
|
||||||
제1장 총칙
|
제1장 총칙
|
||||||
@@ -480,9 +588,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.
|
본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.
|
||||||
부칙
|
부칙
|
||||||
본 약관은 2024년 10월 1일부터 시행됩니다.
|
본 약관은 2024년 10월 1일부터 시행됩니다.
|
||||||
""";
|
""",
|
||||||
|
);
|
||||||
|
|
||||||
static const String _privacyText = """
|
static String get _privacyText => tr(
|
||||||
|
'msg.userfront.signup.privacy_full',
|
||||||
|
fallback: """
|
||||||
개인정보 수집 및 이용 동의
|
개인정보 수집 및 이용 동의
|
||||||
|
|
||||||
바론서비스 개인정보처리방침
|
바론서비스 개인정보처리방침
|
||||||
@@ -590,33 +701,46 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.
|
회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.
|
||||||
제8조 (기타)
|
제8조 (기타)
|
||||||
본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.
|
본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.
|
||||||
""";
|
""",
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildStepAuth() {
|
Widget _buildStepAuth() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('본인 확인을 위해\n인증을 진행해주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.auth.title',
|
||||||
|
fallback: '본인 확인을 위해\n인증을 진행해주세요',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 가족사 이메일 안내 문구
|
// 가족사 이메일 안내 문구
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(6)),
|
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(6)),
|
||||||
child: const Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.info_outline, size: 16, color: Colors.blue),
|
const Icon(Icons.info_outline, size: 16, color: Colors.blue),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
|
tr(
|
||||||
style: TextStyle(fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500),
|
'msg.userfront.signup.auth.affiliate_notice',
|
||||||
|
fallback: '가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('이메일 인증', style: const TextStyle(fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
tr('ui.userfront.signup.auth.email.title', fallback: '이메일 인증'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -625,7 +749,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
|
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '이메일 주소',
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.auth.email.label',
|
||||||
|
fallback: '이메일 주소',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _emailError,
|
errorText: _emailError,
|
||||||
hintText: 'example@hanmaceng.co.kr',
|
hintText: 'example@hanmaceng.co.kr',
|
||||||
@@ -639,7 +766,14 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: (_isEmailVerified || _isLoading) ? null : _sendEmailCode,
|
onPressed: (_isEmailVerified || _isLoading) ? null : _sendEmailCode,
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
||||||
child: Text(_emailSeconds > 0 ? '재발송' : '인증요청'),
|
child: Text(
|
||||||
|
_emailSeconds > 0
|
||||||
|
? tr('ui.common.resend', fallback: '재발송')
|
||||||
|
: tr(
|
||||||
|
'ui.userfront.signup.auth.request_code',
|
||||||
|
fallback: '인증요청',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -649,7 +783,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailCodeController,
|
controller: _emailCodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '인증코드 6자리',
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.auth.code_label',
|
||||||
|
fallback: '인증코드 6자리',
|
||||||
|
),
|
||||||
suffixText: _formatTime(_emailSeconds),
|
suffixText: _formatTime(_emailSeconds),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -658,19 +795,40 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
onChanged: (val) { if(val.length == 6) _verifyEmailCode(); },
|
onChanged: (val) { if(val.length == 6) _verifyEmailCode(); },
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (_isEmailVerified) const Padding(
|
if (_isEmailVerified)
|
||||||
padding: EdgeInsets.only(top: 8),
|
Padding(
|
||||||
child: Text('✅ 이메일 인증 완료', style: TextStyle(color: Colors.green, fontSize: 13, fontWeight: FontWeight.bold)),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.email.verified',
|
||||||
|
fallback: '✅ 이메일 인증 완료',
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.green,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('휴대폰 인증', style: const TextStyle(fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
tr('ui.userfront.signup.phone.title', fallback: '휴대폰 인증'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _phoneController,
|
controller: _phoneController,
|
||||||
decoration: InputDecoration(labelText: '휴대폰 번호 (-없이)', border: const OutlineInputBorder(), errorText: _phoneError),
|
decoration: InputDecoration(
|
||||||
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.phone.label',
|
||||||
|
fallback: '휴대폰 번호 (-없이)',
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: _phoneError,
|
||||||
|
),
|
||||||
readOnly: _isPhoneVerified,
|
readOnly: _isPhoneVerified,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
),
|
),
|
||||||
@@ -681,7 +839,14 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: (_isPhoneVerified || _isLoading) ? null : _sendPhoneCode,
|
onPressed: (_isPhoneVerified || _isLoading) ? null : _sendPhoneCode,
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
||||||
child: Text(_phoneSeconds > 0 ? '재발송' : '인증요청'),
|
child: Text(
|
||||||
|
_phoneSeconds > 0
|
||||||
|
? tr('ui.common.resend', fallback: '재발송')
|
||||||
|
: tr(
|
||||||
|
'ui.userfront.signup.auth.request_code',
|
||||||
|
fallback: '인증요청',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -691,7 +856,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _phoneCodeController,
|
controller: _phoneCodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '인증코드 6자리',
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.auth.code_label',
|
||||||
|
fallback: '인증코드 6자리',
|
||||||
|
),
|
||||||
suffixText: _formatTime(_phoneSeconds),
|
suffixText: _formatTime(_phoneSeconds),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -700,10 +868,21 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
onChanged: (val) { if(val.length == 6) _verifyPhoneCode(); },
|
onChanged: (val) { if(val.length == 6) _verifyPhoneCode(); },
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (_isPhoneVerified) const Padding(
|
if (_isPhoneVerified)
|
||||||
padding: EdgeInsets.only(top: 8),
|
Padding(
|
||||||
child: Text('✅ 휴대폰 인증 완료', style: TextStyle(color: Colors.green, fontSize: 13, fontWeight: FontWeight.bold)),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.phone.verified',
|
||||||
|
fallback: '✅ 휴대폰 인증 완료',
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.green,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -712,12 +891,24 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('회원님의\n소속 정보를 알려주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.profile.title',
|
||||||
|
fallback: '회원님의\n소속 정보를 알려주세요',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
onChanged: (_) => setState(() {}),
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: const InputDecoration(labelText: '이름', border: OutlineInputBorder()),
|
decoration: InputDecoration(
|
||||||
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.profile.name',
|
||||||
|
fallback: '이름',
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 소속 유형 선택 (가족사 메일일 경우 비활성화)
|
// 소속 유형 선택 (가족사 메일일 경우 비활성화)
|
||||||
@@ -726,17 +917,51 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _isAffiliateEmail ? 0.7 : 1.0,
|
opacity: _isAffiliateEmail ? 0.7 : 1.0,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _affiliationType,
|
key: ValueKey(_affiliationType),
|
||||||
|
initialValue: _affiliationType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '소속 유형',
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.profile.affiliation_type',
|
||||||
|
fallback: '소속 유형',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
helperText: _isAffiliateEmail ? '가족사 이메일 사용 시 자동으로 선택됩니다.' : null,
|
helperText: _isAffiliateEmail
|
||||||
|
? tr(
|
||||||
|
'msg.userfront.signup.profile.affiliate_hint',
|
||||||
|
fallback: '가족사 이메일 사용 시 자동으로 선택됩니다.',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(value: 'GENERAL', child: Text('일반 사용자')),
|
DropdownMenuItem(
|
||||||
DropdownMenuItem(value: 'AFFILIATE', child: Text('가족사 임직원')),
|
value: 'GENERAL',
|
||||||
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'domain.affiliation.general',
|
||||||
|
fallback: '일반 사용자',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'AFFILIATE',
|
||||||
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'domain.affiliation.affiliate',
|
||||||
|
fallback: '가족사 임직원',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onChanged: _isAffiliateEmail ? null : (val) => setState(() { _affiliationType = val!; }),
|
onChanged: _isAffiliateEmail
|
||||||
|
? null
|
||||||
|
: (val) {
|
||||||
|
if (val == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_affiliationType = val;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -748,17 +973,56 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _isAffiliateEmail ? 0.7 : 1.0,
|
opacity: _isAffiliateEmail ? 0.7 : 1.0,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _companyCode,
|
key: ValueKey(_companyCode ?? 'none'),
|
||||||
decoration: const InputDecoration(labelText: '가족사 선택', border: OutlineInputBorder()),
|
initialValue: _companyCode,
|
||||||
items: const [
|
decoration: InputDecoration(
|
||||||
DropdownMenuItem(value: 'HANMAC', child: Text('한맥')),
|
labelText: tr(
|
||||||
DropdownMenuItem(value: 'SAMAN', child: Text('삼안')),
|
'ui.userfront.signup.profile.company',
|
||||||
DropdownMenuItem(value: 'PTC', child: Text('PTC')),
|
fallback: '가족사 선택',
|
||||||
DropdownMenuItem(value: 'JANGHEON', child: Text('장헌')),
|
),
|
||||||
DropdownMenuItem(value: 'BARON', child: Text('바론')),
|
border: const OutlineInputBorder(),
|
||||||
DropdownMenuItem(value: 'HALLA', child: Text('한라')),
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'HANMAC',
|
||||||
|
child: Text(
|
||||||
|
tr('domain.company.hanmac', fallback: '한맥'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'SAMAN',
|
||||||
|
child: Text(
|
||||||
|
tr('domain.company.saman', fallback: '삼안'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'PTC',
|
||||||
|
child: Text(
|
||||||
|
tr('domain.company.ptc', fallback: 'PTC'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'JANGHEON',
|
||||||
|
child: Text(
|
||||||
|
tr('domain.company.jangheon', fallback: '장헌'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'BARON',
|
||||||
|
child: Text(
|
||||||
|
tr('domain.company.baron', fallback: '바론'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'HALLA',
|
||||||
|
child: Text(
|
||||||
|
tr('domain.company.halla', fallback: '한라'),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onChanged: _isAffiliateEmail ? null : (val) => setState(() => _companyCode = val),
|
onChanged: _isAffiliateEmail
|
||||||
|
? null
|
||||||
|
: (val) => setState(() => _companyCode = val),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -768,7 +1032,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
controller: _deptController,
|
controller: _deptController,
|
||||||
onChanged: (_) => setState(() {}),
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: _affiliationType == 'AFFILIATE' ? '부서명' : '소속 정보 (선택)',
|
labelText: _affiliationType == 'AFFILIATE'
|
||||||
|
? tr('ui.userfront.signup.profile.department', fallback: '부서명')
|
||||||
|
: tr(
|
||||||
|
'ui.userfront.signup.profile.department_optional',
|
||||||
|
fallback: '소속 정보 (선택)',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder()
|
border: const OutlineInputBorder()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -778,7 +1047,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
|
|
||||||
String _buildPolicyDescription() {
|
String _buildPolicyDescription() {
|
||||||
if (_isPolicyLoading) {
|
if (_isPolicyLoading) {
|
||||||
return "비밀번호 정책을 불러오는 중입니다...";
|
return tr(
|
||||||
|
'msg.userfront.signup.policy.loading',
|
||||||
|
fallback: '비밀번호 정책을 불러오는 중입니다...',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||||
@@ -787,16 +1059,60 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
final requiresNumber = _policy?['number'] ?? true;
|
final requiresNumber = _policy?['number'] ?? true;
|
||||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||||
|
|
||||||
final parts = <String>["최소 $minLength자 이상"];
|
final parts = <String>[
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.policy.min_length',
|
||||||
|
fallback: '최소 {{count}}자 이상',
|
||||||
|
params: {'count': minLength.toString()},
|
||||||
|
),
|
||||||
|
];
|
||||||
if (minTypes > 0) {
|
if (minTypes > 0) {
|
||||||
parts.add("영문 대/소문자/숫자/특수문자 중 ${minTypes}가지 이상");
|
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: '대문자',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresLower) {
|
||||||
|
parts.add(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.policy.lowercase',
|
||||||
|
fallback: '소문자',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresNumber) {
|
||||||
|
parts.add(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.policy.number',
|
||||||
|
fallback: '숫자',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresSymbol) {
|
||||||
|
parts.add(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.policy.symbol',
|
||||||
|
fallback: '특수문자',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (requiresUpper) parts.add("대문자");
|
|
||||||
if (requiresLower) parts.add("소문자");
|
|
||||||
if (requiresNumber) parts.add("숫자");
|
|
||||||
if (requiresSymbol) parts.add("특수문자");
|
|
||||||
|
|
||||||
return "보안 정책: ${parts.join(', ')}";
|
return tr(
|
||||||
|
'msg.userfront.signup.policy.summary',
|
||||||
|
fallback: '보안 정책: {{rules}}',
|
||||||
|
params: {'rules': parts.join(', ')},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStepPassword() {
|
Widget _buildStepPassword() {
|
||||||
@@ -825,7 +1141,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('마지막으로\n비밀번호를 설정해주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.password.title',
|
||||||
|
fallback: '마지막으로\n비밀번호를 설정해주세요',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 비밀번호 정책 안내 박스
|
// 비밀번호 정책 안내 박스
|
||||||
Container(
|
Container(
|
||||||
@@ -850,7 +1172,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (_) => setState(() {}),
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '비밀번호',
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.password.label',
|
||||||
|
fallback: '비밀번호',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _passwordError,
|
errorText: _passwordError,
|
||||||
),
|
),
|
||||||
@@ -859,12 +1184,55 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
children: [
|
children: [
|
||||||
_cryptoCheck('$minLength자 이상', hasLength),
|
_cryptoCheck(
|
||||||
if (minTypes > 0) _cryptoCheck('문자 유형 ${minTypes}가지 이상', hasTypeCount),
|
tr(
|
||||||
if (requiresUpper) _cryptoCheck('대문자', hasUpper),
|
'msg.userfront.signup.password.rule.min_length',
|
||||||
if (requiresLower) _cryptoCheck('소문자', hasLower),
|
fallback: '{{count}}자 이상',
|
||||||
if (requiresNumber) _cryptoCheck('숫자', hasDigit),
|
params: {'count': minLength.toString()},
|
||||||
if (requiresSymbol) _cryptoCheck('특수문자', hasSpecial),
|
),
|
||||||
|
hasLength,
|
||||||
|
),
|
||||||
|
if (minTypes > 0)
|
||||||
|
_cryptoCheck(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.password.rule.min_types',
|
||||||
|
fallback: '문자 유형 {{count}}가지 이상',
|
||||||
|
params: {'count': minTypes.toString()},
|
||||||
|
),
|
||||||
|
hasTypeCount,
|
||||||
|
),
|
||||||
|
if (requiresUpper)
|
||||||
|
_cryptoCheck(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.password.rule.uppercase',
|
||||||
|
fallback: '대문자',
|
||||||
|
),
|
||||||
|
hasUpper,
|
||||||
|
),
|
||||||
|
if (requiresLower)
|
||||||
|
_cryptoCheck(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.password.rule.lowercase',
|
||||||
|
fallback: '소문자',
|
||||||
|
),
|
||||||
|
hasLower,
|
||||||
|
),
|
||||||
|
if (requiresNumber)
|
||||||
|
_cryptoCheck(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.password.rule.number',
|
||||||
|
fallback: '숫자',
|
||||||
|
),
|
||||||
|
hasDigit,
|
||||||
|
),
|
||||||
|
if (requiresSymbol)
|
||||||
|
_cryptoCheck(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.signup.password.rule.symbol',
|
||||||
|
fallback: '특수문자',
|
||||||
|
),
|
||||||
|
hasSpecial,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -873,11 +1241,19 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_confirmPasswordError = (val != _passwordController.text) ? '비밀번호가 일치하지 않습니다.' : null;
|
_confirmPasswordError = (val != _passwordController.text)
|
||||||
|
? tr(
|
||||||
|
'msg.userfront.signup.password.mismatch',
|
||||||
|
fallback: '비밀번호가 일치하지 않습니다.',
|
||||||
|
)
|
||||||
|
: null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '비밀번호 확인',
|
labelText: tr(
|
||||||
|
'ui.userfront.signup.password.confirm_label',
|
||||||
|
fallback: '비밀번호 확인',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _confirmPasswordError,
|
errorText: _confirmPasswordError,
|
||||||
),
|
),
|
||||||
@@ -917,7 +1293,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('회원가입', style: TextStyle(fontWeight: FontWeight.bold)),
|
title: Text(
|
||||||
|
tr('ui.userfront.signup.title', fallback: '회원가입'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
@@ -951,7 +1330,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => setState(() => _currentStep--),
|
onPressed: () => setState(() => _currentStep--),
|
||||||
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55), side: const BorderSide(color: Colors.black)),
|
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55), side: const BorderSide(color: Colors.black)),
|
||||||
child: const Text('이전', style: TextStyle(color: Colors.black)),
|
child: Text(
|
||||||
|
tr('ui.common.prev', fallback: '이전'),
|
||||||
|
style: const TextStyle(color: Colors.black),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -967,7 +1349,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
: Text(_currentStep < 4 ? '다음 단계' : '가입 완료'),
|
: Text(
|
||||||
|
_currentStep < 4
|
||||||
|
? tr('ui.userfront.signup.next_step', fallback: '다음 단계')
|
||||||
|
: tr('ui.userfront.signup.complete', fallback: '가입 완료'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import 'dart:convert';
|
|||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../core/services/auth_proxy_service.dart';
|
|
||||||
import '../../../../core/services/auth_token_store.dart';
|
import '../../../../core/services/auth_token_store.dart';
|
||||||
import '../../../../core/services/http_client.dart';
|
import '../../../../core/services/http_client.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
String _envOrDefault(String key, String fallback) {
|
String _envOrDefault(String key, String fallback) {
|
||||||
@@ -17,19 +17,6 @@ String _envOrDefault(String key, String fallback) {
|
|||||||
|
|
||||||
String get _baseUrl => _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
String get _baseUrl => _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||||
|
|
||||||
Future<List<LinkedRp>> _fetchLinkedRps() async {
|
|
||||||
final items = await AuthProxyService.fetchLinkedRps();
|
|
||||||
final result = <LinkedRp>[];
|
|
||||||
for (final item in items) {
|
|
||||||
if (item is Map) {
|
|
||||||
result.add(LinkedRp.fromJson(Map<String, dynamic>.from(item)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future<AuditPage> _fetchAuthTimelinePage({String? cursor}) async {
|
Future<AuditPage> _fetchAuthTimelinePage({String? cursor}) async {
|
||||||
final queryParameters = <String, String>{
|
final queryParameters = <String, String>{
|
||||||
'limit': '20',
|
'limit': '20',
|
||||||
@@ -192,7 +179,10 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isLoadingMore: false,
|
isLoadingMore: false,
|
||||||
error: '접속이력을 불러오지 못했습니다.',
|
error: tr(
|
||||||
|
'msg.userfront.dashboard.timeline.load_error',
|
||||||
|
fallback: '접속이력을 불러오지 못했습니다.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
import '../models/user_profile_model.dart';
|
import '../models/user_profile_model.dart';
|
||||||
import '../../../../core/services/auth_token_store.dart';
|
import '../../../../core/services/auth_token_store.dart';
|
||||||
import '../../../../core/services/http_client.dart';
|
import '../../../../core/services/http_client.dart';
|
||||||
@@ -23,7 +24,9 @@ class ProfileRepository {
|
|||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) {
|
if (token == null && !useCookie) {
|
||||||
throw Exception('No active session');
|
throw Exception(
|
||||||
|
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||||
@@ -40,7 +43,13 @@ class ProfileRepository {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return UserProfile.fromJson(jsonDecode(response.body));
|
return UserProfile.fromJson(jsonDecode(response.body));
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load profile: ${response.body}');
|
throw Exception(
|
||||||
|
tr(
|
||||||
|
'err.userfront.profile.load_failed',
|
||||||
|
fallback: '프로필을 불러오지 못했습니다: {{error}}',
|
||||||
|
params: {'error': response.body},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +60,11 @@ class ProfileRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) throw Exception('No active session');
|
if (token == null && !useCookie) {
|
||||||
|
throw Exception(
|
||||||
|
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||||
final client = createHttpClient(withCredentials: useCookie);
|
final client = createHttpClient(withCredentials: useCookie);
|
||||||
@@ -73,14 +86,24 @@ class ProfileRepository {
|
|||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to update profile: ${response.body}');
|
throw Exception(
|
||||||
|
tr(
|
||||||
|
'err.userfront.profile.update_failed',
|
||||||
|
fallback: '프로필 업데이트에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': response.body},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendUpdateCode(String phone) async {
|
Future<void> sendUpdateCode(String phone) async {
|
||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) throw Exception('No active session');
|
if (token == null && !useCookie) {
|
||||||
|
throw Exception(
|
||||||
|
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
||||||
final client = createHttpClient(withCredentials: useCookie);
|
final client = createHttpClient(withCredentials: useCookie);
|
||||||
@@ -98,7 +121,13 @@ class ProfileRepository {
|
|||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('인증번호 전송 실패: ${response.body}');
|
throw Exception(
|
||||||
|
tr(
|
||||||
|
'err.userfront.profile.send_code_failed',
|
||||||
|
fallback: '인증번호 전송 실패: {{error}}',
|
||||||
|
params: {'error': response.body},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +137,11 @@ class ProfileRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) throw Exception('No active session');
|
if (token == null && !useCookie) {
|
||||||
|
throw Exception(
|
||||||
|
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
||||||
final client = createHttpClient(withCredentials: useCookie);
|
final client = createHttpClient(withCredentials: useCookie);
|
||||||
@@ -129,14 +162,24 @@ class ProfileRepository {
|
|||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to change password: ${response.body}');
|
throw Exception(
|
||||||
|
tr(
|
||||||
|
'err.userfront.profile.password_change_failed',
|
||||||
|
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
|
||||||
|
params: {'error': response.body},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyUpdateCode(String phone, String code) async {
|
Future<void> verifyUpdateCode(String phone, String code) async {
|
||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) throw Exception('No active session');
|
if (token == null && !useCookie) {
|
||||||
|
throw Exception(
|
||||||
|
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
||||||
final client = createHttpClient(withCredentials: useCookie);
|
final client = createHttpClient(withCredentials: useCookie);
|
||||||
@@ -154,7 +197,13 @@ class ProfileRepository {
|
|||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('인증 실패: ${response.body}');
|
throw Exception(
|
||||||
|
tr(
|
||||||
|
'err.userfront.profile.verify_code_failed',
|
||||||
|
fallback: '인증 실패: {{error}}',
|
||||||
|
params: {'error': response.body},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
import '../../../../core/notifiers/auth_notifier.dart';
|
import '../../../../core/notifiers/auth_notifier.dart';
|
||||||
import '../../../../core/services/auth_token_store.dart';
|
import '../../../../core/services/auth_token_store.dart';
|
||||||
import '../../../../core/ui/layout_breakpoints.dart';
|
import '../../../../core/ui/layout_breakpoints.dart';
|
||||||
@@ -229,14 +230,29 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
});
|
});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('인증번호가 전송되었습니다.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.phone.code_sent',
|
||||||
|
fallback: '인증번호가 전송되었습니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _isVerifying = false);
|
setState(() => _isVerifying = false);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('전송 실패: $e')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.phone.send_failed',
|
||||||
|
fallback: '전송 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +272,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
});
|
});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('인증되었습니다.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.phone.verified',
|
||||||
|
fallback: '인증되었습니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (_editingField == 'phone') {
|
if (_editingField == 'phone') {
|
||||||
@@ -266,7 +289,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
setState(() => _isVerifying = false);
|
setState(() => _isVerifying = false);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('인증 실패: $e')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.phone.verify_failed',
|
||||||
|
fallback: '인증 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,15 +310,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
final confirmPassword = _confirmPasswordController?.text.trim() ?? '';
|
final confirmPassword = _confirmPasswordController?.text.trim() ?? '';
|
||||||
|
|
||||||
if (currentPassword.isEmpty) {
|
if (currentPassword.isEmpty) {
|
||||||
setState(() => _passwordError = '현재 비밀번호를 입력해 주세요.');
|
setState(() => _passwordError = tr(
|
||||||
|
'msg.userfront.profile.password.current_required',
|
||||||
|
fallback: '현재 비밀번호를 입력해 주세요.',
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPassword.isEmpty) {
|
if (newPassword.isEmpty) {
|
||||||
setState(() => _passwordError = '새 비밀번호를 입력해 주세요.');
|
setState(() => _passwordError = tr(
|
||||||
|
'msg.userfront.profile.password.new_required',
|
||||||
|
fallback: '새 비밀번호를 입력해 주세요.',
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPassword != confirmPassword) {
|
if (newPassword != confirmPassword) {
|
||||||
setState(() => _passwordError = '새 비밀번호가 일치하지 않습니다.');
|
setState(() => _passwordError = tr(
|
||||||
|
'msg.userfront.profile.password.mismatch',
|
||||||
|
fallback: '새 비밀번호가 일치하지 않습니다.',
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,12 +346,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
_newPasswordController?.clear();
|
_newPasswordController?.clear();
|
||||||
_confirmPasswordController?.clear();
|
_confirmPasswordController?.clear();
|
||||||
setState(() {
|
setState(() {
|
||||||
_passwordSuccess = '비밀번호가 변경되었습니다.';
|
_passwordSuccess = tr(
|
||||||
|
'msg.userfront.profile.password.changed',
|
||||||
|
fallback: '비밀번호가 변경되었습니다.',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final message = e.toString().replaceFirst('Exception: ', '');
|
final message = e.toString().replaceFirst('Exception: ', '');
|
||||||
setState(() {
|
setState(() {
|
||||||
_passwordError = '비밀번호 변경 실패: $message';
|
_passwordError = tr(
|
||||||
|
'msg.userfront.profile.password.change_failed',
|
||||||
|
fallback: '비밀번호 변경 실패: {{error}}',
|
||||||
|
params: {'error': message},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -385,26 +432,54 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
|
|
||||||
if (_editingField == 'name' && nextName.isEmpty) {
|
if (_editingField == 'name' && nextName.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('이름을 입력해주세요.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.name_required',
|
||||||
|
fallback: '이름을 입력해주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_editingField == 'department' && nextDepartment.isEmpty) {
|
if (_editingField == 'department' && nextDepartment.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('소속을 입력해주세요.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.department_required',
|
||||||
|
fallback: '소속을 입력해주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_editingField == 'phone') {
|
if (_editingField == 'phone') {
|
||||||
if (nextPhone.isEmpty) {
|
if (nextPhone.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('휴대폰 번호를 입력해주세요.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.phone_required',
|
||||||
|
fallback: '휴대폰 번호를 입력해주세요.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_isPhoneChanged && !_isPhoneVerified) {
|
if (_isPhoneChanged && !_isPhoneVerified) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('휴대폰 번호 인증이 필요합니다.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.phone_verify_required',
|
||||||
|
fallback: '휴대폰 번호 인증이 필요합니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -441,13 +516,28 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
_departmentTouched = false;
|
_departmentTouched = false;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('정보가 수정되었습니다.')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.update_success',
|
||||||
|
fallback: '정보가 수정되었습니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('수정 실패: $e')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.update_failed',
|
||||||
|
fallback: '수정 실패: {{error}}',
|
||||||
|
params: {'error': e.toString()},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -461,24 +551,32 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.home_outlined),
|
leading: const Icon(Icons.home_outlined),
|
||||||
title: const Text('대시보드'),
|
title: Text(
|
||||||
|
tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
|
||||||
|
),
|
||||||
onTap: () => context.go('/'),
|
onTap: () => context.go('/'),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person_outline),
|
leading: const Icon(Icons.person_outline),
|
||||||
title: const Text('내 정보'),
|
title: Text(
|
||||||
|
tr('ui.userfront.nav.profile', fallback: '내 정보'),
|
||||||
|
),
|
||||||
selected: true,
|
selected: true,
|
||||||
onTap: () => context.go('/profile'),
|
onTap: () => context.go('/profile'),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.qr_code_scanner),
|
leading: const Icon(Icons.qr_code_scanner),
|
||||||
title: const Text('QR 스캔'),
|
title: Text(
|
||||||
|
tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
|
||||||
|
),
|
||||||
onTap: () => context.go('/scan'),
|
onTap: () => context.go('/scan'),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout),
|
leading: const Icon(Icons.logout),
|
||||||
title: const Text('로그아웃'),
|
title: Text(
|
||||||
|
tr('ui.userfront.nav.logout', fallback: '로그아웃'),
|
||||||
|
),
|
||||||
onTap: _logout,
|
onTap: _logout,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -525,9 +623,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeaderCard(UserProfile profile) {
|
Widget _buildHeaderCard(UserProfile profile) {
|
||||||
final name = profile.name.isEmpty ? '이름 없음' : profile.name;
|
final name = profile.name.isEmpty
|
||||||
final email = profile.email.isEmpty ? '이메일 없음' : profile.email;
|
? tr('msg.userfront.profile.name_missing', fallback: '이름 없음')
|
||||||
final department = profile.department.isEmpty ? '소속 정보 없음' : profile.department;
|
: profile.name;
|
||||||
|
final email = profile.email.isEmpty
|
||||||
|
? tr('msg.userfront.profile.email_missing', fallback: '이메일 없음')
|
||||||
|
: profile.email;
|
||||||
|
final department = profile.department.isEmpty
|
||||||
|
? tr('msg.userfront.profile.department_missing', fallback: '소속 정보 없음')
|
||||||
|
: profile.department;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -538,7 +642,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
border: Border.all(color: _border),
|
border: Border.all(color: _border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.04),
|
color: Colors.black.withValues(alpha: 10),
|
||||||
blurRadius: 18,
|
blurRadius: 18,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 8),
|
||||||
),
|
),
|
||||||
@@ -556,8 +660,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'안녕하세요, $name님',
|
tr(
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: _ink),
|
'msg.userfront.profile.greeting',
|
||||||
|
fallback: '안녕하세요, {{name}}님',
|
||||||
|
params: {'name': name},
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _ink,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(email, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
Text(email, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||||
@@ -566,7 +678,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoChip(Icons.badge_outlined, '프로필 관리'),
|
_buildInfoChip(
|
||||||
|
Icons.badge_outlined,
|
||||||
|
tr('ui.userfront.profile.manage', fallback: '프로필 관리'),
|
||||||
|
),
|
||||||
_buildInfoChip(Icons.apartment, profile.tenant?.name ?? department),
|
_buildInfoChip(Icons.apartment, profile.tenant?.name ?? department),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -588,7 +703,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
border: Border.all(color: _border),
|
border: Border.all(color: _border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.03),
|
color: Colors.black.withValues(alpha: 8),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 6),
|
offset: const Offset(0, 6),
|
||||||
),
|
),
|
||||||
@@ -605,7 +720,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
title: Text(label),
|
title: Text(label),
|
||||||
subtitle: Text(displayValue),
|
subtitle: Text(displayValue),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
'읽기 전용',
|
tr('ui.common.read_only', fallback: '읽기 전용'),
|
||||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -629,7 +744,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
subtitle: Text(displayValue),
|
subtitle: Text(displayValue),
|
||||||
trailing: TextButton(
|
trailing: TextButton(
|
||||||
onPressed: isUpdating ? null : () => _startEditing(field, profile),
|
onPressed: isUpdating ? null : () => _startEditing(field, profile),
|
||||||
child: const Text('수정'),
|
child: Text(tr('ui.common.edit', fallback: '수정')),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -657,7 +772,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||||
child: const Text('취소'),
|
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -672,11 +787,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: const Text('전화번호'),
|
title: Text(
|
||||||
|
tr('ui.userfront.profile.phone.title', fallback: '전화번호'),
|
||||||
|
),
|
||||||
subtitle: Text(displayValue),
|
subtitle: Text(displayValue),
|
||||||
trailing: TextButton(
|
trailing: TextButton(
|
||||||
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
|
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
|
||||||
child: const Text('수정'),
|
child: Text(tr('ui.common.edit', fallback: '수정')),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -684,7 +801,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.w600)),
|
Text(
|
||||||
|
tr('ui.userfront.profile.phone.title', fallback: '전화번호'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
@@ -710,12 +830,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
if (_isPhoneChanged && !_isPhoneVerified)
|
if (_isPhoneChanged && !_isPhoneVerified)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isVerifying ? null : _sendCode,
|
onPressed: _isVerifying ? null : _sendCode,
|
||||||
child: Text(_isCodeSent ? '재전송' : '인증요청'),
|
child: Text(
|
||||||
|
_isCodeSent
|
||||||
|
? tr('ui.common.resend', fallback: '재전송')
|
||||||
|
: tr(
|
||||||
|
'ui.userfront.profile.phone.request_code',
|
||||||
|
fallback: '인증요청',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||||
child: const Text('취소'),
|
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -731,26 +858,32 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onSubmitted: (_) => _verifyCode(profile),
|
onSubmitted: (_) => _verifyCode(profile),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: '인증번호 6자리',
|
hintText: tr(
|
||||||
|
'ui.userfront.profile.phone.code_hint',
|
||||||
|
fallback: '인증번호 6자리',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isVerifying ? null : () => _verifyCode(profile),
|
onPressed: _isVerifying ? null : () => _verifyCode(profile),
|
||||||
child: const Text('확인'),
|
child: Text(tr('ui.common.confirm', fallback: '확인')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (_isPhoneChanged && !_isPhoneVerified)
|
if (_isPhoneChanged && !_isPhoneVerified)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
|
tr(
|
||||||
style: TextStyle(color: Colors.orange, fontSize: 12),
|
'msg.userfront.profile.phone.verify_notice',
|
||||||
|
fallback: '휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Colors.orange, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -763,20 +896,26 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'비밀번호 변경',
|
tr('ui.userfront.profile.password.title', fallback: '비밀번호 변경'),
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
Text(
|
||||||
'현재 비밀번호 확인 후 새 비밀번호로 변경합니다.',
|
tr(
|
||||||
style: TextStyle(color: Color(0xFF6B7280)),
|
'msg.userfront.profile.password.subtitle',
|
||||||
|
fallback: '현재 비밀번호 확인 후 새 비밀번호로 변경합니다.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _currentPasswordController,
|
controller: _currentPasswordController,
|
||||||
obscureText: !_showCurrentPassword,
|
obscureText: !_showCurrentPassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '현재 비밀번호',
|
labelText: tr(
|
||||||
|
'ui.userfront.profile.password.current',
|
||||||
|
fallback: '현재 비밀번호',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(_showCurrentPassword ? Icons.visibility_off : Icons.visibility),
|
icon: Icon(_showCurrentPassword ? Icons.visibility_off : Icons.visibility),
|
||||||
@@ -791,7 +930,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
controller: _newPasswordController,
|
controller: _newPasswordController,
|
||||||
obscureText: !_showNewPassword,
|
obscureText: !_showNewPassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '새 비밀번호',
|
labelText: tr(
|
||||||
|
'ui.userfront.profile.password.new',
|
||||||
|
fallback: '새 비밀번호',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(_showNewPassword ? Icons.visibility_off : Icons.visibility),
|
icon: Icon(_showNewPassword ? Icons.visibility_off : Icons.visibility),
|
||||||
@@ -806,7 +948,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
controller: _confirmPasswordController,
|
controller: _confirmPasswordController,
|
||||||
obscureText: !_showConfirmPassword,
|
obscureText: !_showConfirmPassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '새 비밀번호 확인',
|
labelText: tr(
|
||||||
|
'ui.userfront.profile.password.confirm',
|
||||||
|
fallback: '새 비밀번호 확인',
|
||||||
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(_showConfirmPassword ? Icons.visibility_off : Icons.visibility),
|
icon: Icon(_showConfirmPassword ? Icons.visibility_off : Icons.visibility),
|
||||||
@@ -841,12 +986,22 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('비밀번호 변경'),
|
: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.profile.password.change',
|
||||||
|
fallback: '비밀번호 변경',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.go('/recovery'),
|
onPressed: () => context.go('/recovery'),
|
||||||
child: const Text('비밀번호를 잊으셨나요?'),
|
child: Text(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.profile.password.forgot',
|
||||||
|
fallback: '비밀번호를 잊으셨나요?',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -869,55 +1024,88 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildHeaderCard(profile),
|
_buildHeaderCard(profile),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle('기본 정보', '계정 기본 정보를 관리합니다.'),
|
_buildSectionTitle(
|
||||||
|
tr('ui.userfront.profile.section.basic', fallback: '기본 정보'),
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.section.basic',
|
||||||
|
fallback: '계정 기본 정보를 관리합니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildCard(
|
_buildCard(
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_buildEditableTile(
|
_buildEditableTile(
|
||||||
field: 'name',
|
field: 'name',
|
||||||
label: '이름',
|
label: tr('ui.userfront.profile.field.name', fallback: '이름'),
|
||||||
value: profile.name,
|
value: profile.name,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
controller: _nameController!,
|
controller: _nameController!,
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile('이메일', profile.email),
|
_buildReadOnlyTile(
|
||||||
|
tr('ui.userfront.profile.field.email', fallback: '이메일'),
|
||||||
|
profile.email,
|
||||||
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildPhoneEditor(profile, isUpdating),
|
_buildPhoneEditor(profile, isUpdating),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle('조직 정보', '소속 및 구분 정보입니다.'),
|
_buildSectionTitle(
|
||||||
|
tr('ui.userfront.profile.section.organization', fallback: '조직 정보'),
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.section.organization',
|
||||||
|
fallback: '소속 및 구분 정보입니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildCard(
|
_buildCard(
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_buildEditableTile(
|
_buildEditableTile(
|
||||||
field: 'department',
|
field: 'department',
|
||||||
label: '소속',
|
label: tr('ui.userfront.profile.field.department', fallback: '소속'),
|
||||||
value: profile.department,
|
value: profile.department,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
controller: _departmentController!,
|
controller: _departmentController!,
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile('구분', profile.affiliationType),
|
_buildReadOnlyTile(
|
||||||
|
tr('ui.userfront.profile.field.affiliation', fallback: '구분'),
|
||||||
|
profile.affiliationType,
|
||||||
|
),
|
||||||
if (profile.tenant != null) ...[
|
if (profile.tenant != null) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile('소속 테넌트', profile.tenant!.name),
|
_buildReadOnlyTile(
|
||||||
|
tr(
|
||||||
|
'ui.userfront.profile.field.tenant',
|
||||||
|
fallback: '소속 테넌트',
|
||||||
|
),
|
||||||
|
profile.tenant!.name,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
if (profile.companyCode.isNotEmpty) ...[
|
if (profile.companyCode.isNotEmpty) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile('회사코드', profile.companyCode),
|
_buildReadOnlyTile(
|
||||||
|
tr('ui.userfront.profile.field.company_code', fallback: '회사코드'),
|
||||||
|
profile.companyCode,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle('보안', '비밀번호를 안전하게 관리합니다.'),
|
_buildSectionTitle(
|
||||||
|
tr('ui.userfront.profile.section.security', fallback: '보안'),
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.section.security',
|
||||||
|
fallback: '비밀번호를 안전하게 관리합니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildPasswordSection(),
|
_buildPasswordSection(),
|
||||||
if (isUpdating || _isVerifying) ...[
|
if (isUpdating || _isVerifying) ...[
|
||||||
@@ -943,18 +1131,25 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
final profile = profileState.value ?? _cachedProfile;
|
final profile = profileState.value ?? _cachedProfile;
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('내 정보')),
|
appBar: AppBar(
|
||||||
|
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||||
|
),
|
||||||
body: profileState.isLoading
|
body: profileState.isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Center(
|
: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text('정보를 불러올 수 없습니다.'),
|
Text(
|
||||||
|
tr(
|
||||||
|
'msg.userfront.profile.load_failed',
|
||||||
|
fallback: '정보를 불러올 수 없습니다.',
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => ref.read(profileProvider.notifier).loadProfile(),
|
onPressed: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||||
child: const Text('재시도'),
|
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -971,8 +1166,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
backgroundColor: _subtle,
|
backgroundColor: _subtle,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
'Baron 로그인',
|
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: _surface,
|
backgroundColor: _surface,
|
||||||
@@ -980,17 +1175,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.home_outlined),
|
icon: const Icon(Icons.home_outlined),
|
||||||
tooltip: '대시보드',
|
tooltip: tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
|
||||||
onPressed: () => context.go('/'),
|
onPressed: () => context.go('/'),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.qr_code_scanner),
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
tooltip: 'QR 스캔',
|
tooltip: tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
|
||||||
onPressed: () => context.push('/scan'),
|
onPressed: () => context.push('/scan'),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.logout),
|
icon: const Icon(Icons.logout),
|
||||||
tooltip: '로그아웃',
|
tooltip: tr('ui.userfront.nav.logout', fallback: '로그아웃'),
|
||||||
onPressed: _logout,
|
onPressed: _logout,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
40
userfront/lib/i18n.dart
Normal file
40
userfront/lib/i18n.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
1612
userfront/lib/i18n_data.dart
Normal file
1612
userfront/lib/i18n_data.dart
Normal file
File diff suppressed because one or more lines are too long
@@ -21,6 +21,7 @@ import 'core/services/logger_service.dart';
|
|||||||
import 'core/notifiers/auth_notifier.dart';
|
import 'core/notifiers/auth_notifier.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'features/auth/presentation/consent_screen.dart';
|
import 'features/auth/presentation/consent_screen.dart';
|
||||||
|
import 'i18n.dart';
|
||||||
|
|
||||||
final _log = Logger('Main');
|
final _log = Logger('Main');
|
||||||
|
|
||||||
@@ -200,9 +201,12 @@ final _router = GoRouter(
|
|||||||
path: '/settings',
|
path: '/settings',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
_routerLogger.info("Navigating to /settings (disabled)");
|
_routerLogger.info("Navigating to /settings (disabled)");
|
||||||
return const ErrorScreen(
|
return ErrorScreen(
|
||||||
errorCode: 'settings_disabled',
|
errorCode: 'settings_disabled',
|
||||||
description: '현재 계정 설정 화면은 준비 중입니다.',
|
description: tr(
|
||||||
|
'msg.userfront.settings.disabled',
|
||||||
|
fallback: '현재 계정 설정 화면은 준비 중입니다.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -295,7 +299,7 @@ class BaronSSOApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'Baron 로그인',
|
title: tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base
|
seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ packages:
|
|||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_web_plugins:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:userfront/features/dashboard/domain/dashboard_providers.dart';
|
import 'package:userfront/features/dashboard/domain/dashboard_providers.dart';
|
||||||
import 'package:userfront/features/dashboard/domain/models.dart';
|
import 'package:userfront/features/dashboard/domain/models.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
AuditLogEntry _log(String id) {
|
AuditLogEntry _log(String id) {
|
||||||
return AuditLogEntry.fromJson({
|
return AuditLogEntry.fromJson({
|
||||||
@@ -21,6 +24,14 @@ Future<void> _drainMicrotasks() async {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final dispatcher = TestWidgetsFlutterBinding.instance.platformDispatcher;
|
||||||
|
dispatcher.localeTestValue = const Locale('ko');
|
||||||
|
dispatcher.localesTestValue = const [Locale('ko')];
|
||||||
|
|
||||||
|
tearDownAll(() {
|
||||||
|
dispatcher.clearLocaleTestValue();
|
||||||
|
dispatcher.clearLocalesTestValue();
|
||||||
|
});
|
||||||
|
|
||||||
test('AuthTimelineNotifier는 초기 페이지를 로드한다', () async {
|
test('AuthTimelineNotifier는 초기 페이지를 로드한다', () async {
|
||||||
final cursors = <String?>[];
|
final cursors = <String?>[];
|
||||||
@@ -103,7 +114,13 @@ void main() {
|
|||||||
|
|
||||||
final state = container.read(authTimelineProvider);
|
final state = container.read(authTimelineProvider);
|
||||||
expect(state.items.isEmpty, true);
|
expect(state.items.isEmpty, true);
|
||||||
expect(state.error, '접속이력을 불러오지 못했습니다.');
|
expect(
|
||||||
|
state.error,
|
||||||
|
tr(
|
||||||
|
'msg.userfront.dashboard.timeline.load_error',
|
||||||
|
fallback: '접속이력을 불러오지 못했습니다.',
|
||||||
|
),
|
||||||
|
);
|
||||||
container.dispose();
|
container.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:userfront/core/constants/error_whitelist.dart';
|
||||||
import 'package:userfront/features/auth/presentation/error_screen.dart';
|
import 'package:userfront/features/auth/presentation/error_screen.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
Future<void> _pumpErrorScreen(
|
Future<void> _pumpErrorScreen(
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
@@ -21,6 +23,19 @@ Future<void> _pumpErrorScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
setUpAll(() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final dispatcher = TestWidgetsFlutterBinding.instance.platformDispatcher;
|
||||||
|
dispatcher.localeTestValue = const Locale('ko');
|
||||||
|
dispatcher.localesTestValue = const [Locale('ko')];
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() {
|
||||||
|
final dispatcher = TestWidgetsFlutterBinding.instance.platformDispatcher;
|
||||||
|
dispatcher.clearLocaleTestValue();
|
||||||
|
dispatcher.clearLocalesTestValue();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('개발환경은 원문 메시지를 노출한다', (WidgetTester tester) async {
|
testWidgets('개발환경은 원문 메시지를 노출한다', (WidgetTester tester) async {
|
||||||
await _pumpErrorScreen(
|
await _pumpErrorScreen(
|
||||||
tester,
|
tester,
|
||||||
@@ -29,9 +44,20 @@ void main() {
|
|||||||
isProdOverride: false,
|
isProdOverride: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('오류: custom_error'), findsOneWidget);
|
final title = tr(
|
||||||
|
'msg.userfront.error.title_with_code',
|
||||||
|
fallback: '오류: {{code}}',
|
||||||
|
params: {'code': 'custom_error'},
|
||||||
|
);
|
||||||
|
final type = tr(
|
||||||
|
'msg.userfront.error.type',
|
||||||
|
fallback: '오류 종류: {{type}}',
|
||||||
|
params: {'type': 'custom_error'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(title), findsOneWidget);
|
||||||
expect(find.text('원문 메시지'), findsOneWidget);
|
expect(find.text('원문 메시지'), findsOneWidget);
|
||||||
expect(find.text('오류 종류: custom_error'), findsOneWidget);
|
expect(find.text(type), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('프로덕션은 whitelist 메시지를 노출한다', (WidgetTester tester) async {
|
testWidgets('프로덕션은 whitelist 메시지를 노출한다', (WidgetTester tester) async {
|
||||||
@@ -42,10 +68,24 @@ void main() {
|
|||||||
isProdOverride: true,
|
isProdOverride: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('인증 과정에서 오류가 발생했습니다'), findsOneWidget);
|
final title = tr(
|
||||||
expect(find.text('현재 계정 설정 화면은 준비 중입니다.'), findsOneWidget);
|
'msg.userfront.error.title',
|
||||||
|
fallback: '인증 과정에서 오류가 발생했습니다',
|
||||||
|
);
|
||||||
|
final detail = tr(
|
||||||
|
'msg.userfront.error.whitelist.settings_disabled',
|
||||||
|
fallback: errorWhitelistMessages['settings_disabled']!,
|
||||||
|
);
|
||||||
|
final type = tr(
|
||||||
|
'msg.userfront.error.type',
|
||||||
|
fallback: '오류 종류: {{type}}',
|
||||||
|
params: {'type': 'settings_disabled'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(title), findsOneWidget);
|
||||||
|
expect(find.text(detail), findsOneWidget);
|
||||||
expect(find.text('원문 메시지'), findsNothing);
|
expect(find.text('원문 메시지'), findsNothing);
|
||||||
expect(find.text('오류 종류: settings_disabled'), findsOneWidget);
|
expect(find.text(type), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('프로덕션은 비허용 에러를 unknown_error로 처리한다', (WidgetTester tester) async {
|
testWidgets('프로덕션은 비허용 에러를 unknown_error로 처리한다', (WidgetTester tester) async {
|
||||||
@@ -56,9 +96,23 @@ void main() {
|
|||||||
isProdOverride: true,
|
isProdOverride: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('인증 과정에서 오류가 발생했습니다'), findsOneWidget);
|
final title = tr(
|
||||||
expect(find.text('에러가 계속되면 관리자에게 문의해주세요'), findsOneWidget);
|
'msg.userfront.error.title',
|
||||||
|
fallback: '인증 과정에서 오류가 발생했습니다',
|
||||||
|
);
|
||||||
|
final detail = tr(
|
||||||
|
'msg.userfront.error.detail_contact',
|
||||||
|
fallback: '에러가 계속되면 관리자에게 문의해주세요',
|
||||||
|
);
|
||||||
|
final type = tr(
|
||||||
|
'msg.userfront.error.type',
|
||||||
|
fallback: '오류 종류: {{type}}',
|
||||||
|
params: {'type': 'unknown_error'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(title), findsOneWidget);
|
||||||
|
expect(find.text(detail), findsOneWidget);
|
||||||
expect(find.text('원문 메시지'), findsNothing);
|
expect(find.text('원문 메시지'), findsNothing);
|
||||||
expect(find.text('오류 종류: unknown_error'), findsOneWidget);
|
expect(find.text(type), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user