diff --git a/assets/translations/ko.json b/assets/translations/ko.json index fab8e07..627d596 100644 --- a/assets/translations/ko.json +++ b/assets/translations/ko.json @@ -53,5 +53,12 @@ "isNotDeadlineYetConfirmClose": "아직 마감날이 아닙니다. 마감할까요?", "isUpdateContentCorrect": "수정하신 내용이 맞으신가요?", "canRevertFromPostingManagement": "공고 관리에서 다시 되돌릴 수 있습니다.", - "yearsOld": "세" + "yearsOld": "세", + "showJobPosting": "공고보기", + "workStatus": "출근현황", + "attendance": "출근", + "leaveWork": "퇴근", + "attendanceScheduled": "출근예정", + "attendanceCompleted": "출근완료", + "noApplicants": "지원자가 없습니다." } diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index b1f45e6..78a18d3 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -25,5 +25,9 @@ class AppRouter extends RootStackRouter { page: JobPostingDetailRoute.page, path: '/job-posting-detail', ), + AutoRoute( + page: JobPostingWorkersRoute.page, + path: '/job-posting-workers', + ), ]; } diff --git a/lib/core/router/router.gr.dart b/lib/core/router/router.gr.dart index a32d9a7..72cc2e7 100644 --- a/lib/core/router/router.gr.dart +++ b/lib/core/router/router.gr.dart @@ -8,25 +8,27 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i5; -import 'package:flutter/material.dart' as _i6; +import 'package:auto_route/auto_route.dart' as _i6; +import 'package:flutter/material.dart' as _i7; import 'package:withu_app/feature/job_posting/presentation/pages/job_posting_detail_page.dart' as _i1; import 'package:withu_app/feature/job_posting/presentation/pages/job_posting_form_page.dart' as _i2; -import 'package:withu_app/feature/job_posting/presentation/pages/job_postings_page.dart' +import 'package:withu_app/feature/job_posting/presentation/pages/job_posting_workers_page.dart' as _i3; -import 'package:withu_app/feature/splash/presentation/pages/splash_page.dart' +import 'package:withu_app/feature/job_posting/presentation/pages/job_postings_page.dart' as _i4; +import 'package:withu_app/feature/splash/presentation/pages/splash_page.dart' + as _i5; /// generated route for /// [_i1.JobPostingDetailPage] class JobPostingDetailRoute - extends _i5.PageRouteInfo { + extends _i6.PageRouteInfo { JobPostingDetailRoute({ - _i6.Key? key, + _i7.Key? key, required String jobPostingId, - List<_i5.PageRouteInfo>? children, + List<_i6.PageRouteInfo>? children, }) : super( JobPostingDetailRoute.name, args: JobPostingDetailRouteArgs( @@ -38,7 +40,7 @@ class JobPostingDetailRoute static const String name = 'JobPostingDetailRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -56,7 +58,7 @@ class JobPostingDetailRouteArgs { required this.jobPostingId, }); - final _i6.Key? key; + final _i7.Key? key; final String jobPostingId; @@ -68,11 +70,11 @@ class JobPostingDetailRouteArgs { /// generated route for /// [_i2.JobPostingFormPage] -class JobPostingFormRoute extends _i5.PageRouteInfo { +class JobPostingFormRoute extends _i6.PageRouteInfo { JobPostingFormRoute({ - _i6.Key? key, + _i7.Key? key, String? jobPostingId, - List<_i5.PageRouteInfo>? children, + List<_i6.PageRouteInfo>? children, }) : super( JobPostingFormRoute.name, args: JobPostingFormRouteArgs( @@ -84,7 +86,7 @@ class JobPostingFormRoute extends _i5.PageRouteInfo { static const String name = 'JobPostingFormRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -103,7 +105,7 @@ class JobPostingFormRouteArgs { this.jobPostingId, }); - final _i6.Key? key; + final _i7.Key? key; final String? jobPostingId; @@ -114,9 +116,56 @@ class JobPostingFormRouteArgs { } /// generated route for -/// [_i3.JobPostingsPage] -class JobPostingsRoute extends _i5.PageRouteInfo { - const JobPostingsRoute({List<_i5.PageRouteInfo>? children}) +/// [_i3.JobPostingWorkersPage] +class JobPostingWorkersRoute + extends _i6.PageRouteInfo { + JobPostingWorkersRoute({ + _i7.Key? key, + required String jobPostingId, + List<_i6.PageRouteInfo>? children, + }) : super( + JobPostingWorkersRoute.name, + args: JobPostingWorkersRouteArgs( + key: key, + jobPostingId: jobPostingId, + ), + initialChildren: children, + ); + + static const String name = 'JobPostingWorkersRoute'; + + static _i6.PageInfo page = _i6.PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return _i3.JobPostingWorkersPage( + key: args.key, + jobPostingId: args.jobPostingId, + ); + }, + ); +} + +class JobPostingWorkersRouteArgs { + const JobPostingWorkersRouteArgs({ + this.key, + required this.jobPostingId, + }); + + final _i7.Key? key; + + final String jobPostingId; + + @override + String toString() { + return 'JobPostingWorkersRouteArgs{key: $key, jobPostingId: $jobPostingId}'; + } +} + +/// generated route for +/// [_i4.JobPostingsPage] +class JobPostingsRoute extends _i6.PageRouteInfo { + const JobPostingsRoute({List<_i6.PageRouteInfo>? children}) : super( JobPostingsRoute.name, initialChildren: children, @@ -124,18 +173,18 @@ class JobPostingsRoute extends _i5.PageRouteInfo { static const String name = 'JobPostingsRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { - return const _i3.JobPostingsPage(); + return const _i4.JobPostingsPage(); }, ); } /// generated route for -/// [_i4.SplashPage] -class SplashRoute extends _i5.PageRouteInfo { - const SplashRoute({List<_i5.PageRouteInfo>? children}) +/// [_i5.SplashPage] +class SplashRoute extends _i6.PageRouteInfo { + const SplashRoute({List<_i6.PageRouteInfo>? children}) : super( SplashRoute.name, initialChildren: children, @@ -143,10 +192,10 @@ class SplashRoute extends _i5.PageRouteInfo { static const String name = 'SplashRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { - return const _i4.SplashPage(); + return const _i5.SplashPage(); }, ); } diff --git a/lib/core/utils/resource/string_res.dart b/lib/core/utils/resource/string_res.dart index 6411fb6..7fb3275 100644 --- a/lib/core/utils/resource/string_res.dart +++ b/lib/core/utils/resource/string_res.dart @@ -55,6 +55,13 @@ enum StringRes { isUpdateContentCorrect, canRevertFromPostingManagement, yearsOld, + showJobPosting, + workStatus, + attendance, + leaveWork, + attendanceScheduled, + attendanceCompleted, + noApplicants, } extension StringResEx on StringRes { diff --git a/lib/feature/job_posting/data/data_sources/dto/dto.dart b/lib/feature/job_posting/data/data_sources/dto/dto.dart index 9ad58e0..6e78387 100644 --- a/lib/feature/job_posting/data/data_sources/dto/dto.dart +++ b/lib/feature/job_posting/data/data_sources/dto/dto.dart @@ -3,3 +3,4 @@ export 'list/job_postings_model.dart'; export 'request/job_posting_request_dto.dart'; export 'detail/job_posting_detail_dto.dart'; export 'worker/worker.dart'; +export 'workers/workers.dart'; diff --git a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_worker_dto.mock.dart b/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_worker_dto.mock.dart index 3281c05..6095b49 100644 --- a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_worker_dto.mock.dart +++ b/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_worker_dto.mock.dart @@ -5,7 +5,7 @@ extension JobPostingWorkerDtoExt on JobPostingWorkerDto { required String id, }) { return JobPostingWorkerDto( - id: '', + id: id, workStatus: false, userInfo: SimpleUserDtoExt.mock(id: id), workStartTime: DateTime.now(), diff --git a/lib/feature/job_posting/data/data_sources/dto/worker/worker.dart b/lib/feature/job_posting/data/data_sources/dto/worker/worker.dart index 1939a48..a6c5d87 100644 --- a/lib/feature/job_posting/data/data_sources/dto/worker/worker.dart +++ b/lib/feature/job_posting/data/data_sources/dto/worker/worker.dart @@ -1,2 +1,2 @@ -export 'job_posting_workers_dto.dart'; +export '../workers/job_posting_workers_dto.dart'; export 'job_posting_worker_dto.dart'; diff --git a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.dart b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.dart similarity index 71% rename from lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.dart rename to lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.dart index 6272556..b2e78b3 100644 --- a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.dart +++ b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.dart @@ -1,11 +1,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:withu_app/core/core.dart'; -import 'job_posting_worker_dto.dart'; +import '../worker/job_posting_worker_dto.dart'; part 'job_posting_workers_dto.freezed.dart'; part 'job_posting_workers_dto.g.dart'; +part 'job_posting_workers_dto.mock.dart'; + /// 공고 목록 모델 @freezed class JobPostingWorkersDto with _$JobPostingWorkersDto { @@ -25,16 +27,3 @@ class JobPostingWorkersDto with _$JobPostingWorkersDto { factory JobPostingWorkersDto.fromJson(Map json) => _$JobPostingWorkersDtoFromJson(json); } - -extension JobPostingWorkersDtoExt on JobPostingWorkersDto { - static List mock({ - required int page, - }) { - return List.generate( - 10, - (int index) => JobPostingWorkerDtoExt.mock( - id: '${(page * 10) + index}', - ), - ); - } -} diff --git a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.freezed.dart b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.freezed.dart similarity index 100% rename from lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.freezed.dart rename to lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.freezed.dart diff --git a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.g.dart b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.g.dart similarity index 96% rename from lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.g.dart rename to lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.g.dart index 44fd9e6..55ccc19 100644 --- a/lib/feature/job_posting/data/data_sources/dto/worker/job_posting_workers_dto.g.dart +++ b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'job_posting_workers_dto.dart'; +part of '../workers/job_posting_workers_dto.dart'; // ************************************************************************** // JsonSerializableGenerator diff --git a/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.mock.dart b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.mock.dart new file mode 100644 index 0000000..1bbdc9d --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/workers/job_posting_workers_dto.mock.dart @@ -0,0 +1,27 @@ +part of 'job_posting_workers_dto.dart'; + +extension JobPostingWorkersDtoExt on JobPostingWorkersDto { + static JobPostingWorkersDto mock({ + required int page, + }) { + const int totalElements = 27; + final List workers = List.generate( + (10 * page) > totalElements ? (10 * page) - totalElements : 10, + (int index) => JobPostingWorkerDtoExt.mock( + id: '${(page * 10) + index}', + ), + ); + + return JobPostingWorkersDto( + title: '공고제목!', + participants: 49, + workStartDate: DateTime.now(), + workEndDate: DateTime.now().add(const Duration(days: 4)), + content: workers, + totalPages: 3, + totalElements: totalElements, + size: 10, + number: page, + ); + } +} diff --git a/lib/feature/job_posting/data/data_sources/dto/workers/workers.dart b/lib/feature/job_posting/data/data_sources/dto/workers/workers.dart new file mode 100644 index 0000000..4cf1fe7 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/workers/workers.dart @@ -0,0 +1 @@ +export 'job_posting_workers_dto.dart'; diff --git a/lib/feature/job_posting/data/data_sources/mock/job_posting_mock_api.dart b/lib/feature/job_posting/data/data_sources/mock/job_posting_mock_api.dart index 8edc7f3..9f4d035 100644 --- a/lib/feature/job_posting/data/data_sources/mock/job_posting_mock_api.dart +++ b/lib/feature/job_posting/data/data_sources/mock/job_posting_mock_api.dart @@ -223,7 +223,7 @@ class JobPostingMockApi extends JobPostingApi with MockAPI { path, (server) => server.reply( 200, - JobPostingWorkersDtoExt.mock(page: page), + JobPostingWorkersDtoExt.mock(page: page).toJson(), delay: const Duration(milliseconds: 300), ), ); diff --git a/lib/feature/job_posting/domain/entities/job_posting_workers_entity.dart b/lib/feature/job_posting/domain/entities/job_posting_workers_entity.dart index 4366489..7908005 100644 --- a/lib/feature/job_posting/domain/entities/job_posting_workers_entity.dart +++ b/lib/feature/job_posting/domain/entities/job_posting_workers_entity.dart @@ -6,7 +6,10 @@ class JobPostingWorkersEntity { /// 공고명 final String title; - /// 인원 수 + /// 지원한 수 + final int applicants; + + /// 최대 인원 수 final int participants; /// 근무 시작 일 @@ -20,6 +23,7 @@ class JobPostingWorkersEntity { JobPostingWorkersEntity({ required this.title, + required this.applicants, required this.participants, required this.workStartDate, required this.workEndDate, @@ -31,6 +35,7 @@ extension JobPostingWorkersEntityExt on JobPostingWorkersEntity { static JobPostingWorkersEntity fromDto(JobPostingWorkersDto dto) { return JobPostingWorkersEntity( title: dto.title, + applicants: dto.totalElements, participants: dto.participants, workStartDate: dto.workStartDate, workEndDate: dto.workEndDate, diff --git a/lib/feature/job_posting/init_injections.dart b/lib/feature/job_posting/init_injections.dart index 763c3f8..df21dab 100644 --- a/lib/feature/job_posting/init_injections.dart +++ b/lib/feature/job_posting/init_injections.dart @@ -2,6 +2,7 @@ import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/job_posting/data/data.dart'; import 'package:withu_app/feature/job_posting/domain/domain.dart'; import 'package:withu_app/feature/job_posting/job_posting.dart'; +import 'package:withu_app/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.dart'; initJobPostingInjections() { getIt.registerSingleton( @@ -31,4 +32,7 @@ initJobPostingInjections() { getIt.registerFactory( () => JobPostingDetailBloc(useCase: getIt()), ); + getIt.registerFactory( + () => JobPostingWorkersBloc(useCase: getIt()), + ); } diff --git a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.dart b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.dart index 979874a..469200a 100644 --- a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.dart +++ b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.dart @@ -19,6 +19,7 @@ class JobPostingWorkersBloc status: JobPostingWorkersStatus.initial, message: '', title: '', + applicants: 0, participants: 0, workStartDate: '', workEndDate: '', @@ -27,7 +28,8 @@ class JobPostingWorkersBloc ), ) { on(_onIdStored); - on(_onInitialized); + on(_onSearched); + on(_onMessageCleared); } } @@ -41,8 +43,8 @@ extension JobPostingWorkersBlocHandler on JobPostingWorkersBloc { } /// 초기화 작업 - void _onInitialized( - JobPostingWorkersInitialized event, + void _onSearched( + JobPostingWorkersSearched event, Emitter emit, ) async { final jobPostingId = state.jobPostingId; @@ -56,17 +58,19 @@ extension JobPostingWorkersBlocHandler on JobPostingWorkersBloc { final Either result = await useCase.searchJobPostingWorkers( jobPostingId: jobPostingId, - page: 0, + page: event.page, ); result.when(success: (JobPostingWorkersEntity data) { emit(state.copyWith( status: JobPostingWorkersStatus.loaded, title: data.title, + applicants: data.applicants, participants: data.participants, - workStartDate: data.workStartDate.format('yyyy-MM-dd'), - workEndDate: data.workEndDate.format('yyyy-MM-dd'), + workStartDate: data.workStartDate.format('yy / MM / dd (E)'), + workEndDate: data.workEndDate.format('yy / MM / dd (E)'), list: data.workers, + isLast: data.workers.length < 10, message: '', )); }, fail: (String message) { @@ -76,4 +80,12 @@ extension JobPostingWorkersBlocHandler on JobPostingWorkersBloc { )); }); } + + /// 메시지 초기화 + void _onMessageCleared( + JobPostingWorkersMessageCleared event, + Emitter emit, + ) { + emit(state.copyWith(message: '')); + } } diff --git a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.freezed.dart b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.freezed.dart index 95a514a..cb848de 100644 --- a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.freezed.dart +++ b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.freezed.dart @@ -19,6 +19,7 @@ mixin _$JobPostingWorkersState { JobPostingWorkersStatus get status => throw _privateConstructorUsedError; String get message => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; + int get applicants => throw _privateConstructorUsedError; int get participants => throw _privateConstructorUsedError; String get workStartDate => throw _privateConstructorUsedError; String get workEndDate => throw _privateConstructorUsedError; @@ -43,6 +44,7 @@ abstract class $JobPostingWorkersStateCopyWith<$Res> { {JobPostingWorkersStatus status, String message, String title, + int applicants, int participants, String workStartDate, String workEndDate, @@ -70,6 +72,7 @@ class _$JobPostingWorkersStateCopyWithImpl<$Res, Object? status = null, Object? message = null, Object? title = null, + Object? applicants = null, Object? participants = null, Object? workStartDate = null, Object? workEndDate = null, @@ -90,6 +93,10 @@ class _$JobPostingWorkersStateCopyWithImpl<$Res, ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + applicants: null == applicants + ? _value.applicants + : applicants // ignore: cast_nullable_to_non_nullable + as int, participants: null == participants ? _value.participants : participants // ignore: cast_nullable_to_non_nullable @@ -131,6 +138,7 @@ abstract class _$$JobPostingWorkersStateImplCopyWith<$Res> {JobPostingWorkersStatus status, String message, String title, + int applicants, int participants, String workStartDate, String workEndDate, @@ -157,6 +165,7 @@ class __$$JobPostingWorkersStateImplCopyWithImpl<$Res> Object? status = null, Object? message = null, Object? title = null, + Object? applicants = null, Object? participants = null, Object? workStartDate = null, Object? workEndDate = null, @@ -177,6 +186,10 @@ class __$$JobPostingWorkersStateImplCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + applicants: null == applicants + ? _value.applicants + : applicants // ignore: cast_nullable_to_non_nullable + as int, participants: null == participants ? _value.participants : participants // ignore: cast_nullable_to_non_nullable @@ -212,6 +225,7 @@ class _$JobPostingWorkersStateImpl implements _JobPostingWorkersState { {required this.status, required this.message, required this.title, + required this.applicants, required this.participants, required this.workStartDate, required this.workEndDate, @@ -227,6 +241,8 @@ class _$JobPostingWorkersStateImpl implements _JobPostingWorkersState { @override final String title; @override + final int applicants; + @override final int participants; @override final String workStartDate; @@ -247,7 +263,7 @@ class _$JobPostingWorkersStateImpl implements _JobPostingWorkersState { @override String toString() { - return 'JobPostingWorkersState(status: $status, message: $message, title: $title, participants: $participants, workStartDate: $workStartDate, workEndDate: $workEndDate, list: $list, isLast: $isLast, jobPostingId: $jobPostingId)'; + return 'JobPostingWorkersState(status: $status, message: $message, title: $title, applicants: $applicants, participants: $participants, workStartDate: $workStartDate, workEndDate: $workEndDate, list: $list, isLast: $isLast, jobPostingId: $jobPostingId)'; } @override @@ -258,6 +274,8 @@ class _$JobPostingWorkersStateImpl implements _JobPostingWorkersState { (identical(other.status, status) || other.status == status) && (identical(other.message, message) || other.message == message) && (identical(other.title, title) || other.title == title) && + (identical(other.applicants, applicants) || + other.applicants == applicants) && (identical(other.participants, participants) || other.participants == participants) && (identical(other.workStartDate, workStartDate) || @@ -276,6 +294,7 @@ class _$JobPostingWorkersStateImpl implements _JobPostingWorkersState { status, message, title, + applicants, participants, workStartDate, workEndDate, @@ -298,6 +317,7 @@ abstract class _JobPostingWorkersState implements JobPostingWorkersState { {required final JobPostingWorkersStatus status, required final String message, required final String title, + required final int applicants, required final int participants, required final String workStartDate, required final String workEndDate, @@ -312,6 +332,8 @@ abstract class _JobPostingWorkersState implements JobPostingWorkersState { @override String get title; @override + int get applicants; + @override int get participants; @override String get workStartDate; diff --git a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_event.dart b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_event.dart index ab62398..4dd6e97 100644 --- a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_event.dart +++ b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_event.dart @@ -11,7 +11,11 @@ class JobPostingWorkersIdStored extends JobPostingWorkersEvent { } /// 화면 초기화 -class JobPostingWorkersInitialized extends JobPostingWorkersEvent {} +class JobPostingWorkersSearched extends JobPostingWorkersEvent { + final int page; + + JobPostingWorkersSearched({required this.page}); +} /// 메시지 초기화 class JobPostingWorkersMessageCleared extends JobPostingWorkersEvent {} diff --git a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_state.dart b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_state.dart index 55c762f..b6f9141 100644 --- a/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_state.dart +++ b/lib/feature/job_posting/presentation/blocs/workers/job_posting_workers_state.dart @@ -2,12 +2,17 @@ part of 'job_posting_workers_bloc.dart'; enum JobPostingWorkersStatus { initial, loading, loaded, failure } +extension JobPostingWorkersStatusExt on JobPostingWorkersStatus { + bool get isLoading => this == JobPostingWorkersStatus.loading; +} + @freezed class JobPostingWorkersState with _$JobPostingWorkersState { factory JobPostingWorkersState({ required JobPostingWorkersStatus status, required String message, required String title, + required int applicants, required int participants, required String workStartDate, required String workEndDate, @@ -16,3 +21,16 @@ class JobPostingWorkersState with _$JobPostingWorkersState { String? jobPostingId, }) = _JobPostingWorkersState; } + +extension JobPostingWorkersStateExt on JobPostingWorkersState { + /// 출근현황 + String get workStatus => '$applicants/$participants'; + + /// 근무 기간 + String get workPeriod => _getWorkPeriod(); + + String _getWorkPeriod() { + final isShort = workStartDate == workEndDate; + return isShort ? workStartDate : '$workStartDate - $workEndDate'; + } +} diff --git a/lib/feature/job_posting/presentation/pages/job_posting_workers_page.dart b/lib/feature/job_posting/presentation/pages/job_posting_workers_page.dart new file mode 100644 index 0000000..d9a4cd5 --- /dev/null +++ b/lib/feature/job_posting/presentation/pages/job_posting_workers_page.dart @@ -0,0 +1,231 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/domain/domain.dart'; +import 'package:withu_app/feature/job_posting/presentation/blocs/workers/job_posting_workers_bloc.dart'; +import 'package:withu_app/feature/user/presentation/presentation.dart'; +import 'package:withu_app/shared/shared.dart'; + +import '../widgets/worker_profile/worker_profile.dart'; + +/// 공고 상세 화면 +@RoutePage() +class JobPostingWorkersPage extends StatelessWidget { + final String jobPostingId; + + const JobPostingWorkersPage({ + super.key, + required this.jobPostingId, + }); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt()..add(JobPostingWorkersIdStored(id: jobPostingId))), + ], + child: _JobPostingWorkersPage(), + ); + } +} + +class _JobPostingWorkersPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) async { + if (state.message.isNotEmpty) { + CustomAlertDialog.showContentAlert( + context: context, + content: state.message, + closeCallback: () { + context + .read() + .add(JobPostingWorkersMessageCleared()); + }, + ); + } + }, + builder: (context, state) { + return PageRoot( + isLoading: state.status.isLoading, + safeAreaTop: false, + appBar: CustomAppBar.back( + context: context, + trailing: [ + TextButton( + onPressed: () {}, + child: Text( + StringRes.showJobPosting.tr, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + child: NestedScrollView( + headerSliverBuilder: ( + BuildContext context, + bool innerBoxIsScrolled, + ) { + return [ + _Header( + title: state.title, + workPeriod: state.workPeriod, + workStatus: state.workStatus, + ), + ]; + }, + body: _WorkerList(), + ), + ); + }, + ); + } +} + +/// 헤더 영역 +class _Header extends StatelessWidget { + /// 공고 제목 + final String title; + + /// 근무 기간 + final String workPeriod; + + /// 출근현황 + final String workStatus; + + const _Header({ + required this.title, + required this.workPeriod, + required this.workStatus, + }); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: context.textTheme.headlineMedium, + ), + Text( + workPeriod, + style: context.textTheme.bodyMedium, + ), + ], + ), + const Spacer(), + Text( + '${StringRes.workStatus.tr} $workStatus', + style: context.textTheme.bodyMediumBold, + ), + ], + ) + ], + ), + ), + ); + } +} + +/// List +class _WorkerList extends StatefulWidget { + @override + State createState() => _WorkerListState(); +} + +class _WorkerListState extends State<_WorkerList> { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + void initState() { + _pagingController.addPageRequestListener( + (pageKey) { + context.read().add( + JobPostingWorkersSearched( + page: pageKey, + ), + ); + }, + ); + super.initState(); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.status == JobPostingWorkersStatus.loaded) { + final isLast = state.isLast; + if (isLast) { + _pagingController.appendLastPage(state.list); + } else { + final nextPageKey = (_pagingController.nextPageKey ?? 0) + 1; + _pagingController.appendPage(state.list, nextPageKey); + } + } + }, + child: PagedGridView( + pagingController: _pagingController, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 20, + ), + builderDelegate: PagedChildBuilderDelegate( + firstPageProgressIndicatorBuilder: (context) => _emptyView(), + noItemsFoundIndicatorBuilder: (context) => _emptyView(), + itemBuilder: (context, item, index) { + final userInfo = item.userInfo; + return WorkerProfile( + profile: userInfo.profile, + name: userInfo.name, + age: userInfo.age, + startTime: item.workStartTime?.format('HH:mm') ?? '', + endTime: item.workEndTime?.format('HH:mm') ?? '', + workStatus: item.workStatus, + ); + }, + ), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 150 / 198, + ), + ), + ); + } + + /// 페이지 비어있을 때. + Widget _emptyView() { + return Center( + child: Text( + StringRes.noApplicants.tr, + style: context.textTheme.bodyMedium, + ), + ); + } +} diff --git a/lib/feature/job_posting/presentation/pages/job_postings_page.dart b/lib/feature/job_posting/presentation/pages/job_postings_page.dart index 327ae98..8117fc5 100644 --- a/lib/feature/job_posting/presentation/pages/job_postings_page.dart +++ b/lib/feature/job_posting/presentation/pages/job_postings_page.dart @@ -184,7 +184,8 @@ class JobPostingsListState entity: item, onPressed: () async { final bool? result = await context.router.push( - JobPostingDetailRoute(jobPostingId: item.id), + // JobPostingDetailRoute(jobPostingId: item.id), + JobPostingWorkersRoute(jobPostingId: item.id), ); if (result == true) { diff --git a/lib/feature/job_posting/presentation/pages/pages.dart b/lib/feature/job_posting/presentation/pages/pages.dart index 8581af1..641c519 100644 --- a/lib/feature/job_posting/presentation/pages/pages.dart +++ b/lib/feature/job_posting/presentation/pages/pages.dart @@ -1,2 +1,4 @@ export 'job_postings_page.dart'; export 'job_posting_form_page.dart'; +export 'job_posting_detail_page.dart'; +export 'job_posting_workers_page.dart'; diff --git a/lib/feature/job_posting/presentation/widgets/widgets.dart b/lib/feature/job_posting/presentation/widgets/widgets.dart index dee8248..b541b7c 100644 --- a/lib/feature/job_posting/presentation/widgets/widgets.dart +++ b/lib/feature/job_posting/presentation/widgets/widgets.dart @@ -1,2 +1,3 @@ export 'job_postings_item.dart'; export 'detail_bottom_sheet/detail_bottom_sheet.dart'; +export 'worker_profile/worker_profile.dart'; diff --git a/lib/feature/job_posting/presentation/widgets/worker_profile/worker_profile.dart b/lib/feature/job_posting/presentation/widgets/worker_profile/worker_profile.dart new file mode 100644 index 0000000..620ff25 --- /dev/null +++ b/lib/feature/job_posting/presentation/widgets/worker_profile/worker_profile.dart @@ -0,0 +1,113 @@ +import 'package:flutter/cupertino.dart'; +import 'package:withu_app/core/utils/utils.dart'; +import 'package:withu_app/feature/user/presentation/presentation.dart'; +import 'package:withu_app/gen/colors.gen.dart'; +import 'package:withu_app/shared/shared.dart'; + +/// 근로자 프로필 +class WorkerProfile extends StatelessWidget { + /// 프로필 이미지 URL + final String profile; + + /// 이름 + final String name; + + /// 나이 + final int age; + + /// 출근 시간 + final String startTime; + + /// 퇴근시간 + final String endTime; + + /// 출근여부 + final bool workStatus; + + const WorkerProfile({ + super.key, + required this.profile, + required this.name, + required this.age, + required this.startTime, + required this.endTime, + required this.workStatus, + }); + + @override + Widget build(BuildContext context) { + final String badgeText = workStatus + ? StringRes.attendanceScheduled.tr + : StringRes.attendanceCompleted.tr; + + final Color badgeColor = + workStatus ? ColorName.annotations : ColorName.teritary; + return Column( + children: [ + Stack( + children: [ + UserProfile( + profile: profile, + name: name, + age: age, + ), + Positioned( + top: 5, + right: 5, + child: BaseBadge( + text: badgeText, + textStyle: context.textTheme.bodySmall, + backgroundColor: badgeColor, + ), + ), + ], + ), + _WorkTime( + startTime: startTime, + endTime: endTime, + ), + ], + ); + } +} + +class _WorkTime extends StatelessWidget { + final String startTime; + final String endTime; + + const _WorkTime({ + required this.startTime, + required this.endTime, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: [ + Text( + StringRes.attendance.tr, + style: context.textTheme.bodySmall, + ), + Expanded( + child: Text( + startTime.isEmpty ? '' : ' - $startTime', + style: context.textTheme.bodySmall, + ), + ), + Text( + StringRes.leaveWork.tr, + style: context.textTheme.bodySmall, + ), + Expanded( + child: Text( + endTime.isEmpty ? '' : ' - $endTime', + style: context.textTheme.bodySmall, + ), + ), + ], + ), + ); + } +} diff --git a/lib/feature/user/presentation/widgets/user_profile/user_profile.dart b/lib/feature/user/presentation/widgets/user_profile/user_profile.dart index ae14f34..a7c507c 100644 --- a/lib/feature/user/presentation/widgets/user_profile/user_profile.dart +++ b/lib/feature/user/presentation/widgets/user_profile/user_profile.dart @@ -50,6 +50,7 @@ class _ProfileImage extends StatelessWidget { return AspectRatio( aspectRatio: 1, child: Container( + clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: ColorName.teritary, borderRadius: BorderRadius.circular(20), @@ -82,19 +83,22 @@ class _UserInfo extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - Text( - name, - style: context.textTheme.bodyMediumBold, - ), - const SizedBox(width: 1), - Text( - '$age${StringRes.yearsOld.tr}', - style: context.textTheme.bodySmall, - ), - const Spacer(), - ], + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: [ + Text( + name, + style: context.textTheme.bodyMediumBold, + ), + const SizedBox(width: 1), + Text( + '$age${StringRes.yearsOld.tr}', + style: context.textTheme.bodySmall, + ), + const Spacer(), + ], + ), ); } }