// ๐ŸŽจ ํ”„๋กœํ•„ ์ปค์Šคํ…€ ํ™”๋ฉด (UI ์ „์šฉ). ๊ณต๋ถ€์‹œ๊ฐ„์œผ๋กœ ์ ๋ฆฝํ•œ ์บ์‹œ๋กœ ํ”„๋กœํ•„ ์นด๋“œ ํ…Œ๋งˆ๋ฅผ // ๊ตฌ๋งค/์žฅ์ฐฉํ•˜๋Š” ์ƒ์ . ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/profile_shop_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/profile_shop_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class ProfileCustomizeScreen extends StatefulWidget { final String studentId; final String studentName; const ProfileCustomizeScreen({ super.key, required this.studentId, required this.studentName, }); @override State createState() => _ProfileCustomizeScreenState(); } class _ProfileCustomizeScreenState extends State { final ProfileShopController _controller = ProfileShopController(); // ๐Ÿšง ์ฒซ ๋ฒ„์ „์€ "๊ธฐ๋ณธ ํ”„๋กœํ•„ ๊พธ๋ฏธ๊ธฐ"๋งŒ ์‹ค์ œ๋กœ ๋™์ž‘ํ•œ๋‹ค. ๋‚˜๋จธ์ง€ ๋‘ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” // ์™€์ด์–ดํ”„๋ ˆ์ž„ ์ž๋ฆฌ๋งŒ ์žก์•„๋‘” ์ค€๋น„์ค‘ ์ƒํƒœ. String _selectedCategory = 'basic_profile'; @override void initState() { super.initState(); _controller.loadCatalog(widget.studentId); } @override void dispose() { _controller.dispose(); super.dispose(); } Future _handlePurchase(ShopTemplate template) async { final (success, message) = await _controller.purchase( widget.studentId, template.id, ); if (!mounted) return; AppNotice.show( context, message, color: success ? AppPalette.success : AppPalette.danger, ); } Future _handleEquip(int? templateId) async { final (success, message) = await _controller.equip( widget.studentId, templateId, ); if (!mounted) return; AppNotice.show( context, message, color: success ? AppPalette.approved : AppPalette.danger, ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, centerTitle: true, title: const TitlePill('ํ”„๋กœํ•„ ๊พธ๋ฏธ๊ธฐ'), ), body: ListenableBuilder( listenable: _controller, builder: (context, _) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: RefreshIndicator( onRefresh: () => _controller.loadCatalog(widget.studentId), child: ListView( padding: const EdgeInsets.all(20), children: [ _buildCashBalance(), const SizedBox(height: 20), _buildCategoryTiles(), const SizedBox(height: 24), if (_controller.isLoading) const Padding( padding: EdgeInsets.symmetric(vertical: 40), child: Center(child: CircularProgressIndicator()), ) else _buildTemplateGrid(), ], ), ), ), ); }, ), ); } Widget _buildCashBalance() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( color: AppPalette.ink, borderRadius: BorderRadius.circular(18), ), child: Row( children: [ const Icon(Icons.savings_rounded, color: Colors.white, size: 22), const SizedBox(width: 10), const Text( '๋ณด์œ  ์บ์‹œ', style: TextStyle(color: Colors.white70, fontSize: 13), ), const Spacer(), Text( '${_controller.cashBalance} ์บ์‹œ', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ); } Widget _buildCategoryTiles() { // ๐Ÿ“ ListView(์Šคํฌ๋กค ์˜์—ญ) ์•ˆ์—์„œ๋Š” ๋†’์ด๊ฐ€ ์ •ํ•ด์ ธ ์žˆ์ง€ ์•Š์•„ Row์— // stretch ์ •๋ ฌ๋งŒ ์ฃผ๋ฉด ์ž์‹ ํฌ๊ธฐ ๊ณ„์‚ฐ์ด ๊นจ์ง„๋‹ค. SizedBox๋กœ ๋†’์ด๋ฅผ ๋ชป๋ฐ•์•„๋‘”๋‹ค. return SizedBox( height: 210, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 3, child: _CategoryTile( title: '๊ธฐ๋ณธ ํ”„๋กœํ•„ ๊พธ๋ฏธ๊ธฐ', icon: Icons.person_rounded, selected: _selectedCategory == 'basic_profile', enabled: true, onTap: () => setState(() => _selectedCategory = 'basic_profile'), ), ), const SizedBox(width: 12), Expanded( flex: 4, child: Column( children: [ Expanded( child: _CategoryTile( title: '๋žญํ‚น ยท ์นœ๊ตฌ์ฐฝ ๋ฐฐ๊ฒฝ', subtitle: '์ค€๋น„์ค‘', icon: Icons.leaderboard_rounded, selected: false, enabled: false, onTap: null, ), ), const SizedBox(height: 12), Expanded( child: _CategoryTile( title: '์ถœ์„ ยท ๊ทธ๋ฃน์ฐฝ ํ‘œ์‹œ', subtitle: '์ค€๋น„์ค‘', icon: Icons.groups_rounded, selected: false, enabled: false, onTap: null, ), ), ], ), ), ], ), ); } Widget _buildTemplateGrid() { final templates = _controller.templates .where((t) => t.category == _selectedCategory) .toList(); final hasEquippedTemplate = templates.any((t) => t.equipped); return GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.15, children: [ _DefaultTemplateTile( equipped: !hasEquippedTemplate, onEquip: () => _handleEquip(null), ), for (final template in templates) _TemplateTile( template: template, canAfford: _controller.cashBalance >= template.price, onPurchase: () => _handlePurchase(template), onEquip: () => _handleEquip(template.id), ), ], ); } } class _CategoryTile extends StatelessWidget { final String title; final String? subtitle; final IconData icon; final bool selected; final bool enabled; final VoidCallback? onTap; const _CategoryTile({ required this.title, this.subtitle, required this.icon, required this.selected, required this.enabled, required this.onTap, }); @override Widget build(BuildContext context) { return Material( color: selected ? AppPalette.ink : (enabled ? AppPalette.paper : AppPalette.linen), borderRadius: BorderRadius.circular(20), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all( color: selected ? AppPalette.ink : AppPalette.sage, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 22, color: selected ? Colors.white : (enabled ? AppPalette.ink : Colors.grey), ), const SizedBox(height: 8), Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 13, color: selected ? Colors.white : (enabled ? AppPalette.ink : Colors.grey[600]), ), ), if (subtitle != null) ...[ const SizedBox(height: 2), Text( subtitle!, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 10, color: Colors.grey[500]), ), ], ], ), ), ), ); } } class _DefaultTemplateTile extends StatelessWidget { final bool equipped; final VoidCallback onEquip; const _DefaultTemplateTile({required this.equipped, required this.onEquip}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(18), border: Border.all( color: equipped ? AppPalette.approved : AppPalette.sage, width: equipped ? 2 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( width: double.infinity, decoration: BoxDecoration( color: AppPalette.ink, borderRadius: BorderRadius.circular(12), ), ), ), const SizedBox(height: 10), const Text( '๊ธฐ๋ณธ ๋ชจ์Šต', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: equipped ? OutlinedButton( onPressed: null, style: OutlinedButton.styleFrom( foregroundColor: AppPalette.approved, side: const BorderSide(color: AppPalette.approved), ), child: const Text('์žฅ์ฐฉ์ค‘'), ) : OutlinedButton(onPressed: onEquip, child: const Text('์žฅ์ฐฉํ•˜๊ธฐ')), ), ], ), ); } } class _TemplateTile extends StatelessWidget { final ShopTemplate template; final bool canAfford; final VoidCallback onPurchase; final VoidCallback onEquip; const _TemplateTile({ required this.template, required this.canAfford, required this.onPurchase, required this.onEquip, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(18), border: Border.all( color: template.equipped ? AppPalette.approved : AppPalette.sage, width: template.equipped ? 2 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [template.primaryColor, template.secondaryColor], ), borderRadius: BorderRadius.circular(12), ), ), ), const SizedBox(height: 10), Text( template.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), ), const SizedBox(height: 2), Text( '${template.price} ์บ์‹œ', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: template.equipped ? OutlinedButton( onPressed: null, style: OutlinedButton.styleFrom( foregroundColor: AppPalette.approved, side: const BorderSide(color: AppPalette.approved), ), child: const Text('์žฅ์ฐฉ์ค‘'), ) : template.owned ? OutlinedButton(onPressed: onEquip, child: const Text('์žฅ์ฐฉํ•˜๊ธฐ')) : ElevatedButton( onPressed: canAfford ? onPurchase : null, style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: Colors.white, disabledBackgroundColor: AppPalette.sage, ), child: const Text('๊ตฌ๋งคํ•˜๊ธฐ'), ), ), ], ), ); } }