Files
school-attendance/lib/function/profile_shop_controller.dart
T
sihoo 0fc80eb5c5 템플릿마다 메인 대시보드 전체 테마(배경/타일/아이콘 색)가 같이 바뀌게 추가
용어 정리: 템플릿 = 상점에서 사는 색/애니메이션 상품, 테마 = 그 템플릿이
몰고 오는 화면 전체 디자인(배경색/타일색/아이콘색). 1단계로 메인 대시보드부터
적용 - 프로필 카드 그라데이션뿐 아니라 페이지 배경, 타일 카드 배경/테두리,
아이콘 동그라미 배경, 타일 아이콘 강조색, 제목/부제목 텍스트 색까지 장착한
템플릿에 맞춰 통째로 바뀐다. 잠긴/비활성 타일(NFC 미인증 등)은 기존처럼
중립 회색(sage)을 그대로 써서 상태 구분은 유지.

6개 기존 템플릿마다 직접 디자인한 테마(lib/theme/profile_themes.dart)를
새로 만들었고, 최고가 "갤럭시" 템플릿은 다른 5개와 달리 통째로 다크 테마로
디자인해 프리미엄 아이템 느낌을 살렸다.

백엔드 /api/shop/equipped가 템플릿 code를 같이 내려주도록 수정해서
프론트엔드가 code로 디자인해둔 테마를 찾아 쓸 수 있게 했다.
2026-10-01 06:13:18 +00:00

147 lines
4.5 KiB
Dart

// 🛍️ 프로필 꾸미기 상점 기능(서버 통신/상태) 담당 컨트롤러.
// UI는 lib/ui/profile_customize_screen.dart가 담당한다.
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Color;
import 'package:http/http.dart' as http;
import '../config.dart';
import '../theme/profile_themes.dart';
class ShopTemplate {
final int id;
final String category;
final String name;
final int price;
final Color primaryColor;
final Color secondaryColor;
final bool owned;
final bool equipped;
ShopTemplate({
required this.id,
required this.category,
required this.name,
required this.price,
required this.primaryColor,
required this.secondaryColor,
required this.owned,
required this.equipped,
});
factory ShopTemplate.fromJson(Map<String, dynamic> json) {
return ShopTemplate(
id: json['id'] as int,
category: json['category'] as String,
name: json['name'] as String,
price: json['price'] as int,
primaryColor: _parseColor(json['primaryColor'] as String),
secondaryColor: _parseColor(json['secondaryColor'] as String),
owned: json['owned'] == true,
equipped: json['equipped'] == true,
);
}
}
Color _parseColor(String hex) {
return Color(int.parse(hex.substring(2), radix: 16));
}
class ProfileShopController extends ChangeNotifier {
bool _isLoading = false;
bool get isLoading => _isLoading;
int cashBalance = 0;
List<ShopTemplate> templates = [];
void _safeNotify() {
if (hasListeners) notifyListeners();
}
Future<void> loadCatalog(String studentId) async {
_isLoading = true;
_safeNotify();
try {
final url = Uri.parse(
'$baseUrl/api/shop/catalog?studentId=${Uri.encodeComponent(studentId)}',
);
final response = await http.get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
cashBalance = (data['cashBalance'] as num?)?.toInt() ?? 0;
templates = ((data['templates'] as List?) ?? [])
.map((t) => ShopTemplate.fromJson(t as Map<String, dynamic>))
.toList();
}
} catch (_) {
// 네트워크 에러여도 화면은 빈 상태로라도 그려지게 조용히 넘어간다.
} finally {
_isLoading = false;
_safeNotify();
}
}
Future<(bool success, String message)> purchase(
String studentId,
int templateId,
) async {
try {
final url = Uri.parse('$baseUrl/api/shop/purchase');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'studentId': studentId, 'templateId': templateId}),
);
final data = jsonDecode(response.body);
final success = data['status'] == 'success';
if (success) {
await loadCatalog(studentId);
}
return (success, '${data['message'] ?? (success ? '구매 완료!' : '구매 실패')}');
} catch (e) {
return (false, '서버와 연결할 수 없습니다. (네트워크 에러)');
}
}
Future<(bool success, String message)> equip(
String studentId,
int? templateId,
) async {
try {
final url = Uri.parse('$baseUrl/api/shop/equip');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'studentId': studentId, 'templateId': templateId}),
);
final data = jsonDecode(response.body);
final success = data['status'] == 'success';
if (success) {
await loadCatalog(studentId);
}
return (success, '${data['message'] ?? (success ? '장착 완료' : '장착 실패')}');
} catch (e) {
return (false, '서버와 연결할 수 없습니다. (네트워크 에러)');
}
}
/// 🏠 메인 대시보드 프로필 카드 렌더링 전용 - 상점 화면을 열지 않고도
/// 현재 장착 중인 테마 색상만 가볍게 조회한다.
static Future<ProfileTheme?> fetchEquippedTheme(String studentId) async {
try {
final url = Uri.parse(
'$baseUrl/api/shop/equipped?studentId=${Uri.encodeComponent(studentId)}',
);
final response = await http.get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['equipped'] == true) {
return profileThemes[data['code'] as String?];
}
}
} catch (_) {
// 실패하면 그냥 기본 모습으로 보여준다.
}
return null;
}
}