diff --git a/assets/img/FORESTRING_Logo.png b/assets/img/FORESTRING_Logo.png deleted file mode 100644 index 0d36f009..00000000 Binary files a/assets/img/FORESTRING_Logo.png and /dev/null differ diff --git a/assets/img/FORESTRING_Logo_small.png b/assets/img/FORESTRING_Logo_small.png deleted file mode 100644 index 880075e4..00000000 Binary files a/assets/img/FORESTRING_Logo_small.png and /dev/null differ diff --git a/lib/core/qa/manager_qa_repositories.dart b/lib/core/qa/manager_qa_repositories.dart deleted file mode 100644 index 055d0868..00000000 --- a/lib/core/qa/manager_qa_repositories.dart +++ /dev/null @@ -1,4 +0,0 @@ -// Backward-compatible export kept while the manager QA sandbox is migrated -// to the shared store/repository architecture. -export 'qa_sandbox_repositories.dart'; -export 'qa_sandbox_store.dart'; diff --git a/lib/features/lessons/presentation/widgets/canceled_lesson_detail_sheet.dart b/lib/features/lessons/presentation/widgets/canceled_lesson_detail_sheet.dart deleted file mode 100644 index 24650e4e..00000000 --- a/lib/features/lessons/presentation/widgets/canceled_lesson_detail_sheet.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import '../../../../core/theme/forestring_theme.dart'; -import '../../domain/lesson.dart'; - -Future showCanceledLessonDetailSheet({ - required BuildContext context, - required Lesson lesson, -}) async { - await showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - barrierColor: Colors.black.withValues(alpha: 0.42), - builder: (sheetContext) { - return SafeArea( - top: false, - child: Container( - padding: const EdgeInsets.fromLTRB(18, 10, 18, 22), - decoration: const BoxDecoration( - color: neutralIvory, - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - child: FutureBuilder<_CancellationActor>( - future: _loadCancellationActor(lesson), - builder: (context, snapshot) { - final actor = snapshot.data; - final canceledAt = lesson.canceledAt; - final reason = lesson.cancellationReason?.trim(); - final isStudentCancel = actor?.id == lesson.studentId; - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - width: 42, - height: 4, - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(999), - ), - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: Text( - '취소 내역', - style: forestringTextStyle.copyWith( - color: primaryColor, - fontSize: 21, - fontWeight: FontWeight.w500, - ), - ), - ), - _pill('취소됨'), - ], - ), - const SizedBox(height: 6), - Text( - '${lesson.studentName ?? '학생'} · ${lesson.type.label}', - style: forestringTextStyle.copyWith( - color: Colors.black87, - fontSize: 17, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: primaryColor.withValues(alpha: 0.14), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '취소된 수업', - style: forestringTextStyle.copyWith( - color: Colors.black54, - fontSize: 13, - ), - ), - const SizedBox(height: 5), - Text( - '${DateFormat('M월 d일 HH:mm').format(lesson.startsAt)} ~ ' - '${DateFormat('HH:mm').format(lesson.endsAt)}', - style: forestringTextStyle.copyWith( - color: Colors.black87, - fontSize: 16, - decoration: TextDecoration.lineThrough, - ), - ), - if (lesson.teacherName != null) ...[ - const SizedBox(height: 4), - Text( - '${lesson.teacherName} 선생님', - style: forestringTextStyle.copyWith( - color: Colors.black54, - fontSize: 13, - ), - ), - ], - ], - ), - ), - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.redAccent.withValues(alpha: 0.055), - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _detailRow( - '처리 구분', - snapshot.connectionState == ConnectionState.waiting - ? '확인 중...' - : isStudentCancel - ? '학생 취소' - : '학원 취소', - ), - const SizedBox(height: 8), - _detailRow( - '처리자', - snapshot.connectionState == ConnectionState.waiting - ? '확인 중...' - : actor?.displayLabel ?? '확인되지 않음', - ), - const SizedBox(height: 8), - _detailRow( - '취소 일시', - canceledAt == null - ? '기록 없음' - : DateFormat('yyyy.MM.dd HH:mm').format(canceledAt), - ), - if (reason != null && reason.isNotEmpty) ...[ - const SizedBox(height: 8), - _detailRow('사유', reason), - ], - ], - ), - ), - const SizedBox(height: 14), - FilledButton( - onPressed: () => Navigator.of(sheetContext).pop(), - style: FilledButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - child: const Text('확인'), - ), - ], - ); - }, - ), - ), - ); - }, - ); -} - -Widget _detailRow(String label, String value) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 72, - child: Text( - label, - style: forestringTextStyle.copyWith( - color: Colors.black45, - fontSize: 13, - ), - ), - ), - Expanded( - child: Text( - value, - style: forestringTextStyle.copyWith( - color: Colors.black87, - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ); -} - -Widget _pill(String text) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(999), - ), - child: Text( - text, - style: forestringTextStyle.copyWith( - color: Colors.black54, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ); -} - -Future<_CancellationActor> _loadCancellationActor(Lesson lesson) async { - try { - final client = Supabase.instance.client; - final lessonRow = await client - .from('lessons') - .select('canceled_by') - .eq('id', lesson.id) - .maybeSingle(); - final actorId = lessonRow?['canceled_by']?.toString(); - if (actorId == null || actorId.isEmpty) { - return const _CancellationActor( - id: '', - name: null, - role: null, - ); - } - - final profileRow = await client - .from('profiles') - .select('display_name, role') - .eq('id', actorId) - .maybeSingle(); - - return _CancellationActor( - id: actorId, - name: profileRow?['display_name']?.toString(), - role: profileRow?['role']?.toString(), - ); - } catch (_) { - return const _CancellationActor( - id: '', - name: null, - role: null, - ); - } -} - -class _CancellationActor { - const _CancellationActor({ - required this.id, - required this.name, - required this.role, - }); - - final String id; - final String? name; - final String? role; - - String get displayLabel { - if (id.isEmpty) return '확인되지 않음'; - - final roleLabel = switch (role) { - 'master' => '전체 관리자', - 'manager' => '지점장', - 'teacher' => '선생님', - 'student' => '수강생', - _ => '사용자', - }; - final displayName = name?.trim(); - return displayName == null || displayName.isEmpty - ? roleLabel - : '$displayName · $roleLabel'; - } -} diff --git a/lib/features/managers/presentation/create_manager_smoke_page.dart b/lib/features/managers/presentation/create_manager_smoke_page.dart deleted file mode 100644 index eaa157cb..00000000 --- a/lib/features/managers/presentation/create_manager_smoke_page.dart +++ /dev/null @@ -1,295 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../../branches/data/branch_repository.dart'; -import '../../branches/domain/academy_branch.dart'; -import '../data/manager_repository.dart'; - -class CreateManagerSmokePage extends StatefulWidget { - const CreateManagerSmokePage({ - super.key, - }); - - @override - State createState() => _CreateManagerSmokePageState(); -} - -class _CreateManagerSmokePageState extends State { - final _nameController = TextEditingController(); - - final _pinController = TextEditingController(); - - final _repository = ManagerRepository(); - - final _branchRepository = BranchRepository(); - - List _branches = const []; - - String? _branchId; - - bool _isLoading = false; - bool _isLoadingBranches = true; - - String? _resultMessage; - - @override - void initState() { - super.initState(); - - _loadBranches(); - } - - @override - void dispose() { - _nameController.dispose(); - _pinController.dispose(); - - super.dispose(); - } - - Future _loadBranches() async { - setState(() { - _isLoadingBranches = true; - }); - - try { - final branches = await _branchRepository.fetchBranches(); - - if (!mounted) { - return; - } - - final activeBranches = branches - .where( - (branch) => branch.isActive, - ) - .toList(); - - setState(() { - _branches = activeBranches; - - if (activeBranches.isNotEmpty) { - _branchId = activeBranches.first.id; - } - - _resultMessage = null; - }); - } on BranchFailure catch (error) { - if (!mounted) { - return; - } - - setState(() { - _resultMessage = '지점 목록 조회 실패\n' - '${error.message}'; - }); - } finally { - if (mounted) { - setState(() { - _isLoadingBranches = false; - }); - } - } - } - - Future _createManager() async { - FocusManager.instance.primaryFocus?.unfocus(); - - final branchId = _branchId; - - if (branchId == null || branchId.isEmpty) { - setState(() { - _resultMessage = '지점을 선택해주세요.'; - }); - - return; - } - - setState(() { - _isLoading = true; - _resultMessage = null; - }); - - try { - final manager = await _repository.createManager( - name: _nameController.text, - pin: _pinController.text, - branchId: branchId, - ); - - if (!mounted) { - return; - } - - String? branchName; - - for (final branch in _branches) { - if (branch.id == branchId) { - branchName = branch.name; - break; - } - } - - setState(() { - _resultMessage = '생성 성공\n' - '지점: ${branchName ?? '-'}\n' - '지점장: ${manager.displayName}\n' - '${manager.id}'; - - _pinController.clear(); - }); - } on ManagerFailure catch (error) { - if (!mounted) { - return; - } - - setState(() { - _resultMessage = '생성 실패\n' - '${error.message}'; - }); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: const Text( - '지점장 생성 Smoke Test', - ), - ), - body: SafeArea( - child: ListView( - padding: const EdgeInsets.all( - 24, - ), - children: [ - const Text( - '실제 Supabase DB에 지점장 계정을 생성합니다.', - ), - const SizedBox( - height: 24, - ), - if (_isLoadingBranches) - const Center( - child: Padding( - padding: EdgeInsets.all( - 16, - ), - child: CircularProgressIndicator(), - ), - ) - else if (_branches.isEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - '등록된 활성 지점이 없습니다.', - ), - const SizedBox( - height: 8, - ), - OutlinedButton( - onPressed: _loadBranches, - child: const Text( - '지점 다시 불러오기', - ), - ), - ], - ) - else - DropdownButtonFormField( - initialValue: _branchId, - decoration: const InputDecoration( - labelText: '지점', - border: OutlineInputBorder(), - ), - items: _branches - .map( - (branch) => DropdownMenuItem( - value: branch.id, - child: Text( - branch.name, - ), - ), - ) - .toList(), - onChanged: (value) { - setState(() { - _branchId = value; - }); - }, - ), - const SizedBox( - height: 16, - ), - TextField( - controller: _nameController, - decoration: const InputDecoration( - labelText: '지점장 이름', - border: OutlineInputBorder(), - ), - ), - const SizedBox( - height: 16, - ), - TextField( - controller: _pinController, - keyboardType: TextInputType.number, - obscureText: true, - maxLength: 4, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter( - 4, - ), - ], - decoration: const InputDecoration( - labelText: '4자리 PIN', - border: OutlineInputBorder(), - ), - ), - const SizedBox( - height: 8, - ), - const SizedBox( - height: 24, - ), - FilledButton( - onPressed: _isLoading || _isLoadingBranches || _branches.isEmpty - ? null - : _createManager, - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Text( - '테스트 지점장 생성', - ), - ), - if (_resultMessage != null) ...[ - const SizedBox( - height: 24, - ), - SelectableText( - _resultMessage!, - ), - ], - ], - ), - ), - ); - } -} diff --git a/lib/features/managers/presentation/manager_rls_smoke_panel.dart b/lib/features/managers/presentation/manager_rls_smoke_panel.dart deleted file mode 100644 index f1f96544..00000000 --- a/lib/features/managers/presentation/manager_rls_smoke_panel.dart +++ /dev/null @@ -1,260 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -class ManagerRlsSmokePanel extends StatefulWidget { - const ManagerRlsSmokePanel({ - super.key, - }); - - @override - State createState() => _ManagerRlsSmokePanelState(); -} - -class _ManagerRlsSmokePanelState extends State { - bool _isLoading = false; - String? _result; - - Future _runTest() async { - setState(() { - _isLoading = true; - _result = null; - }); - - try { - final client = Supabase.instance.client; - - final currentUserId = client.auth.currentUser?.id; - - if (currentUserId == null) { - throw Exception('로그인 세션이 없습니다.'); - } - - // ------------------------------------------------------ - // RLS가 적용된 실제 client 조회 - // service_role 아님. - // ------------------------------------------------------ - - final branchRaw = await client.from('branches').select( - 'id, name', - ); - - final profileRaw = await client.from('profiles').select( - 'id, display_name, role, branch_id', - ); - - final teacherRaw = await client.from('teachers').select( - 'id', - ); - - final studentRaw = await client.from('students').select( - 'id', - ); - - final lessonRaw = await client.from('lessons').select( - 'id', - ); - - final branches = List>.from( - branchRaw, - ); - - final profiles = List>.from( - profileRaw, - ); - - final teachers = List>.from( - teacherRaw, - ); - - final students = List>.from( - studentRaw, - ); - - final lessons = List>.from( - lessonRaw, - ); - - final profileById = >{}; - - for (final profile in profiles) { - final id = profile['id']; - - if (id is String) { - profileById[id] = profile; - } - } - - final branchNames = branches - .map( - (branch) => branch['name']?.toString() ?? '(이름 없음)', - ) - .join(', '); - - final visibleProfiles = profiles.map( - (profile) { - final name = profile['display_name']?.toString() ?? '(이름 없음)'; - - final role = profile['role']?.toString() ?? '?'; - - final isMe = profile['id'] == currentUserId; - - return '$name [$role]' - '${isMe ? ' ← 나' : ''}'; - }, - ).join('\n'); - - final visibleTeachers = teachers.map( - (teacher) { - final id = teacher['id']?.toString(); - - final profile = id == null ? null : profileById[id]; - - final name = profile?['display_name']?.toString() ?? '(프로필 이름 조회 불가)'; - - final role = profile?['role']?.toString() ?? '?'; - - return '$name [$role]'; - }, - ).join('\n'); - - // 현재 테스트 환경에는 지점이 2개 이상 존재하므로 - // manager가 1개 지점만 본다면 기본 branch RLS PASS. - final branchRlsPass = branches.length == 1; - - final buffer = StringBuffer(); - - buffer.writeln( - branchRlsPass ? '✅ RLS 기본 판정: PASS' : '❌ RLS 기본 판정: FAIL', - ); - - buffer.writeln(); - buffer.writeln( - '현재 사용자 UUID', - ); - buffer.writeln( - currentUserId, - ); - - buffer.writeln(); - buffer.writeln( - '조회 가능한 지점 (${branches.length})', - ); - buffer.writeln( - branchNames.isEmpty ? '(없음)' : branchNames, - ); - - buffer.writeln(); - buffer.writeln( - '조회 가능한 프로필 (${profiles.length})', - ); - buffer.writeln( - visibleProfiles.isEmpty ? '(없음)' : visibleProfiles, - ); - - buffer.writeln(); - buffer.writeln( - '조회 가능한 수업 담당자 (${teachers.length})', - ); - buffer.writeln( - visibleTeachers.isEmpty ? '(없음)' : visibleTeachers, - ); - - buffer.writeln(); - buffer.writeln( - '조회 가능한 학생: ${students.length}명', - ); - - buffer.writeln( - '조회 가능한 수업: ${lessons.length}개', - ); - - if (!mounted) { - return; - } - - setState(() { - _result = buffer.toString(); - }); - } on PostgrestException catch (error) { - if (!mounted) { - return; - } - - setState(() { - _result = '❌ Supabase 조회 실패\n' - '${error.message}\n' - 'code: ${error.code}'; - }); - } catch (error) { - if (!mounted) { - return; - } - - setState(() { - _result = '❌ 테스트 실패\n$error'; - }); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build( - BuildContext context, - ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 32, - ), - const Divider(), - const SizedBox( - height: 16, - ), - const Text( - '임시 RLS 권한 테스트', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox( - height: 8, - ), - const Text( - '현재 로그인한 계정 권한으로 Supabase를 직접 조회합니다.', - ), - const SizedBox( - height: 16, - ), - OutlinedButton( - onPressed: _isLoading ? null : _runTest, - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Text( - 'RLS 권한 테스트 실행', - ), - ), - if (_result != null) ...[ - const SizedBox( - height: 20, - ), - SelectableText( - _result!, - ), - ], - ], - ); - } -} diff --git a/lib/features/students/data/student_repository.dart b/lib/features/students/data/student_repository.dart deleted file mode 100644 index 18d299bf..00000000 --- a/lib/features/students/data/student_repository.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:supabase_flutter/supabase_flutter.dart'; - -class StudentFailure implements Exception { - const StudentFailure(this.message); - - final String message; - - @override - String toString() => message; -} - -enum StudentType { - regular, - flex, -} - -extension StudentTypeX on StudentType { - String get value { - return switch (this) { - StudentType.regular => 'regular', - StudentType.flex => 'flex', - }; - } - - String get label { - return switch (this) { - StudentType.regular => '정규 학생', - StudentType.flex => '자율 예약 학생', - }; - } -} - -class CreatedStudent { - const CreatedStudent({ - required this.id, - required this.displayName, - required this.branchId, - required this.studentType, - }); - - final String id; - final String displayName; - final String branchId; - final StudentType studentType; -} - -class StudentRepository { - StudentRepository({ - SupabaseClient? client, - }) : _client = client ?? Supabase.instance.client; - - final SupabaseClient _client; - - Future createStudent({ - required String name, - required String pin, - required String branchId, - required StudentType studentType, - }) async { - final normalizedName = name - .trim() - .replaceAll( - RegExp(r'\s+'), - ' ', - ); - - if (normalizedName.isEmpty) { - throw const StudentFailure( - '학생 이름을 입력해주세요.', - ); - } - - if (!RegExp(r'^\d{4}$').hasMatch(pin)) { - throw const StudentFailure( - 'PIN은 4자리 숫자로 입력해주세요.', - ); - } - - if (branchId.isEmpty) { - throw const StudentFailure( - '지점을 선택해주세요.', - ); - } - - final session = _client.auth.currentSession; - - if (session == null) { - throw const StudentFailure( - '로그인이 필요합니다.', - ); - } - - try { - final response = await _client.functions.invoke( - 'staff-create-student', - headers: { - 'Authorization': 'Bearer ${session.accessToken}', - }, - body: { - 'name': normalizedName, - 'pin': pin, - 'branchId': branchId, - 'studentType': studentType.value, - }, - ); - - final data = response.data; - - if (data is! Map) { - throw const StudentFailure( - '학생 생성 서버 응답을 확인하지 못했습니다. 잠시 후 다시 시도해주세요.', - ); - } - - final studentId = data['studentId']; - - if (studentId is! String || studentId.isEmpty) { - final message = data['message']; - - throw StudentFailure( - message is String ? message : '학생 계정을 생성하지 못했습니다.', - ); - } - - final returnedType = data['studentType']; - - return CreatedStudent( - id: studentId, - displayName: data['displayName'] is String - ? data['displayName'] as String - : normalizedName, - branchId: data['branchId'] is String - ? data['branchId'] as String - : branchId, - studentType: - returnedType == 'flex' ? StudentType.flex : StudentType.regular, - ); - } on StudentFailure { - rethrow; - } on FunctionException catch (error) { - final details = error.details; - if (details is Map && details['message'] != null) { - throw StudentFailure(details['message'].toString()); - } - throw const StudentFailure( - '학생 생성 서버에 연결하지 못했습니다. 네트워크 상태를 확인해주세요.', - ); - } catch (_) { - throw const StudentFailure( - '학생 계정을 생성하는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', - ); - } - } -} diff --git a/lib/features/students/presentation/create_student_smoke_page.dart b/lib/features/students/presentation/create_student_smoke_page.dart deleted file mode 100644 index cb6d782a..00000000 --- a/lib/features/students/presentation/create_student_smoke_page.dart +++ /dev/null @@ -1,426 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../../auth/domain/current_profile.dart'; -import '../../branches/data/branch_repository.dart'; -import '../../branches/domain/academy_branch.dart'; -import '../data/student_repository.dart'; - -class CreateStudentSmokePage extends StatefulWidget { - const CreateStudentSmokePage({ - required this.actorProfile, - super.key, - }); - - final CurrentProfile actorProfile; - - @override - State createState() => - _CreateStudentSmokePageState(); -} - -class _CreateStudentSmokePageState - extends State { - final _nameController = - TextEditingController(); - - final _pinController = - TextEditingController(); - - final _repository = - StudentRepository(); - - final _branchRepository = - BranchRepository(); - - List _branches = - const []; - - String? _branchId; - - StudentType _studentType = - StudentType.regular; - - bool _isLoadingBranches = true; - bool _isLoading = false; - - String? _resultMessage; - - bool get _isMaster => - widget.actorProfile.role == - AppRole.master; - - @override - void initState() { - super.initState(); - - _loadBranches(); - } - - @override - void dispose() { - _nameController.dispose(); - _pinController.dispose(); - - super.dispose(); - } - - Future _loadBranches() async { - setState(() { - _isLoadingBranches = true; - }); - - try { - final branches = - await _branchRepository - .fetchBranches(); - - if (!mounted) { - return; - } - - final activeBranches = - branches - .where( - (branch) => - branch.isActive, - ) - .toList(); - - String? selectedBranchId; - - if (_isMaster) { - if (activeBranches.isNotEmpty) { - selectedBranchId = - activeBranches.first.id; - } - } else { - selectedBranchId = - widget.actorProfile.branchId; - - if (selectedBranchId != null && - !activeBranches.any( - (branch) => - branch.id == - selectedBranchId, - )) { - selectedBranchId = null; - } - } - - setState(() { - _branches = - activeBranches; - _branchId = - selectedBranchId; - _resultMessage = null; - }); - } on BranchFailure catch (error) { - if (!mounted) { - return; - } - - setState(() { - _resultMessage = - '지점 목록 조회 실패\n' - '${error.message}'; - }); - } finally { - if (mounted) { - setState(() { - _isLoadingBranches = - false; - }); - } - } - } - - Future _createStudent() async { - FocusManager - .instance - .primaryFocus - ?.unfocus(); - - final branchId = - _branchId; - - if (branchId == null || - branchId.isEmpty) { - setState(() { - _resultMessage = - '지점을 선택해주세요.'; - }); - - return; - } - - setState(() { - _isLoading = true; - _resultMessage = null; - }); - - try { - final student = - await _repository - .createStudent( - name: - _nameController.text, - pin: - _pinController.text, - branchId: - branchId, - studentType: - _studentType, - ); - - if (!mounted) { - return; - } - - String? branchName; - - for (final branch - in _branches) { - if (branch.id == - branchId) { - branchName = - branch.name; - break; - } - } - - setState(() { - _resultMessage = - '생성 성공\n' - '지점: ${branchName ?? '-'}\n' - '학생: ${student.displayName}\n' - '유형: ${student.studentType.label}\n' - '${student.id}'; - - _pinController.clear(); - }); - } on StudentFailure catch (error) { - if (!mounted) { - return; - } - - setState(() { - _resultMessage = - '생성 실패\n' - '${error.message}'; - }); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: const Text( - '학생 생성 Smoke Test', - ), - ), - body: SafeArea( - child: ListView( - padding: - const EdgeInsets.all( - 24, - ), - children: [ - const Text( - '실제 Supabase DB에 학생 계정을 생성합니다.', - ), - - const SizedBox( - height: 24, - ), - - if (_isLoadingBranches) - const Center( - child: - CircularProgressIndicator(), - ) - else if (_branches.isEmpty) - const Text( - '사용 가능한 지점이 없습니다.', - ) - else - DropdownButtonFormField< - String>( - initialValue: _branchId, - decoration: - const InputDecoration( - labelText: '지점', - border: - OutlineInputBorder(), - ), - items: _branches - .map( - (branch) => - DropdownMenuItem< - String>( - value: - branch.id, - child: Text( - branch.name, - ), - ), - ) - .toList(), - onChanged: - _isMaster - ? (value) { - setState(() { - _branchId = - value; - }); - } - : null, - ), - - if (!_isMaster) ...[ - const SizedBox( - height: 8, - ), - const Text( - '지점 관리자는 본인 지점에만 학생을 등록할 수 있습니다.', - ), - ], - - const SizedBox( - height: 16, - ), - - TextField( - controller: - _nameController, - decoration: - const InputDecoration( - labelText: '학생 이름', - border: - OutlineInputBorder(), - ), - ), - - const SizedBox( - height: 16, - ), - - TextField( - controller: - _pinController, - keyboardType: - TextInputType.number, - obscureText: true, - maxLength: 4, - inputFormatters: [ - FilteringTextInputFormatter - .digitsOnly, - LengthLimitingTextInputFormatter( - 4, - ), - ], - decoration: - const InputDecoration( - labelText: '4자리 PIN', - border: - OutlineInputBorder(), - ), - ), - - const SizedBox( - height: 8, - ), - - DropdownButtonFormField< - StudentType>( - initialValue: _studentType, - decoration: - const InputDecoration( - labelText: '학생 유형', - border: - OutlineInputBorder(), - ), - items: StudentType.values - .map( - (type) => - DropdownMenuItem< - StudentType>( - value: type, - child: Text( - type.label, - ), - ), - ) - .toList(), - onChanged: (value) { - if (value == null) { - return; - } - - setState(() { - _studentType = - value; - }); - }, - ), - - const SizedBox( - height: 8, - ), - - Text( - _studentType == - StudentType.regular - ? '정규 학생: 이후 담당 선생님과 반복 수업 시간표를 설정합니다.' - : '자율 예약 학생: 정규 반복 시간표 없이 필요할 때 수업을 예약합니다.', - ), - - const SizedBox( - height: 24, - ), - - FilledButton( - onPressed: - _isLoading || - _isLoadingBranches || - _branchId == null - ? null - : _createStudent, - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: - CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Text( - '테스트 학생 생성', - ), - ), - - if (_resultMessage != - null) ...[ - const SizedBox( - height: 24, - ), - SelectableText( - _resultMessage!, - ), - ], - ], - ), - ), - ); - } -} diff --git a/lib/features/students/presentation/student_registration_page.dart b/lib/features/students/presentation/student_registration_page.dart deleted file mode 100644 index f7a0d9a2..00000000 --- a/lib/features/students/presentation/student_registration_page.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../auth/domain/current_profile.dart'; -import 'student_management_page.dart'; - -/// 기존 Drawer/라우트 호환용 진입점. -/// v3-complete에서는 수강생 등록을 수강생 관리 화면 내부 액션으로 통합한다. -class StudentRegistrationPage extends StatelessWidget { - const StudentRegistrationPage({ - super.key, - required this.profile, - }); - - final CurrentProfile profile; - - @override - Widget build(BuildContext context) { - return StudentManagementPage(profile: profile); - } -} diff --git a/lib/features/teachers/presentation/create_teacher_smoke_page.dart b/lib/features/teachers/presentation/create_teacher_smoke_page.dart deleted file mode 100644 index c1c9f771..00000000 --- a/lib/features/teachers/presentation/create_teacher_smoke_page.dart +++ /dev/null @@ -1,406 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../../branches/data/branch_repository.dart'; -import '../../branches/domain/academy_branch.dart'; -import '../data/teacher_repository.dart'; - -class CreateTeacherSmokePage extends StatefulWidget { - const CreateTeacherSmokePage({ - super.key, - }); - - @override - State createState() => _CreateTeacherSmokePageState(); -} - -class _CreateTeacherSmokePageState extends State { - final _nameController = TextEditingController(); - final _pinController = TextEditingController(); - - final _repository = TeacherRepository(); - final _branchRepository = BranchRepository(); - - List _branches = const []; - - String? _branchId; - - int _weekday = 1; - - TimeOfDay _startTime = const TimeOfDay( - hour: 9, - minute: 0, - ); - - TimeOfDay _endTime = const TimeOfDay( - hour: 18, - minute: 0, - ); - - bool _isLoading = false; - bool _isLoadingBranches = true; - - String? _resultMessage; - - @override - void initState() { - super.initState(); - - _loadBranches(); - } - - @override - void dispose() { - _nameController.dispose(); - _pinController.dispose(); - - super.dispose(); - } - - Future _loadBranches() async { - setState(() { - _isLoadingBranches = true; - }); - - try { - final branches = await _branchRepository.fetchBranches(); - - if (!mounted) { - return; - } - - final activeBranches = branches - .where( - (branch) => branch.isActive, - ) - .toList(); - - setState(() { - _branches = activeBranches; - - if (activeBranches.isNotEmpty) { - _branchId = activeBranches.first.id; - } - - _resultMessage = null; - }); - } on BranchFailure catch (error) { - if (!mounted) { - return; - } - - setState(() { - _resultMessage = '지점 목록 조회 실패\n${error.message}'; - }); - } finally { - if (mounted) { - setState(() { - _isLoadingBranches = false; - }); - } - } - } - - String _formatTime( - TimeOfDay value, - ) { - return '${value.hour.toString().padLeft(2, '0')}:' - '${value.minute.toString().padLeft(2, '0')}'; - } - - bool _is15MinuteAligned( - TimeOfDay value, - ) { - return value.minute % 15 == 0; - } - - Future _pickStartTime() async { - final picked = await showTimePicker( - context: context, - initialTime: _startTime, - ); - - if (picked == null) { - return; - } - - setState(() { - _startTime = picked; - }); - } - - Future _pickEndTime() async { - final picked = await showTimePicker( - context: context, - initialTime: _endTime, - ); - - if (picked == null) { - return; - } - - setState(() { - _endTime = picked; - }); - } - - Future _createTeacher() async { - FocusManager.instance.primaryFocus?.unfocus(); - - final branchId = _branchId; - - if (branchId == null || branchId.isEmpty) { - setState(() { - _resultMessage = '지점을 선택해주세요.'; - }); - - return; - } - - if (!_is15MinuteAligned(_startTime) || !_is15MinuteAligned(_endTime)) { - setState(() { - _resultMessage = '근무시간은 15분 단위로 선택해주세요.'; - }); - - return; - } - - final startMinutes = _startTime.hour * 60 + _startTime.minute; - - final endMinutes = _endTime.hour * 60 + _endTime.minute; - - if (startMinutes >= endMinutes) { - setState(() { - _resultMessage = '종료시간은 시작시간보다 뒤여야 합니다.'; - }); - - return; - } - - setState(() { - _isLoading = true; - _resultMessage = null; - }); - - try { - final teacher = await _repository.createTeacher( - name: _nameController.text, - pin: _pinController.text, - branchId: branchId, - workHours: [ - TeacherWorkHourInput( - weekday: _weekday, - startTime: _formatTime(_startTime), - endTime: _formatTime(_endTime), - ), - ], - ); - - if (!mounted) { - return; - } - - String? branchName; - - for (final branch in _branches) { - if (branch.id == branchId) { - branchName = branch.name; - break; - } - } - - setState(() { - _resultMessage = '생성 성공\n' - '지점: ${branchName ?? '-'}\n' - '선생님: ${teacher.displayName}\n' - '${teacher.id}'; - - _pinController.clear(); - }); - } on TeacherFailure catch (error) { - if (!mounted) { - return; - } - - setState(() { - _resultMessage = '생성 실패\n${error.message}'; - }); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - const weekdays = { - 1: '월요일', - 2: '화요일', - 3: '수요일', - 4: '목요일', - 5: '금요일', - 6: '토요일', - 7: '일요일', - }; - - return Scaffold( - appBar: AppBar( - title: const Text( - '선생님 생성 Smoke Test', - ), - ), - body: SafeArea( - child: ListView( - padding: const EdgeInsets.all(24), - children: [ - const Text( - '실제 Supabase DB에 테스트 선생님 계정을 생성합니다.', - ), - const SizedBox(height: 24), - if (_isLoadingBranches) - const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ) - else if (_branches.isEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - '등록된 활성 지점이 없습니다.', - ), - const SizedBox(height: 8), - OutlinedButton( - onPressed: _loadBranches, - child: const Text( - '지점 다시 불러오기', - ), - ), - ], - ) - else - DropdownButtonFormField( - initialValue: _branchId, - decoration: const InputDecoration( - labelText: '지점', - border: OutlineInputBorder(), - ), - items: _branches - .map( - (branch) => DropdownMenuItem( - value: branch.id, - child: Text(branch.name), - ), - ) - .toList(), - onChanged: (value) { - setState(() { - _branchId = value; - }); - }, - ), - const SizedBox(height: 16), - TextField( - controller: _nameController, - decoration: const InputDecoration( - labelText: '선생님 이름', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - TextField( - controller: _pinController, - keyboardType: TextInputType.number, - obscureText: true, - maxLength: 4, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter( - 4, - ), - ], - decoration: const InputDecoration( - labelText: '4자리 PIN', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 8), - DropdownButtonFormField( - initialValue: _weekday, - decoration: const InputDecoration( - labelText: '근무 요일', - border: OutlineInputBorder(), - ), - items: weekdays.entries - .map( - (entry) => DropdownMenuItem( - value: entry.key, - child: Text(entry.value), - ), - ) - .toList(), - onChanged: (value) { - if (value == null) { - return; - } - - setState(() { - _weekday = value; - }); - }, - ), - const SizedBox(height: 16), - ListTile( - title: const Text('시작시간'), - subtitle: Text( - _formatTime(_startTime), - ), - trailing: const Icon( - Icons.access_time, - ), - onTap: _pickStartTime, - ), - ListTile( - title: const Text('종료시간'), - subtitle: Text( - _formatTime(_endTime), - ), - trailing: const Icon( - Icons.access_time, - ), - onTap: _pickEndTime, - ), - const SizedBox(height: 24), - FilledButton( - onPressed: _isLoading || _isLoadingBranches || _branches.isEmpty - ? null - : _createTeacher, - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Text( - '테스트 선생님 생성', - ), - ), - if (_resultMessage != null) ...[ - const SizedBox(height: 24), - SelectableText( - _resultMessage!, - ), - ], - ], - ), - ), - ); - } -}