첫 커밋: 로컬 프로젝트 업로드

This commit is contained in:
2026-06-10 15:51:34 +09:00
commit 6a8dbeb2e9
1211 changed files with 312864 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
class Tenant {
final String id;
final String name;
final String slug;
final String description;
Tenant({
required this.id,
required this.name,
required this.slug,
required this.description,
});
factory Tenant.fromJson(Map<String, dynamic> json) {
return Tenant(
id: json['id'] ?? '',
name: json['name'] ?? '',
slug: json['slug'] ?? '',
description: json['description'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'name': name, 'slug': slug, 'description': description};
}
}
class UserProfile {
final String id;
final String email;
final String name;
final String phone;
final String department;
final String affiliationType;
final String companyCode;
final String? sessionAuthenticatedAt;
final Map<String, dynamic>? metadata;
final Tenant? tenant;
UserProfile({
required this.id,
required this.email,
required this.name,
required this.phone,
required this.department,
required this.affiliationType,
required this.companyCode,
this.sessionAuthenticatedAt,
this.metadata,
this.tenant,
});
factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
id: json['id'] ?? '',
email: json['email'] ?? '',
name: json['name'] ?? '',
phone: json['phone'] ?? '',
department: json['department'] ?? '',
affiliationType: json['affiliationType'] ?? '',
companyCode: json['companyCode'] ?? '',
sessionAuthenticatedAt: json['sessionAuthenticatedAt'] as String?,
metadata: json['metadata'] != null
? Map<String, dynamic>.from(json['metadata'])
: null,
tenant: json['tenant'] != null ? Tenant.fromJson(json['tenant']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'name': name,
'phone': phone,
'department': department,
'affiliationType': affiliationType,
'companyCode': companyCode,
'sessionAuthenticatedAt': sessionAuthenticatedAt,
'metadata': metadata,
'tenant': tenant?.toJson(),
};
}
UserProfile copyWith({String? name, String? phone, String? department}) {
return UserProfile(
id: id,
email: email,
name: name ?? this.name,
phone: phone ?? this.phone,
department: department ?? this.department,
affiliationType: affiliationType,
companyCode: companyCode,
sessionAuthenticatedAt: sessionAuthenticatedAt,
tenant: tenant,
);
}
}

View File

@@ -0,0 +1,177 @@
import 'dart:convert';
import 'package:userfront/i18n.dart';
import '../models/user_profile_model.dart';
import '../../../../core/services/auth_token_store.dart';
import '../../../../core/services/http_client.dart';
import '../../../../core/services/runtime_env.dart';
class ProfileRepository {
static String get _baseUrl => runtimeBackendUrl();
// Helper to get session token
static Future<String?> _getToken() async {
return AuthTokenStore.getToken();
}
Future<UserProfile> getMyProfile() async {
final token = await _getToken();
final useCookie = AuthTokenStore.usesCookie();
if (token == null && !useCookie) {
throw Exception(tr('err.userfront.session.missing'));
}
final url = Uri.parse('$_baseUrl/api/v1/user/me');
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.get(url, headers: headers);
client.close();
if (response.statusCode == 200) {
return UserProfile.fromJson(jsonDecode(response.body));
} else {
throw Exception(
tr(
'err.userfront.profile.load_failed',
params: {'error': response.body},
),
);
}
}
Future<void> updateMyProfile({
required String name,
required String phone,
required String department,
}) async {
final token = await _getToken();
final useCookie = AuthTokenStore.usesCookie();
if (token == null && !useCookie) {
throw Exception(tr('err.userfront.session.missing'));
}
final url = Uri.parse('$_baseUrl/api/v1/user/me');
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.put(
url,
headers: headers,
body: jsonEncode({
'name': name,
'phone': phone,
'department': department,
}),
);
client.close();
if (response.statusCode != 200) {
throw Exception(
tr(
'err.userfront.profile.update_failed',
params: {'error': response.body},
),
);
}
}
Future<void> sendUpdateCode(String phone) async {
final token = await _getToken();
final useCookie = AuthTokenStore.usesCookie();
if (token == null && !useCookie) {
throw Exception(tr('err.userfront.session.missing'));
}
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.post(
url,
headers: headers,
body: jsonEncode({'phone': phone}),
);
client.close();
if (response.statusCode != 200) {
throw Exception(
tr(
'err.userfront.profile.send_code_failed',
params: {'error': response.body},
),
);
}
}
Future<void> changePassword({
required String currentPassword,
required String newPassword,
}) async {
final token = await _getToken();
final useCookie = AuthTokenStore.usesCookie();
if (token == null && !useCookie) {
throw Exception(tr('err.userfront.session.missing'));
}
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.post(
url,
headers: headers,
body: jsonEncode({
'currentPassword': currentPassword,
'newPassword': newPassword,
}),
);
client.close();
if (response.statusCode != 200) {
throw Exception(
tr(
'err.userfront.profile.password_change_failed',
params: {'error': response.body},
),
);
}
}
Future<void> verifyUpdateCode(String phone, String code) async {
final token = await _getToken();
final useCookie = AuthTokenStore.usesCookie();
if (token == null && !useCookie) {
throw Exception(tr('err.userfront.session.missing'));
}
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.post(
url,
headers: headers,
body: jsonEncode({'phone': phone, 'code': code}),
);
client.close();
if (response.statusCode != 200) {
throw Exception(
tr(
'err.userfront.profile.verify_code_failed',
params: {'error': response.body},
),
);
}
}
}

View File

@@ -0,0 +1,51 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/models/user_profile_model.dart';
import '../../data/repositories/profile_repository.dart';
// 1. Repository Provider
final profileRepositoryProvider = Provider((ref) => ProfileRepository());
// 2. AsyncNotifier implementation (Modern Riverpod)
class ProfileNotifier extends AsyncNotifier<UserProfile?> {
@override
FutureOr<UserProfile?> build() async {
// Initial data fetch
return _fetch();
}
Future<UserProfile?> _fetch() async {
return ref.read(profileRepositoryProvider).getMyProfile();
}
Future<UserProfile?> loadProfile() async {
state = const AsyncValue.loading();
final profile = await _fetch();
state = AsyncValue.data(profile);
return profile;
}
Future<void> updateProfile({
required String name,
required String phone,
required String department,
}) async {
// Show loading state
state = const AsyncValue.loading();
// Perform update and then re-fetch profile
state = await AsyncValue.guard(() async {
await ref
.read(profileRepositoryProvider)
.updateMyProfile(name: name, phone: phone, department: department);
return _fetch();
});
}
}
// 3. Provider definition
final profileProvider = AsyncNotifierProvider<ProfileNotifier, UserProfile?>(
() {
return ProfileNotifier();
},
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class ProfileInfoRow extends StatelessWidget {
final String label;
final String value;
const ProfileInfoRow({super.key, required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
),
Expanded(
child: Text(
value.isEmpty ? '-' : value,
style: const TextStyle(fontSize: 16),
),
),
],
),
);
}
}