From d5edaf0a4d4a26539af7c00d78f4dabbec4b2bdc Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 2 Jul 2026 13:43:07 +0900 Subject: [PATCH] Build mock directory UI --- .../directory/data/mock_directory_data.dart | 124 +++++ .../presentation/directory_screen.dart | 483 ++++++++++++++++-- app/test/widget_test.dart | 63 ++- ...plus-work-progress-timetable-2026-07-02.md | 12 +- docs/test-logs/2026-07-test-execution-log.md | 17 + 5 files changed, 658 insertions(+), 41 deletions(-) create mode 100644 app/lib/src/features/directory/data/mock_directory_data.dart diff --git a/app/lib/src/features/directory/data/mock_directory_data.dart b/app/lib/src/features/directory/data/mock_directory_data.dart new file mode 100644 index 0000000..2691ae9 --- /dev/null +++ b/app/lib/src/features/directory/data/mock_directory_data.dart @@ -0,0 +1,124 @@ +import '../domain/employee.dart'; +import '../../organization/domain/organization_models.dart'; + +const mockTenants = [ + TenantSummary( + id: 'tenant-hanmac', + name: '한맥', + slug: 'hanmac', + type: 'COMPANY', + parentId: null, + memberCount: 4, + totalMemberCount: 4, + ), + TenantSummary( + id: 'tenant-tdc', + name: 'TDC', + slug: 'tdc', + type: 'COMPANY', + parentId: null, + memberCount: 2, + totalMemberCount: 2, + ), + TenantSummary( + id: 'tenant-ict', + name: 'ICT사업부', + slug: 'ict', + type: 'ORGANIZATION', + parentId: 'tenant-hanmac', + memberCount: 2, + totalMemberCount: 2, + ), + TenantSummary( + id: 'tenant-rnd', + name: '기술연구소', + slug: 'rnd', + type: 'ORGANIZATION', + parentId: 'tenant-hanmac', + memberCount: 2, + totalMemberCount: 2, + ), +]; + +const mockEmployees = [ + Employee( + id: 'user-001', + name: '김하늘', + phoneNumber: '+821012345678', + phoneDisplay: '010-1234-5678', + email: 'haneul.kim@example.com', + tenantId: 'tenant-hanmac', + tenantName: '한맥', + tenantSlug: 'hanmac', + department: '기술연구소', + grade: '책임', + position: '팀장', + jobTitle: 'Flutter 개발', + status: 'active', + sortOrder: 10, + ), + Employee( + id: 'user-002', + name: '박서준', + phoneNumber: '+821023456789', + phoneDisplay: '010-2345-6789', + email: 'seojun.park@example.com', + tenantId: 'tenant-hanmac', + tenantName: '한맥', + tenantSlug: 'hanmac', + department: 'ICT사업부', + grade: '선임', + position: '매니저', + jobTitle: 'API 연동', + status: 'active', + sortOrder: 20, + ), + Employee( + id: 'user-003', + name: '이도윤', + phoneNumber: '+821034567890', + phoneDisplay: '010-3456-7890', + email: 'doyun.lee@example.com', + tenantId: 'tenant-tdc', + tenantName: 'TDC', + tenantSlug: 'tdc', + department: '운영지원', + grade: '책임', + position: '파트장', + jobTitle: '서비스 운영', + status: 'active', + sortOrder: 30, + ), + Employee( + id: 'user-004', + name: '정수빈', + phoneNumber: '+821045678901', + phoneDisplay: '010-4567-8901', + email: null, + tenantId: 'tenant-tdc', + tenantName: 'TDC', + tenantSlug: 'tdc', + department: '고객지원', + grade: '사원', + position: '담당', + jobTitle: '상담', + status: 'active', + sortOrder: 40, + ), + Employee( + id: 'user-005', + name: '최민준', + phoneNumber: '', + phoneDisplay: null, + email: 'minjun.choi@example.com', + tenantId: 'tenant-hanmac', + tenantName: '한맥', + tenantSlug: 'hanmac', + department: '기술연구소', + grade: '수석', + position: '연구원', + jobTitle: '보안 검토', + status: 'temporary_leave', + sortOrder: 50, + ), +]; diff --git a/app/lib/src/features/directory/presentation/directory_screen.dart b/app/lib/src/features/directory/presentation/directory_screen.dart index 522781d..3630adc 100644 --- a/app/lib/src/features/directory/presentation/directory_screen.dart +++ b/app/lib/src/features/directory/presentation/directory_screen.dart @@ -1,54 +1,218 @@ import 'package:flutter/material.dart'; -class DirectoryScreen extends StatelessWidget { +import '../data/mock_directory_data.dart'; +import '../domain/employee.dart'; + +class DirectoryScreen extends StatefulWidget { const DirectoryScreen({super.key}); static const routePath = '/directory'; + @override + State createState() => _DirectoryScreenState(); +} + +enum _DirectoryView { employees, organization, favorites } + +class _DirectoryScreenState extends State { + final _searchController = TextEditingController(); + final _favoriteIds = {'user-001'}; + var _view = _DirectoryView.employees; + var _selectedTenantSlug = 'all'; + var _query = ''; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List get _filteredEmployees { + final normalizedQuery = _query.replaceAll('-', '').toLowerCase(); + return mockEmployees.where((employee) { + final matchesTenant = + _selectedTenantSlug == 'all' || + employee.tenantSlug == _selectedTenantSlug; + final phone = employee.phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); + final phoneDisplay = employee.phoneDisplay?.replaceAll('-', '') ?? ''; + final searchable = [ + employee.name, + employee.phoneNumber, + employee.phoneDisplay ?? '', + employee.email ?? '', + employee.tenantName, + employee.department ?? '', + employee.grade ?? '', + employee.position ?? '', + employee.jobTitle ?? '', + phone, + phoneDisplay, + ].join(' ').toLowerCase(); + final matchesQuery = + normalizedQuery.isEmpty || searchable.contains(normalizedQuery); + final matchesFavorite = + _view != _DirectoryView.favorites || + _favoriteIds.contains(employee.id); + return matchesTenant && matchesQuery && matchesFavorite; + }).toList()..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); + } + @override Widget build(BuildContext context) { + final employees = _filteredEmployees; return Scaffold( appBar: AppBar(title: const Text('직원검색')), body: SafeArea( child: ListView( padding: const EdgeInsets.all(16), - children: const [ - _SearchField(), - SizedBox(height: 16), - _FeatureTile( - icon: Icons.manage_search, - title: '직원검색', - subtitle: '이름, 부서, 직위로 직원을 검색합니다.', + children: [ + _SearchField( + controller: _searchController, + onChanged: (value) => setState(() => _query = value), ), - _FeatureTile( - icon: Icons.phone, - title: '전화번호검색', - subtitle: '전화번호로 직원을 검색합니다.', + const SizedBox(height: 12), + _TenantFilterBar( + selectedTenantSlug: _selectedTenantSlug, + onSelected: (slug) => setState(() => _selectedTenantSlug = slug), ), - _FeatureTile( - icon: Icons.account_tree, - title: '조직도', - subtitle: '가족사와 부서 기준으로 직원을 탐색합니다.', + const SizedBox(height: 12), + _ViewSelector( + selected: _view, + onSelected: (value) => setState(() => _view = value), ), - _FeatureTile( - icon: Icons.star, - title: '즐겨찾기', - subtitle: '자주 연락하는 직원을 저장합니다.', + const SizedBox(height: 16), + _SummaryRow( + count: employees.length, + favoritesCount: _favoriteIds.length, ), + const SizedBox(height: 8), + if (_view == _DirectoryView.organization) + _OrganizationSection( + employees: employees, + favoriteIds: _favoriteIds, + onFavoriteToggle: _toggleFavorite, + onEmployeeTap: _showEmployeeDetail, + ) + else if (employees.isEmpty) + const _EmptyState() + else + for (final employee in employees) + _EmployeeTile( + employee: employee, + isFavorite: _favoriteIds.contains(employee.id), + onFavoriteToggle: () => _toggleFavorite(employee.id), + onTap: () => _showEmployeeDetail(employee), + ), ], ), ), ); } + + void _toggleFavorite(String employeeId) { + setState(() { + if (_favoriteIds.contains(employeeId)) { + _favoriteIds.remove(employeeId); + } else { + _favoriteIds.add(employeeId); + } + }); + } + + void _showEmployeeDetail(Employee employee) { + showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (context) { + final hasPhone = employee.phoneNumber.trim().isNotEmpty; + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar(child: Text(employee.name.characters.first)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + employee.name, + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + '${employee.tenantName} · ${employee.department ?? '-'}', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + IconButton( + tooltip: _favoriteIds.contains(employee.id) + ? '즐겨찾기 해제' + : '즐겨찾기 추가', + onPressed: () { + _toggleFavorite(employee.id); + Navigator.of(context).pop(); + }, + icon: Icon( + _favoriteIds.contains(employee.id) + ? Icons.star + : Icons.star_border, + ), + ), + ], + ), + const SizedBox(height: 16), + _DetailLine(label: '전화번호', value: employee.phoneDisplay ?? '-'), + _DetailLine(label: '이메일', value: employee.email ?? '-'), + _DetailLine(label: '직급/직위', value: _roleText(employee)), + _DetailLine(label: '직무', value: employee.jobTitle ?? '-'), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: FilledButton.icon( + onPressed: hasPhone ? () {} : null, + icon: const Icon(Icons.call), + label: const Text('전화'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: hasPhone ? () {} : null, + icon: const Icon(Icons.sms), + label: const Text('문자'), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } } class _SearchField extends StatelessWidget { - const _SearchField(); + const _SearchField({required this.controller, required this.onChanged}); + + final TextEditingController controller; + final ValueChanged onChanged; @override Widget build(BuildContext context) { - return const TextField( - decoration: InputDecoration( + return TextField( + controller: controller, + onChanged: onChanged, + decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '직원명 또는 전화번호 검색', prefixIcon: Icon(Icons.search), @@ -57,25 +221,276 @@ class _SearchField extends StatelessWidget { } } -class _FeatureTile extends StatelessWidget { - const _FeatureTile({ - required this.icon, - required this.title, - required this.subtitle, +class _TenantFilterBar extends StatelessWidget { + const _TenantFilterBar({ + required this.selectedTenantSlug, + required this.onSelected, }); - final IconData icon; - final String title; - final String subtitle; + final String selectedTenantSlug; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + final companyTenants = mockTenants.where( + (tenant) => tenant.type == 'COMPANY', + ); + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: const Text('전체'), + selected: selectedTenantSlug == 'all', + onSelected: (_) => onSelected('all'), + ), + ), + for (final tenant in companyTenants) + Padding( + padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: Text(tenant.name), + selected: selectedTenantSlug == tenant.slug, + onSelected: (_) => onSelected(tenant.slug), + ), + ), + ], + ), + ); + } +} + +class _ViewSelector extends StatelessWidget { + const _ViewSelector({required this.selected, required this.onSelected}); + + final _DirectoryView selected; + final ValueChanged<_DirectoryView> onSelected; + + @override + Widget build(BuildContext context) { + return SegmentedButton<_DirectoryView>( + segments: const [ + ButtonSegment( + value: _DirectoryView.employees, + icon: Icon(Icons.people), + label: Text('직원'), + ), + ButtonSegment( + value: _DirectoryView.organization, + icon: Icon(Icons.account_tree), + label: Text('조직도'), + ), + ButtonSegment( + value: _DirectoryView.favorites, + icon: Icon(Icons.star), + label: Text('즐겨찾기'), + ), + ], + selected: {selected}, + onSelectionChanged: (values) => onSelected(values.first), + ); + } +} + +class _SummaryRow extends StatelessWidget { + const _SummaryRow({required this.count, required this.favoritesCount}); + + final int count; + final int favoritesCount; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded(child: Text('검색 결과 $count명')), + Icon( + Icons.star, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 4), + Text('즐겨찾기 $favoritesCount명'), + ], + ); + } +} + +class _EmployeeTile extends StatelessWidget { + const _EmployeeTile({ + required this.employee, + required this.isFavorite, + required this.onFavoriteToggle, + required this.onTap, + }); + + final Employee employee; + final bool isFavorite; + final VoidCallback onFavoriteToggle; + final VoidCallback onTap; @override Widget build(BuildContext context) { return Card( child: ListTile( - leading: Icon(icon), - title: Text(title), - subtitle: Text(subtitle), + leading: CircleAvatar(child: Text(employee.name.characters.first)), + title: Text(employee.name), + subtitle: Text( + [ + employee.tenantName, + employee.department, + employee.position, + employee.phoneDisplay, + ].whereType().where((value) => value.isNotEmpty).join(' · '), + ), + trailing: IconButton( + tooltip: isFavorite ? '즐겨찾기 해제' : '즐겨찾기 추가', + onPressed: onFavoriteToggle, + icon: Icon(isFavorite ? Icons.star : Icons.star_border), + ), + onTap: onTap, ), ); } } + +class _OrganizationSection extends StatelessWidget { + const _OrganizationSection({ + required this.employees, + required this.favoriteIds, + required this.onFavoriteToggle, + required this.onEmployeeTap, + }); + + final List employees; + final Set favoriteIds; + final ValueChanged onFavoriteToggle; + final ValueChanged onEmployeeTap; + + @override + Widget build(BuildContext context) { + final tenantNames = employees + .map((employee) => employee.tenantName) + .toSet(); + if (employees.isEmpty) { + return const _EmptyState(); + } + return Column( + children: [ + for (final tenantName in tenantNames) + _OrganizationGroup( + tenantName: tenantName, + employees: employees + .where((employee) => employee.tenantName == tenantName) + .toList(), + favoriteIds: favoriteIds, + onFavoriteToggle: onFavoriteToggle, + onEmployeeTap: onEmployeeTap, + ), + ], + ); + } +} + +class _OrganizationGroup extends StatelessWidget { + const _OrganizationGroup({ + required this.tenantName, + required this.employees, + required this.favoriteIds, + required this.onFavoriteToggle, + required this.onEmployeeTap, + }); + + final String tenantName; + final List employees; + final Set favoriteIds; + final ValueChanged onFavoriteToggle; + final ValueChanged onEmployeeTap; + + @override + Widget build(BuildContext context) { + final departments = employees + .map((employee) => employee.department ?? '미지정') + .toSet(); + return Card( + child: ExpansionTile( + initiallyExpanded: true, + leading: const Icon(Icons.account_tree), + title: Text(tenantName), + subtitle: Text('${employees.length}명'), + children: [ + for (final department in departments) + ExpansionTile( + title: Text(department), + children: [ + for (final employee in employees.where( + (employee) => (employee.department ?? '미지정') == department, + )) + ListTile( + leading: const Icon(Icons.person), + title: Text(employee.name), + subtitle: Text(_roleText(employee)), + trailing: IconButton( + tooltip: favoriteIds.contains(employee.id) + ? '즐겨찾기 해제' + : '즐겨찾기 추가', + onPressed: () => onFavoriteToggle(employee.id), + icon: Icon( + favoriteIds.contains(employee.id) + ? Icons.star + : Icons.star_border, + ), + ), + onTap: () => onEmployeeTap(employee), + ), + ], + ), + ], + ), + ); + } +} + +class _DetailLine extends StatelessWidget { + const _DetailLine({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 80, + child: Text(label, style: Theme.of(context).textTheme.labelLarge), + ), + Expanded(child: Text(value)), + ], + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + const _EmptyState(); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 48), + child: Center(child: Text('검색 결과가 없습니다.')), + ); + } +} + +String _roleText(Employee employee) { + return [ + employee.grade, + employee.position, + employee.jobTitle, + ].whereType().where((value) => value.isNotEmpty).join(' · '); +} diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index bcb8b93..03ae25f 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -22,6 +22,67 @@ void main() { await tester.pumpAndSettle(); expect(find.text('직원검색'), findsWidgets); - expect(find.text('전화번호검색'), findsOneWidget); + expect(find.text('김하늘'), findsOneWidget); + expect(find.text('검색 결과 5명'), findsOneWidget); + }); + + testWidgets('filters employees by search query', (WidgetTester tester) async { + await _openDirectory(tester); + + await tester.enterText(find.byType(TextField), '서준'); + await tester.pumpAndSettle(); + + expect(find.text('박서준'), findsOneWidget); + expect(find.text('김하늘'), findsNothing); + expect(find.text('검색 결과 1명'), findsOneWidget); + }); + + testWidgets('filters employees by tenant chip', (WidgetTester tester) async { + await _openDirectory(tester); + + await tester.tap(find.text('TDC')); + await tester.pumpAndSettle(); + + expect(find.text('이도윤'), findsOneWidget); + expect(find.text('정수빈'), findsOneWidget); + expect(find.text('김하늘'), findsNothing); + expect(find.text('검색 결과 2명'), findsOneWidget); + }); + + testWidgets('shows favorite employees view', (WidgetTester tester) async { + await _openDirectory(tester); + + await tester.tap(find.text('즐겨찾기')); + await tester.pumpAndSettle(); + + expect(find.text('김하늘'), findsOneWidget); + expect(find.text('박서준'), findsNothing); + expect(find.text('검색 결과 1명'), findsOneWidget); + }); + + testWidgets('opens employee detail bottom sheet', ( + WidgetTester tester, + ) async { + await _openDirectory(tester); + + await tester.tap(find.text('김하늘')); + await tester.pumpAndSettle(); + + expect(find.text('haneul.kim@example.com'), findsOneWidget); + expect(find.text('전화'), findsOneWidget); + expect(find.text('문자'), findsOneWidget); }); } + +Future _openDirectory(WidgetTester tester) async { + await tester.pumpWidget(const ProviderScope(child: Tdc114PlusApp())); + await tester.pumpAndSettle(); + + if (find.text('직원검색').evaluate().isNotEmpty) { + return; + } + + await tester.enterText(find.byType(TextField), '01012345678'); + await tester.tap(find.text('로그인')); + await tester.pumpAndSettle(); +} diff --git a/docs/tdc114plus-work-progress-timetable-2026-07-02.md b/docs/tdc114plus-work-progress-timetable-2026-07-02.md index 55f20a9..ad514d8 100644 --- a/docs/tdc114plus-work-progress-timetable-2026-07-02.md +++ b/docs/tdc114plus-work-progress-timetable-2026-07-02.md @@ -49,8 +49,8 @@ | 15 | 테스트 정책 정식화 | 완료 | AI testing 참고 초안을 tdc114plus Flutter 앱/API/보안 중심 테스트 정책으로 재해석 | `docs/tdc114plus-testing-policy-2026-07-02.md` | | 16 | 테스트 자동화 스크립트 초기 구성 | 완료 | 테스트 정책 기반 scripts 추가 및 scaffold 상태 문서화 | `scripts/*.sh`, `docs/tdc114plus-script-automation-plan-2026-07-02.md` | | 17 | 테스트 실행 로그 관리 체계 생성 | 완료 | 월별 누적 테스트 로그 폴더와 2026-07 로그 파일 생성 | `docs/test-logs/` | -| 18 | Mock 데이터 기반 화면 확장 | 다음 작업 | 직원목록, 검색, 가족사 필터, 조직도 화면을 mock 데이터로 우선 구현 | 동작 가능한 UI | -| 19 | Baron SSO 로그인 연동 | 대기 | 전화번호 입력 후 SSO 등록 인원 여부 확인 연동 | 로그인 client/repository | +| 18 | Mock 데이터 기반 화면 확장 | 완료 | 직원목록, 검색, 가족사 필터, 조직도, 직원 상세, 즐겨찾기 화면을 mock 데이터로 우선 구현 | 동작 가능한 UI 및 widget test | +| 19 | Baron SSO 로그인 연동 | 다음 작업 | 전화번호 입력 후 SSO 등록 인원 여부 확인 연동 | 로그인 client/repository | | 20 | orgFront 데이터 연동 | 대기 | 직원/조직 데이터 API 연동 | directory/organization client | | 21 | 전화/문자 액션 구현 | 대기 | `url_launcher` 기반 전화걸기/문자보내기 구현 | 연락 액션 | | 22 | 즐겨찾기 구현 | 대기 | 1차 로컬 저장 기반 즐겨찾기 구현 | favorites feature | @@ -69,14 +69,14 @@ | Phase 2-4 | 완료 | 테스트 정책 정식화 | AI testing 참고 초안을 tdc114plus 적용 기준으로 정리 | `docs/tdc114plus-testing-policy-2026-07-02.md` | | Phase 2-5 | 완료 | 테스트 자동화 스크립트 초기 구성 | 즉시 실행 가능한 quality/format/report 스크립트와 scaffold 스크립트 추가 | `docs/tdc114plus-script-automation-plan-2026-07-02.md` | | Phase 2-6 | 완료 | 테스트 실행 로그 관리 체계 생성 | 월별 누적 테스트 로그 문서와 작성 규칙 추가 | `docs/test-logs/` | -| Phase 3 | 다음 | Mock 기반 1차 UI 완성 | 직원목록, 검색, 가족사 필터, 조직도, 상세 화면 구성 | API 없이 화면 흐름 확인 가능 | -| Phase 4 | 이후 | 실제 API 연동 | SSO 로그인, orgFront 직원/조직 데이터 연동 | 등록 사용자 로그인 및 직원목록 조회 | +| Phase 3 | 완료 | Mock 기반 1차 UI 완성 | 직원목록, 검색, 가족사 필터, 조직도, 상세 화면 구성 | analyze/test 통과 | +| Phase 4 | 다음 | 실제 API 연동 | SSO 로그인, orgFront 직원/조직 데이터 연동 | 등록 사용자 로그인 및 직원목록 조회 | | Phase 5 | 이후 | 핵심 액션 완성 | 전화걸기, 문자보내기, 즐겨찾기 저장 | 1차 기본 기능 수동 검증 | | Phase 6 | 이후 | 빌드/배포 준비 | Android debug APK, README 실행법, 잔여 이슈 정리 | APK 빌드 성공 | ## 4. 다음 작업 판단 -현재 다음 작업은 **Phase 3: Mock 기반 1차 UI 완성**이다. +현재 다음 작업은 **Phase 4: 실제 API 연동 준비 및 Baron SSO 로그인 연동**이다. 공식 Baron SSO `origin/dev` 기준의 API 개발용 worktree는 `/home/ubuntu/workspace/baron-sso-tdc114plus-api`에 생성 완료했다. 기존 `baron-sso` 작업 브랜치는 수정/미추적 파일이 많으므로 직접 merge/rebase하지 않는다. @@ -140,4 +140,4 @@ git -C /home/ubuntu/workspace/tdc114plus log --oneline origin/main -n 10 git -C /home/ubuntu/workspace/tdc114plus status --short --branch ``` -현재 문서 기준 다음 작업은 `Phase 3: Mock 기반 1차 UI 완성`이다. +현재 문서 기준 다음 작업은 `Phase 4: 실제 API 연동 준비 및 Baron SSO 로그인 연동`이다. diff --git a/docs/test-logs/2026-07-test-execution-log.md b/docs/test-logs/2026-07-test-execution-log.md index 1a2bbff..7a52630 100644 --- a/docs/test-logs/2026-07-test-execution-log.md +++ b/docs/test-logs/2026-07-test-execution-log.md @@ -88,3 +88,20 @@ - Phase 3 선택 테스트에 Playwright MCP 기반 smoke/screenshot 검증 후보 추가 - 후속 조치: - Flutter web/preview 실행 방식이 정해지면 Playwright MCP 시나리오와 로그 기록 방식을 구체화 + +## 2026-07-02 14:05 KST - Phase 3 Mock UI 1차 구현 검증 + +- 목적: 직원목록, 검색, 가족사 필터, 조직도, 직원 상세, 즐겨찾기 mock UI 구현 결과 검증 +- 실행 명령: + - `./scripts/format-dart.sh` + - `./scripts/quality-gate.sh` +- 결과: 1차 실패 후 수정하여 통과 +- 주요 출력: + - `format-dart.sh`: `directory_screen.dart`, `widget_test.dart` 포맷 적용 + - 1차 `quality-gate.sh`: analyze 통과, widget test 4건 실패 + - 실패 원인: 전역 `GoRouter`가 이전 테스트의 `/directory` 위치를 유지하여 helper가 로그인 버튼을 찾지 못함 + - 수정 내용: 테스트 helper가 이미 직원검색 화면이면 로그인 단계를 건너뛰도록 보정 + - 최종 `quality-gate.sh`: analyze 통과, `flutter test` All tests passed +- 후속 조치: + - Phase 3 Mock 기반 1차 UI 완성 처리 + - 다음 단계는 Phase 4 실제 API 연동 준비 및 Baron SSO 로그인 연동