백엔드에 새로 생긴 /api/shop/* 엔드포인트를 붙여서 프로필 커스텀 화면을 실제 상점으로 채웠다. 랭킹/친구창 배경, 출석/그룹창 표시 카테고리는 와이어프레임 자리만 잡아둔 준비중 상태 - 1단계는 기본 프로필 테마만 동작. 장착한 테마는 메인 대시보드 프로필 카드 배경에 바로 반영된다. 학생이 직접 템플릿을 만들어 파는 기능은 이후 단계로 보류.
151 lines
4.6 KiB
Dart
151 lines
4.6 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';
|
|
|
|
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<(Color? primary, Color? secondary)> fetchEquippedColors(
|
|
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 (
|
|
_parseColor(data['primaryColor'] as String),
|
|
_parseColor(data['secondaryColor'] as String),
|
|
);
|
|
}
|
|
}
|
|
} catch (_) {
|
|
// 실패하면 그냥 기본 모습으로 보여준다.
|
|
}
|
|
return (null, null);
|
|
}
|
|
}
|