// ๐Ÿ’ฌ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œํŒ - ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ + ๋Œ“๊ธ€ ํ™”๋ฉด (UI ์ „์šฉ). // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/board_post_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/board_post_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class BoardPostDetailScreen extends StatefulWidget { final int postId; final String studentId; const BoardPostDetailScreen({ super.key, required this.postId, required this.studentId, }); @override State createState() => _BoardPostDetailScreenState(); } class _BoardPostDetailScreenState extends State { late final BoardPostController _controller; final _commentController = TextEditingController(); bool _changed = false; @override void initState() { super.initState(); _controller = BoardPostController( postId: widget.postId, studentId: widget.studentId, ); _controller.init(); } @override void dispose() { _controller.dispose(); _commentController.dispose(); super.dispose(); } Future _submitComment() async { final text = _commentController.text; final (success, message) = await _controller.addComment(text); if (!mounted) return; if (success) { _commentController.clear(); _changed = true; } AppNotice.show( context, message, icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded, ); } Future _showReportSheet({ required String targetType, required int targetId, }) async { final reasonController = TextEditingController(); final reason = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: AppPalette.paper, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (sheetContext) => Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( '์‹ ๊ณ  ์‚ฌ์œ  (์„ ํƒ)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), const SizedBox(height: 10), TextField( controller: reasonController, maxLines: 3, decoration: InputDecoration( hintText: '์–ด๋–ค ์ ์ด ๋ฌธ์ œ์ธ์ง€ ์•Œ๋ ค์ฃผ์„ธ์š”.', filled: true, fillColor: AppPalette.mist, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), const SizedBox(height: 14), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, padding: const EdgeInsets.symmetric(vertical: 14), ), onPressed: () => Navigator.of(sheetContext).pop(reasonController.text), child: const Text('์‹ ๊ณ ํ•˜๊ธฐ'), ), ], ), ), ); if (reason == null || !mounted) return; final (success, message) = await _controller.report( targetType: targetType, targetId: targetId, reason: reason.trim().isEmpty ? null : reason.trim(), ); if (!mounted) return; AppNotice.show( context, message, icon: success ? Icons.flag_rounded : Icons.error_outline_rounded, ); } Future _deletePost() async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ'), content: const Text('์ด ๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('์ทจ์†Œ', style: TextStyle(color: Colors.grey)), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), child: const Text('์‚ญ์ œ', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed != true) return; final (success, message) = await _controller.deletePost(); if (!mounted) return; AppNotice.show(context, message); if (success) Navigator.of(context).pop(true); } Future _deleteComment(int commentId) async { final (success, message) = await _controller.deleteComment(commentId); if (!mounted) return; if (success) _changed = true; AppNotice.show(context, message); } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) Navigator.of(context).pop(_changed); }, child: ListenableBuilder( listenable: _controller, builder: (context, _) { final post = _controller.post; return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, centerTitle: true, title: const TitlePill('๊ฒŒ์‹œ๊ธ€'), leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18), onPressed: () => Navigator.of(context).pop(_changed), ), actions: [ if (post != null && post['isMine'] != true) IconButton( icon: const Icon(Icons.flag_outlined), tooltip: '์‹ ๊ณ ', onPressed: () => _showReportSheet( targetType: 'post', targetId: widget.postId, ), ), if (post != null && post['isMine'] == true) IconButton( icon: const Icon(Icons.delete_outline_rounded), tooltip: '์‚ญ์ œ', onPressed: _deletePost, ), ], ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : _controller.postDeleted || post == null ? const Center( child: Text( '์‚ญ์ œ๋˜์—ˆ๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค.', style: TextStyle(color: Colors.grey), ), ) : Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.all(16), children: [ _buildPostCard(post), const SizedBox(height: 16), Text( '๋Œ“๊ธ€ ${_controller.comments.length}๊ฐœ', style: const TextStyle( fontWeight: FontWeight.bold, color: AppPalette.ink, ), ), const SizedBox(height: 8), ..._controller.comments.map(_buildCommentTile), ], ), ), _buildCommentComposer(), ], ), ); }, ), ); } Widget _buildPostCard(Map post) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppPalette.sage), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( post['title'] ?? '', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, color: AppPalette.ink, ), ), const SizedBox(height: 6), Row( children: [ Text( post['isMine'] == true ? '๊ธ€์“ด์ด(๋‚˜)' : '์ต๋ช…', style: const TextStyle(color: Colors.grey, fontSize: 12.5), ), const SizedBox(width: 8), Text( '${post['createdAt'] ?? ''}', style: const TextStyle(color: Colors.grey, fontSize: 12.5), ), const Spacer(), Icon(Icons.visibility_outlined, size: 14, color: Colors.grey), const SizedBox(width: 3), Text( '${post['viewCount'] ?? 0}', style: const TextStyle(color: Colors.grey, fontSize: 12.5), ), ], ), const Divider(height: 24), Text( post['content'] ?? '', style: const TextStyle(fontSize: 15, height: 1.5), ), ], ), ); } Widget _buildCommentTile(dynamic comment) { final bool isMine = comment['isMine'] == true; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(12), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( '${comment['anonLabel'] ?? '์ต๋ช…'}', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12.5, color: isMine ? AppPalette.ink : Colors.grey[700], ), ), const SizedBox(width: 8), Text( '${comment['createdAt'] ?? ''}', style: const TextStyle( color: Colors.grey, fontSize: 11.5, ), ), ], ), const SizedBox(height: 4), Text( comment['content'] ?? '', style: const TextStyle(fontSize: 14, height: 1.4), ), ], ), ), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: Icon( isMine ? Icons.delete_outline_rounded : Icons.flag_outlined, size: 18, color: Colors.grey, ), onPressed: isMine ? () => _deleteComment(comment['id'] as int) : () => _showReportSheet( targetType: 'comment', targetId: comment['id'] as int, ), ), ], ), ); } Widget _buildCommentComposer() { return SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), child: Row( children: [ Expanded( child: TextField( controller: _commentController, decoration: InputDecoration( hintText: '์ต๋ช…์œผ๋กœ ๋Œ“๊ธ€ ๋‹ฌ๊ธฐ', filled: true, fillColor: AppPalette.paper, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(999), borderSide: BorderSide.none, ), ), ), ), const SizedBox(width: 8), _controller.isBusy ? const SizedBox( width: 40, height: 40, child: Padding( padding: EdgeInsets.all(8), child: CircularProgressIndicator(strokeWidth: 2), ), ) : IconButton( icon: const Icon(Icons.send_rounded, color: AppPalette.ink), onPressed: _submitComment, ), ], ), ), ); } }