1591 lines
47 KiB
Dart
1591 lines
47 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:tdc114plus/src/app.dart';
|
|
import 'package:tdc114plus/src/core/actions/contact_launcher.dart';
|
|
import 'package:tdc114plus/src/core/network/api_error.dart';
|
|
import 'package:tdc114plus/src/features/auth/data/auth_repository.dart';
|
|
import 'package:tdc114plus/src/features/auth/data/auth_session_store.dart';
|
|
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
|
|
import 'package:tdc114plus/src/features/directory/data/directory_repository.dart';
|
|
import 'package:tdc114plus/src/features/directory/data/mock_directory_data.dart';
|
|
import 'package:tdc114plus/src/features/directory/domain/employee.dart';
|
|
import 'package:tdc114plus/src/features/favorites/data/favorites_repository.dart';
|
|
import 'package:tdc114plus/src/features/favorites/domain/favorite_employee.dart';
|
|
import 'package:tdc114plus/src/features/organization/data/organization_api_client.dart';
|
|
import 'package:tdc114plus/src/features/organization/domain/organization_models.dart';
|
|
|
|
void main() {
|
|
testWidgets('shows link-based login screen', (WidgetTester tester) async {
|
|
await _pumpApp(tester);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('로그인 링크 발송'), findsOneWidget);
|
|
expect(find.byType(TextField), findsOneWidget);
|
|
expect(find.text('로그인 링크 보내기'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('restores saved session on app launch', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('직원검색'), findsWidgets);
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
expect(find.text('이도윤'), findsNothing);
|
|
expect(find.text('검색 결과 2명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('shows link login guidance with phone input', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
|
|
expect(find.byType(TextField), findsOneWidget);
|
|
expect(
|
|
find.textContaining('입력하신 전화번호로 바론SSO 로그인 링크가 발송됩니다.'),
|
|
findsNothing,
|
|
);
|
|
expect(find.textContaining('문자 승인 후 앱으로 돌아오면 자동 로그인됩니다.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('keeps login link button visible when keyboard is open', (
|
|
WidgetTester tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(750, 1334);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
await _pumpApp(tester);
|
|
|
|
await tester.tap(find.byType(TextField));
|
|
tester.view.viewInsets = const FakeViewPadding(bottom: 420);
|
|
addTearDown(tester.view.resetViewInsets);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('로그인 링크 보내기'), findsOneWidget);
|
|
expect(tester.getBottomLeft(find.text('로그인 링크 보내기')).dy, lessThan(914));
|
|
});
|
|
|
|
testWidgets('requests login link and shows pending status', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(pollMode: _FakePollMode.pendingOnly),
|
|
);
|
|
|
|
await tester.enterText(find.byType(TextField), '010-1234-5678');
|
|
await tester.tap(find.text('로그인 링크 보내기'));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
find.textContaining('문자 링크를 누른 뒤 TDC114PLUS 앱으로 돌아오세요.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('승인 대기 중'), findsOneWidget);
|
|
expect(find.text('다른 번호 입력'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('restores pending login link and completes polling', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final repository = _FakeAuthRepository(
|
|
initialPendingLink: const PhoneLoginLinkInitResponse(
|
|
status: 'pending',
|
|
pendingRef: 'restored-pending-ref',
|
|
expiresIn: 180,
|
|
interval: 1,
|
|
resendAfter: 0,
|
|
),
|
|
);
|
|
await _pumpApp(tester, repository: repository);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(repository.polledRefs, contains('restored-pending-ref'));
|
|
expect(find.text('직원검색'), findsWidgets);
|
|
});
|
|
|
|
testWidgets('defaults directory scope to logged-in user tenant', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
expect(find.text('이도윤'), findsNothing);
|
|
expect(find.text('정수빈'), findsNothing);
|
|
});
|
|
|
|
testWidgets('keeps initial selection at logged-in pinned team scope', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
directoryRepository: const _OrganizationScopedDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Hanmac'), findsNothing);
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
expect(find.text('검색 결과 2명'), 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('박서준'), findsNothing);
|
|
expect(find.text('김하늘'), findsNothing);
|
|
expect(find.text('검색 결과 0명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('allows one hangul character for employee search', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.enterText(find.byType(TextField), '김');
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
});
|
|
|
|
testWidgets('searches employees inside selected company subtree', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('한맥').first);
|
|
await tester.pumpAndSettle();
|
|
await tester.enterText(find.byType(TextField), '서준');
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('박서준'), findsOneWidget);
|
|
expect(find.text('김하늘'), findsNothing);
|
|
expect(find.text('검색 결과 1명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('applies local name filter even when API returns broad results', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(
|
|
hasStoredSession: true,
|
|
department: '통합시스템',
|
|
),
|
|
directoryRepository: const _BroadSearchDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(find.byType(TextField), '한');
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('한승민'), findsOneWidget);
|
|
expect(find.text('김윤재'), findsNothing);
|
|
expect(find.text('권오재'), findsNothing);
|
|
});
|
|
|
|
testWidgets('keeps current scope list when phone query is under minimum', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.enterText(find.byType(TextField), '010');
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('전화번호는 010 제외 2자리 이상 입력'), findsOneWidget);
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('검색 결과 2명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('searches only within selected tenant scope', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('TDC'));
|
|
await tester.pumpAndSettle();
|
|
await tester.enterText(find.byType(TextField), '김');
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('김하늘'), findsNothing);
|
|
expect(find.text('검색 결과 0명'), 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('starts hierarchy drilldown from all chip', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('가족사 2개'), findsOneWidget);
|
|
expect(find.text('한맥'), findsWidgets);
|
|
expect(find.text('TDC'), findsWidgets);
|
|
expect(find.text('김하늘'), findsNothing);
|
|
});
|
|
|
|
testWidgets('does not load employees while showing child organizations', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final directoryRepository = _CountingDirectoryRepository();
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
directoryRepository: directoryRepository,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
final callsBeforeHierarchy = directoryRepository.calls;
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('가족사 2개'), findsOneWidget);
|
|
expect(directoryRepository.calls, callsBeforeHierarchy);
|
|
});
|
|
|
|
testWidgets(
|
|
'keeps pinned team chip visible after selecting another company',
|
|
(WidgetTester tester) async {
|
|
await _openDirectory(tester);
|
|
|
|
expect(find.widgetWithText(FilterChip, '기술연구소'), findsWidgets);
|
|
|
|
await tester.tap(find.text('TDC'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.widgetWithText(FilterChip, '기술연구소'), findsWidgets);
|
|
expect(find.text('이도윤'), findsOneWidget);
|
|
expect(find.text('정수빈'), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
testWidgets('shows pinned team chip before all chip', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
final chips = tester
|
|
.widgetList<FilterChip>(find.byType(FilterChip))
|
|
.toList();
|
|
final labels = chips
|
|
.map((chip) => (chip.label as Text).data ?? '')
|
|
.where((label) => label.isNotEmpty)
|
|
.toList();
|
|
|
|
final pinnedIndex = labels.indexOf('기술연구소');
|
|
final allIndex = labels.indexOf('전체');
|
|
|
|
expect(pinnedIndex, isNonNegative);
|
|
expect(allIndex, isNonNegative);
|
|
expect(pinnedIndex, lessThan(allIndex));
|
|
});
|
|
|
|
testWidgets('shows horizontal scrollbar for tenant chips', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
expect(find.byType(Scrollbar), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('uses emphasized style for selected tenant chip', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
final chip = tester.widget<FilterChip>(
|
|
find.widgetWithText(FilterChip, '기술연구소').first,
|
|
);
|
|
|
|
expect(chip.selected, isTrue);
|
|
expect(chip.showCheckmark, isFalse);
|
|
expect(chip.selectedColor, isNotNull);
|
|
});
|
|
|
|
testWidgets('selects pinned team chip on initial entry when available', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
final pinnedChip = tester.widget<FilterChip>(
|
|
find.widgetWithText(FilterChip, '기술연구소').first,
|
|
);
|
|
final companyChip = tester.widget<FilterChip>(
|
|
find.widgetWithText(FilterChip, '한맥').first,
|
|
);
|
|
|
|
expect(pinnedChip.selected, isTrue);
|
|
expect(companyChip.selected, isFalse);
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
expect(find.text('검색 결과 2명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'resolves initial team from org chart when session scope is empty',
|
|
(WidgetTester tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(
|
|
hasStoredSession: true,
|
|
tenantSlug: '',
|
|
department: '',
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final pinnedChip = tester.widget<FilterChip>(
|
|
find.widgetWithText(FilterChip, '기술연구소').first,
|
|
);
|
|
final allChip = tester.widget<FilterChip>(
|
|
find.widgetWithText(FilterChip, '전체').first,
|
|
);
|
|
|
|
expect(pinnedChip.selected, isTrue);
|
|
expect(allChip.selected, isFalse);
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
expect(find.text('검색 결과 2명'), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'shows synthetic pinned team chip before all chip when tenant node is missing',
|
|
(WidgetTester tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(
|
|
hasStoredSession: true,
|
|
department: 'IS3',
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final chips = tester
|
|
.widgetList<FilterChip>(find.byType(FilterChip))
|
|
.toList();
|
|
final labels = chips
|
|
.map((chip) => (chip.label as Text).data ?? '')
|
|
.where((label) => label.isNotEmpty)
|
|
.toList();
|
|
|
|
final pinnedIndex = labels.indexOf('IS3');
|
|
final allIndex = labels.indexOf('전체');
|
|
|
|
expect(pinnedIndex, isNonNegative);
|
|
expect(allIndex, isNonNegative);
|
|
expect(pinnedIndex, lessThan(allIndex));
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'does not duplicate affiliation text when tenant and department match',
|
|
(WidgetTester tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(
|
|
hasStoredSession: true,
|
|
department: 'IS3',
|
|
),
|
|
directoryRepository: const _DuplicateAffiliationDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('IS3 · +821012345678'), findsOneWidget);
|
|
expect(find.text('IS3 · IS3 · +821012345678'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('drills down from all to company to department to employees', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('한맥').first);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('ICT사업부'), findsWidgets);
|
|
expect(find.text('기술연구소'), findsWidgets);
|
|
|
|
await tester.tap(find.text('기술연구소').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('박서준'), findsNothing);
|
|
});
|
|
|
|
testWidgets('shows direct employees for non-leaf organization separately', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
directoryRepository: const _DirectAffiliationDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('한맥').first);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('한맥 조직 관리자'), findsOneWidget);
|
|
expect(find.text('이영희'), findsOneWidget);
|
|
expect(find.text('김하늘'), findsNothing);
|
|
expect(
|
|
tester.getTopLeft(find.text('한맥 조직 관리자')).dy,
|
|
lessThan(tester.getTopLeft(find.text('ICT사업부').last).dy),
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'excludes root employees that also belong to child organizations from direct employees',
|
|
(WidgetTester tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
directoryRepository: const _OverlappingMembershipDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('한맥').first);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('한맥 조직 관리자'), findsOneWidget);
|
|
expect(find.text('이영희'), findsOneWidget);
|
|
expect(find.text('김하늘'), findsNothing);
|
|
expect(find.text('박서준'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('shows divider between top chips and breadcrumb chips', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('한맥').first);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Divider), findsAtLeastNWidgets(1));
|
|
});
|
|
|
|
testWidgets('shows emphasized icon on selected breadcrumb chip', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('한맥').first);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('기술연구소').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byIcon(Icons.check_circle), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('sorts organization employees by manager then grade then name', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
directoryRepository: const _OrganizationSortingDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('기술연구소').last);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('인프라 BIM3'), findsOneWidget);
|
|
expect(find.text('미지정'), findsOneWidget);
|
|
|
|
final leaderY = tester.getTopLeft(find.text('김지영')).dy;
|
|
final directorY = tester.getTopLeft(find.text('박도현')).dy;
|
|
final principalY = tester.getTopLeft(find.text('김성엽')).dy;
|
|
final seniorY = tester.getTopLeft(find.text('한반석')).dy;
|
|
final staffY = tester.getTopLeft(find.text('유서진')).dy;
|
|
|
|
expect(leaderY, lessThan(principalY));
|
|
expect(principalY, lessThan(directorY));
|
|
expect(directorY, lessThan(seniorY));
|
|
expect(seniorY, lessThan(staffY));
|
|
});
|
|
|
|
testWidgets('returns to employee list after switching organization then employees', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.text('전체'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('조직도'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('가족사 2개'), findsOneWidget);
|
|
expect(find.text('한맥'), findsWidgets);
|
|
|
|
await tester.tap(find.text('직원'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.byType(Card), findsWidgets);
|
|
});
|
|
|
|
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('toggles favorite employees', (WidgetTester tester) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.tap(find.byTooltip('즐겨찾기 추가').first);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('즐겨찾기'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('김하늘'), findsOneWidget);
|
|
expect(find.text('최민준'), findsOneWidget);
|
|
expect(find.text('검색 결과 2명'), 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);
|
|
});
|
|
|
|
testWidgets('loads enriched employee detail in bottom sheet', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
employeeDetailRepository: const _EnrichedEmployeeDetailRepository(),
|
|
);
|
|
|
|
await tester.tap(find.text('김하늘'));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('kim.haneul@corp.example.com'), findsOneWidget);
|
|
expect(find.text('보안 아키텍트'), findsOneWidget);
|
|
expect(find.text('한맥, 기술연구소'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('falls back to list data when employee detail load fails', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
employeeDetailRepository: const _FailingEmployeeDetailRepository(),
|
|
);
|
|
|
|
await tester.tap(find.text('김하늘'));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('haneul.kim@example.com'), findsOneWidget);
|
|
expect(find.text('Flutter 개발'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('disables contact actions when employee has no phone number', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.enterText(find.byType(TextField), '최민준');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('최민준').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester
|
|
.widget<FilledButton>(find.widgetWithText(FilledButton, '전화'))
|
|
.onPressed,
|
|
isNull,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<OutlinedButton>(find.widgetWithText(OutlinedButton, '문자'))
|
|
.onPressed,
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
testWidgets('honors employee detail action permissions', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
employeeDetailRepository: const _ActionRestrictedEmployeeDetailRepository(),
|
|
);
|
|
|
|
await tester.tap(find.text('김하늘'));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester
|
|
.widget<FilledButton>(find.widgetWithText(FilledButton, '전화'))
|
|
.onPressed,
|
|
isNull,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<OutlinedButton>(find.widgetWithText(OutlinedButton, '문자'))
|
|
.onPressed,
|
|
isNotNull,
|
|
);
|
|
});
|
|
|
|
testWidgets('toggles favorite from employee detail bottom sheet', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _openDirectory(tester);
|
|
|
|
await tester.enterText(find.byType(TextField), '최민준');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('최민준').last);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byTooltip('즐겨찾기 추가').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('즐겨찾기'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('최민준').last, findsOneWidget);
|
|
});
|
|
|
|
testWidgets('routes call and sms actions through contact launcher', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final launcher = _FakeContactLauncher();
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
launcher: launcher,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('김하늘'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('전화'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('문자'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(launcher.calledNumbers, ['call:+821012345678', 'sms:+821012345678']);
|
|
});
|
|
|
|
testWidgets('returns to login when directory API requires relogin', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final repository = _FakeAuthRepository(hasStoredSession: true);
|
|
await _pumpApp(
|
|
tester,
|
|
repository: repository,
|
|
directoryRepository: const _UnauthorizedDirectoryRepository(),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('다시 로그인'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('다시 로그인'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(repository.logoutCalled, isTrue);
|
|
expect(find.text('로그인 링크 발송'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('logs out from directory app bar', (WidgetTester tester) async {
|
|
final repository = _FakeAuthRepository(hasStoredSession: true);
|
|
await _pumpApp(tester, repository: repository);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byTooltip('로그아웃'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(repository.logoutCalled, isTrue);
|
|
expect(find.text('로그인 링크 발송'), findsOneWidget);
|
|
});
|
|
}
|
|
|
|
Future<void> _openDirectory(WidgetTester tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
repository: _FakeAuthRepository(hasStoredSession: true),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
if (find.text('직원검색').evaluate().isNotEmpty) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
Future<void> _pumpApp(
|
|
WidgetTester tester, {
|
|
AuthRepository? repository,
|
|
DirectoryRepository? directoryRepository,
|
|
EmployeeDetailRepository? employeeDetailRepository,
|
|
OrganizationRepository? organizationRepository,
|
|
ContactLauncher? launcher,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
authRepositoryProvider.overrideWithValue(
|
|
repository ?? _FakeAuthRepository(),
|
|
),
|
|
directoryRepositoryProvider.overrideWithValue(
|
|
directoryRepository ?? const _FakeDirectoryRepository(),
|
|
),
|
|
employeeDetailRepositoryProvider.overrideWithValue(
|
|
employeeDetailRepository ?? const _FakeEmployeeDetailRepository(),
|
|
),
|
|
organizationRepositoryProvider.overrideWithValue(
|
|
organizationRepository ?? const _FakeOrganizationRepository(),
|
|
),
|
|
favoritesRepositoryProvider.overrideWithValue(
|
|
_FakeFavoritesRepository(),
|
|
),
|
|
contactLauncherProvider.overrideWithValue(
|
|
launcher ?? _FakeContactLauncher(),
|
|
),
|
|
],
|
|
child: const Tdc114PlusApp(),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
class _FakeAuthRepository implements AuthRepository {
|
|
_FakeAuthRepository({
|
|
this.hasStoredSession = false,
|
|
this.tenantSlug = 'hanmac',
|
|
this.department = '기술연구소',
|
|
this.pollMode = _FakePollMode.completeImmediately,
|
|
this.initialPendingLink,
|
|
});
|
|
|
|
final bool hasStoredSession;
|
|
final String tenantSlug;
|
|
final String department;
|
|
final _FakePollMode pollMode;
|
|
final PhoneLoginLinkInitResponse? initialPendingLink;
|
|
StoredAuthSession? _storedSession;
|
|
PhoneLoginLinkInitResponse? _pendingLink;
|
|
var _pendingCleared = false;
|
|
final List<String> polledRefs = [];
|
|
var logoutCalled = false;
|
|
|
|
Future<PhoneLoginResponse> phoneLogin(String phoneNumber) async {
|
|
final response = PhoneLoginResponse(
|
|
status: 'ok',
|
|
token: 'session-token',
|
|
expiresAt: DateTime.parse('2026-07-02T12:00:00Z'),
|
|
user: LoginUser(
|
|
id: 'user-uuid',
|
|
name: 'User One',
|
|
phoneNumber: '+821012345678',
|
|
tenantId: 'tenant-uuid',
|
|
tenantName: 'Hanmac',
|
|
tenantSlug: tenantSlug,
|
|
department: department,
|
|
),
|
|
);
|
|
_storedSession = StoredAuthSession(
|
|
token: response.token,
|
|
expiresAt: response.expiresAt,
|
|
user: response.user,
|
|
);
|
|
return response;
|
|
}
|
|
|
|
@override
|
|
Future<PhoneLoginLinkInitResponse> requestPhoneLoginLink(
|
|
String phoneNumber,
|
|
) async {
|
|
_pendingCleared = false;
|
|
_pendingLink = const PhoneLoginLinkInitResponse(
|
|
status: 'pending',
|
|
pendingRef: 'pending-ref',
|
|
expiresIn: 180,
|
|
interval: 1,
|
|
resendAfter: 30,
|
|
);
|
|
return _pendingLink!;
|
|
}
|
|
|
|
@override
|
|
Future<PhoneLoginLinkPollResponse> pollPhoneLoginLink(
|
|
String pendingRef,
|
|
) async {
|
|
polledRefs.add(pendingRef);
|
|
if (pollMode == _FakePollMode.pendingOnly) {
|
|
return const PhoneLoginLinkPollResponse(
|
|
status: 'pending',
|
|
code: 'authorization_pending',
|
|
interval: 1,
|
|
);
|
|
}
|
|
return PhoneLoginLinkPollResponse(
|
|
status: 'ok',
|
|
session: await phoneLogin('01012345678'),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<PhoneLoginLinkInitResponse?> loadPendingPhoneLoginLink() async {
|
|
if (_pendingCleared) {
|
|
return null;
|
|
}
|
|
return _pendingLink ?? initialPendingLink;
|
|
}
|
|
|
|
@override
|
|
Future<void> clearPendingPhoneLoginLink() async {
|
|
_pendingLink = null;
|
|
_pendingCleared = true;
|
|
}
|
|
|
|
@override
|
|
Future<StoredAuthSession?> loadSession() async {
|
|
if (_storedSession != null) {
|
|
return _storedSession;
|
|
}
|
|
if (!hasStoredSession) {
|
|
return null;
|
|
}
|
|
return StoredAuthSession(
|
|
token: 'session-token',
|
|
expiresAt: DateTime.now().toUtc().add(const Duration(hours: 1)),
|
|
user: LoginUser(
|
|
id: 'user-uuid',
|
|
name: 'User One',
|
|
phoneNumber: '+821012345678',
|
|
tenantId: 'tenant-uuid',
|
|
tenantName: 'Hanmac',
|
|
tenantSlug: tenantSlug,
|
|
department: department,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> logout() async {
|
|
logoutCalled = true;
|
|
_pendingLink = null;
|
|
_pendingCleared = true;
|
|
}
|
|
}
|
|
|
|
enum _FakePollMode { completeImmediately, pendingOnly }
|
|
|
|
class _FakeDirectoryRepository implements DirectoryRepository {
|
|
const _FakeDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
return _filteredEmployees(query);
|
|
}
|
|
|
|
List<Employee> _filteredEmployees(DirectoryQuery query) {
|
|
final normalizedQuery = query.query.replaceAll('-', '').toLowerCase();
|
|
final selectedTenant = _findTenantBySlug(query.tenantSlug);
|
|
final tenantSlugs = query.apiTenantSlugs;
|
|
return mockEmployees.where((employee) {
|
|
final matchesTenant =
|
|
tenantSlugs == null ||
|
|
tenantSlugs.contains(employee.tenantSlug) ||
|
|
(selectedTenant != null &&
|
|
selectedTenant.type != 'COMPANY' &&
|
|
(employee.department ?? '') == selectedTenant.name);
|
|
final matchesDepartment =
|
|
query.department == null || employee.department == query.department;
|
|
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);
|
|
return matchesTenant && matchesDepartment && matchesQuery;
|
|
}).toList();
|
|
}
|
|
|
|
TenantSummary? _findTenantBySlug(String slug) {
|
|
for (final tenant in mockTenants) {
|
|
if (tenant.slug == slug) {
|
|
return tenant;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class _FakeEmployeeDetailRepository implements EmployeeDetailRepository {
|
|
const _FakeEmployeeDetailRepository();
|
|
|
|
@override
|
|
Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
|
|
final employee = mockEmployees.firstWhere((item) => item.id == employeeId);
|
|
return EmployeeDetail(
|
|
id: employee.id,
|
|
name: employee.name,
|
|
phoneNumber: employee.phoneNumber,
|
|
phoneDisplay: employee.phoneDisplay,
|
|
email: employee.email,
|
|
tenantId: employee.tenantId,
|
|
tenantName: employee.tenantName,
|
|
tenantSlug: employee.tenantSlug,
|
|
department: employee.department,
|
|
grade: employee.grade,
|
|
position: employee.position,
|
|
jobTitle: employee.jobTitle,
|
|
status: employee.status,
|
|
profileImageUrl: employee.profileImageUrl,
|
|
sortOrder: employee.sortOrder,
|
|
isManager: employee.isManager,
|
|
joinedTenants: [
|
|
TenantRef(
|
|
id: employee.tenantId,
|
|
name: employee.tenantName,
|
|
slug: employee.tenantSlug,
|
|
type: 'COMPANY',
|
|
),
|
|
],
|
|
actions: const EmployeeActions(call: true, sms: true, email: true),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EnrichedEmployeeDetailRepository implements EmployeeDetailRepository {
|
|
const _EnrichedEmployeeDetailRepository();
|
|
|
|
@override
|
|
Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
|
|
return const EmployeeDetail(
|
|
id: 'user-001',
|
|
name: '김하늘',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '010-1234-5678',
|
|
email: 'kim.haneul@corp.example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'hanmac',
|
|
department: '기술연구소',
|
|
grade: '책임',
|
|
position: '팀장',
|
|
jobTitle: '보안 아키텍트',
|
|
status: 'active',
|
|
joinedTenants: [
|
|
TenantRef(
|
|
id: 'tenant-hanmac',
|
|
name: '한맥',
|
|
slug: 'hanmac',
|
|
type: 'COMPANY',
|
|
),
|
|
TenantRef(
|
|
id: 'tenant-rnd',
|
|
name: '기술연구소',
|
|
slug: 'rnd',
|
|
type: 'ORGANIZATION',
|
|
parentId: 'tenant-hanmac',
|
|
),
|
|
],
|
|
actions: EmployeeActions(call: true, sms: true, email: true),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FailingEmployeeDetailRepository implements EmployeeDetailRepository {
|
|
const _FailingEmployeeDetailRepository();
|
|
|
|
@override
|
|
Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
|
|
throw const ApiException(
|
|
statusCode: 500,
|
|
apiError: ApiError(
|
|
error: 'detail_failed',
|
|
code: 'detail_failed',
|
|
details: {},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ActionRestrictedEmployeeDetailRepository
|
|
implements EmployeeDetailRepository {
|
|
const _ActionRestrictedEmployeeDetailRepository();
|
|
|
|
@override
|
|
Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
|
|
return const EmployeeDetail(
|
|
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',
|
|
joinedTenants: [],
|
|
actions: EmployeeActions(call: false, sms: true, email: true),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CountingDirectoryRepository implements DirectoryRepository {
|
|
var calls = 0;
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
calls += 1;
|
|
return const _FakeDirectoryRepository().loadEmployees(query);
|
|
}
|
|
}
|
|
|
|
class _FakeOrganizationRepository implements OrganizationRepository {
|
|
const _FakeOrganizationRepository();
|
|
|
|
@override
|
|
Future<OrgChartSnapshot> getOrgChart({
|
|
String? tenantId,
|
|
bool refresh = false,
|
|
}) async {
|
|
return OrgChartSnapshot(
|
|
tenants: mockTenants,
|
|
employees: mockEmployees,
|
|
generatedAt: DateTime.parse('2026-07-02T00:00:00Z'),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<TenantListResponse> listTenants() async {
|
|
return TenantListResponse(
|
|
items: mockTenants,
|
|
generatedAt: DateTime.parse('2026-07-02T00:00:00Z'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FakeFavoritesRepository implements FavoritesRepository {
|
|
_FakeFavoritesRepository()
|
|
: _favorites = [
|
|
FavoriteEmployee(
|
|
employeeId: 'user-001',
|
|
createdAt: DateTime.parse('2026-07-02T00:00:00Z'),
|
|
),
|
|
];
|
|
|
|
final List<FavoriteEmployee> _favorites;
|
|
|
|
@override
|
|
Future<List<FavoriteEmployee>> loadFavorites() async {
|
|
return List.unmodifiable(_favorites);
|
|
}
|
|
|
|
@override
|
|
Future<void> toggleFavorite(String employeeId) async {
|
|
final existingIndex = _favorites.indexWhere(
|
|
(favorite) => favorite.employeeId == employeeId,
|
|
);
|
|
if (existingIndex >= 0) {
|
|
_favorites.removeAt(existingIndex);
|
|
return;
|
|
}
|
|
|
|
_favorites.add(
|
|
FavoriteEmployee(employeeId: employeeId, createdAt: DateTime.now()),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _UnauthorizedDirectoryRepository implements DirectoryRepository {
|
|
const _UnauthorizedDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
throw const ApiException(
|
|
statusCode: 401,
|
|
apiError: ApiError(
|
|
error: 'unauthorized',
|
|
code: 'unauthorized',
|
|
details: {},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DuplicateAffiliationDirectoryRepository implements DirectoryRepository {
|
|
const _DuplicateAffiliationDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
return const [
|
|
Employee(
|
|
id: 'is3-user',
|
|
name: '송대일',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '+821012345678',
|
|
tenantId: 'tenant-is3',
|
|
tenantName: 'IS3',
|
|
tenantSlug: 'is3',
|
|
department: 'IS3',
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _OrganizationScopedDirectoryRepository implements DirectoryRepository {
|
|
const _OrganizationScopedDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
if (query.tenantSlug == 'hanmac' && query.query == '+821012345678') {
|
|
return const [
|
|
Employee(
|
|
id: 'user-002',
|
|
name: '박서준',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '010-1234-5678',
|
|
email: 'seojun.park@example.com',
|
|
tenantId: 'tenant-ict',
|
|
tenantName: 'ICT사업부',
|
|
tenantSlug: 'ict',
|
|
department: 'ICT사업부',
|
|
position: '매니저',
|
|
),
|
|
];
|
|
}
|
|
|
|
if (query.tenantSlug == 'ict') {
|
|
return const [
|
|
Employee(
|
|
id: 'user-002',
|
|
name: '박서준',
|
|
phoneNumber: '+821023456789',
|
|
phoneDisplay: '010-2345-6789',
|
|
email: 'seojun.park@example.com',
|
|
tenantId: 'tenant-ict',
|
|
tenantName: 'ICT사업부',
|
|
tenantSlug: 'ict',
|
|
department: 'ICT사업부',
|
|
grade: '선임',
|
|
position: '매니저',
|
|
),
|
|
];
|
|
}
|
|
|
|
if (query.tenantSlug == 'rnd') {
|
|
return const [
|
|
Employee(
|
|
id: 'user-001',
|
|
name: '김하늘',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '010-1234-5678',
|
|
email: 'haneul.kim@example.com',
|
|
tenantId: 'tenant-rnd',
|
|
tenantName: '기술연구소',
|
|
tenantSlug: 'rnd',
|
|
department: '기술연구소',
|
|
grade: '책임',
|
|
position: '팀장',
|
|
),
|
|
Employee(
|
|
id: 'user-005',
|
|
name: '최민준',
|
|
phoneNumber: '',
|
|
phoneDisplay: null,
|
|
email: 'minjun.choi@example.com',
|
|
tenantId: 'tenant-rnd',
|
|
tenantName: '기술연구소',
|
|
tenantSlug: 'rnd',
|
|
department: '기술연구소',
|
|
grade: '수석',
|
|
position: '연구원',
|
|
),
|
|
];
|
|
}
|
|
|
|
if (query.tenantSlug == 'ict' && query.query.isEmpty) {
|
|
return const [
|
|
Employee(
|
|
id: 'user-002',
|
|
name: '박서준',
|
|
phoneNumber: '+821023456789',
|
|
phoneDisplay: '010-2345-6789',
|
|
email: 'seojun.park@example.com',
|
|
tenantId: 'tenant-ict',
|
|
tenantName: 'ICT사업부',
|
|
tenantSlug: 'ict',
|
|
department: 'ICT사업부',
|
|
grade: '선임',
|
|
position: '매니저',
|
|
jobTitle: 'API 연동',
|
|
status: 'active',
|
|
sortOrder: 20,
|
|
),
|
|
];
|
|
}
|
|
|
|
return const _FakeDirectoryRepository().loadEmployees(query);
|
|
}
|
|
}
|
|
|
|
class _OrganizationSortingDirectoryRepository implements DirectoryRepository {
|
|
const _OrganizationSortingDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
if (query.tenantSlug != 'rnd') {
|
|
return const _FakeDirectoryRepository().loadEmployees(query);
|
|
}
|
|
return const [
|
|
Employee(
|
|
id: 'user-b',
|
|
name: '유서진',
|
|
phoneNumber: '+821011111113',
|
|
phoneDisplay: '010-1111-1113',
|
|
email: 'user3@example.com',
|
|
tenantId: 'tenant-infra-bim3',
|
|
tenantName: '인프라 BIM3',
|
|
tenantSlug: 'rnd',
|
|
department: '미지정',
|
|
grade: '사원',
|
|
position: '연구원',
|
|
),
|
|
Employee(
|
|
id: 'user-director',
|
|
name: '박도현',
|
|
phoneNumber: '+821011111115',
|
|
phoneDisplay: '010-1111-1115',
|
|
email: 'director@example.com',
|
|
tenantId: 'tenant-infra-bim3',
|
|
tenantName: '인프라 BIM3',
|
|
tenantSlug: 'rnd',
|
|
department: '미지정',
|
|
grade: '이사',
|
|
position: '임원',
|
|
),
|
|
Employee(
|
|
id: 'user-a',
|
|
name: '김성엽',
|
|
phoneNumber: '+821011111112',
|
|
phoneDisplay: '010-1111-1112',
|
|
email: 'user2@example.com',
|
|
tenantId: 'tenant-infra-bim3',
|
|
tenantName: '인프라 BIM3',
|
|
tenantSlug: 'rnd',
|
|
department: '미지정',
|
|
grade: '수석연구원',
|
|
position: '연구원',
|
|
),
|
|
Employee(
|
|
id: 'user-leader',
|
|
name: '김지영',
|
|
phoneNumber: '+821011111111',
|
|
phoneDisplay: '010-1111-1111',
|
|
email: 'leader@example.com',
|
|
tenantId: 'tenant-infra-bim3',
|
|
tenantName: '인프라 BIM3',
|
|
tenantSlug: 'rnd',
|
|
department: '미지정',
|
|
grade: '책임연구원',
|
|
position: '팀장',
|
|
isManager: true,
|
|
),
|
|
Employee(
|
|
id: 'user-c',
|
|
name: '한반석',
|
|
phoneNumber: '+821011111114',
|
|
phoneDisplay: '010-1111-1114',
|
|
email: 'user4@example.com',
|
|
tenantId: 'tenant-infra-bim3',
|
|
tenantName: '인프라 BIM3',
|
|
tenantSlug: 'rnd',
|
|
department: '미지정',
|
|
grade: '선임연구원',
|
|
position: '연구원',
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _DirectAffiliationDirectoryRepository implements DirectoryRepository {
|
|
const _DirectAffiliationDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
if (query.tenantSlug != 'hanmac') {
|
|
return const _FakeDirectoryRepository().loadEmployees(query);
|
|
}
|
|
return const [
|
|
Employee(
|
|
id: 'direct-manager',
|
|
name: '이영희',
|
|
phoneNumber: '+821011112222',
|
|
phoneDisplay: '010-1111-2222',
|
|
email: 'younghee.lee@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'hanmac',
|
|
grade: '이사',
|
|
position: '디비전장',
|
|
isManager: true,
|
|
),
|
|
Employee(
|
|
id: 'child-member',
|
|
name: '김하늘',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '010-1234-5678',
|
|
email: 'haneul.kim@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'hanmac',
|
|
department: '기술연구소',
|
|
grade: '책임',
|
|
position: '팀장',
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _OverlappingMembershipDirectoryRepository implements DirectoryRepository {
|
|
const _OverlappingMembershipDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
if (query.tenantSlug != 'hanmac') {
|
|
return const _FakeDirectoryRepository().loadEmployees(query);
|
|
}
|
|
return const [
|
|
Employee(
|
|
id: 'root-only-exec',
|
|
name: '이영희',
|
|
phoneNumber: '+821011112222',
|
|
phoneDisplay: '010-1111-2222',
|
|
email: 'younghee.lee@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'hanmac',
|
|
grade: '이사',
|
|
position: '디비전장',
|
|
isManager: true,
|
|
),
|
|
Employee(
|
|
id: 'child-member-1',
|
|
name: '김하늘',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '010-1234-5678',
|
|
email: 'haneul.kim@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'hanmac',
|
|
grade: '책임',
|
|
position: '팀장',
|
|
),
|
|
Employee(
|
|
id: 'child-member-1',
|
|
name: '김하늘',
|
|
phoneNumber: '+821012345678',
|
|
phoneDisplay: '010-1234-5678',
|
|
email: 'haneul.kim@example.com',
|
|
tenantId: 'tenant-rnd',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'rnd',
|
|
department: '기술연구소',
|
|
grade: '책임',
|
|
position: '팀장',
|
|
),
|
|
Employee(
|
|
id: 'child-member-2',
|
|
name: '박서준',
|
|
phoneNumber: '+821023456789',
|
|
phoneDisplay: '010-2345-6789',
|
|
email: 'seojun.park@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'hanmac',
|
|
grade: '선임',
|
|
position: '매니저',
|
|
),
|
|
Employee(
|
|
id: 'child-member-2',
|
|
name: '박서준',
|
|
phoneNumber: '+821023456789',
|
|
phoneDisplay: '010-2345-6789',
|
|
email: 'seojun.park@example.com',
|
|
tenantId: 'tenant-ict',
|
|
tenantName: '한맥',
|
|
tenantSlug: 'ict',
|
|
department: 'ICT사업부',
|
|
grade: '선임',
|
|
position: '매니저',
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _BroadSearchDirectoryRepository implements DirectoryRepository {
|
|
const _BroadSearchDirectoryRepository();
|
|
|
|
@override
|
|
Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
|
|
if (query.tenantSlug != 'hanmac' && query.apiDepartment != '통합시스템') {
|
|
return const _FakeDirectoryRepository().loadEmployees(query);
|
|
}
|
|
|
|
return const [
|
|
Employee(
|
|
id: 'broad-001',
|
|
name: '김윤재',
|
|
phoneNumber: '+821097479838',
|
|
phoneDisplay: '010-9747-9838',
|
|
email: 'yoonjae.kim@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '총괄기획&기술개발센터',
|
|
tenantSlug: 'hanmac',
|
|
department: '통합시스템',
|
|
position: 'IS1',
|
|
),
|
|
Employee(
|
|
id: 'broad-002',
|
|
name: '한승민',
|
|
phoneNumber: '+821031891514',
|
|
phoneDisplay: '010-3189-1514',
|
|
email: 'seungmin.han@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '총괄기획&기술개발센터',
|
|
tenantSlug: 'hanmac',
|
|
department: '통합시스템',
|
|
position: 'IS1',
|
|
),
|
|
Employee(
|
|
id: 'broad-003',
|
|
name: '권오재',
|
|
phoneNumber: '+821091143943',
|
|
phoneDisplay: '010-9114-3943',
|
|
email: 'ojae.kwon@example.com',
|
|
tenantId: 'tenant-hanmac',
|
|
tenantName: '총괄기획&기술개발센터',
|
|
tenantSlug: 'hanmac',
|
|
department: '통합시스템',
|
|
position: 'IS2',
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _FakeContactLauncher implements ContactLauncher {
|
|
_FakeContactLauncher();
|
|
|
|
final List<String> calledNumbers = [];
|
|
|
|
@override
|
|
Future<bool> call(String phoneNumber) async {
|
|
calledNumbers.add('call:$phoneNumber');
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> sms(String phoneNumber) async {
|
|
calledNumbers.add('sms:$phoneNumber');
|
|
return true;
|
|
}
|
|
}
|