diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0ee24b4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/README.md b/README.md index aadd767..08b4ab3 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ ### 프로젝트 사용 기술 -* flutter -* dart -* flutter_bloc: 상태관리 -* autoRouter: navigation -* get_it: di -* dio: api -* freezed: data generation -* derry - script manager +* Flutter, Dart +* State Manegement : flutter_bloc +* Navigation: auto_route +* DI : get_it +* REST API: dio +* Data class: freezed +* Script manager: derry +* Resource manager: flutter_gen +* Lint: flutter_lint ### Architecture diff --git a/analysis_options.yaml b/analysis_options.yaml index 365de27..f18e0e4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,4 +3,5 @@ include: package:flutter_lints/flutter.yaml linter: rules: - - eol_at_end_of_file + - eol_at_end_of_file: true + - require_trailing_commas: true diff --git a/android/app/build.gradle b/android/app/build.gradle index c9b18ef..deca3bc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,7 +6,7 @@ plugins { } android { - namespace = "com.example.withu_app" + namespace = "com.android.withu" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -21,7 +21,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.withu_app" + applicationId = "com.android.withu" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/kotlin/com/example/withu_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/withu_app/MainActivity.kt index 8a826eb..356b852 100644 --- a/android/app/src/main/kotlin/com/example/withu_app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/withu_app/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.withu_app +package com.android.withu import io.flutter.embedding.android.FlutterActivity diff --git a/assets/color/colors.xml b/assets/color/colors.xml new file mode 100644 index 0000000..c0807f1 --- /dev/null +++ b/assets/color/colors.xml @@ -0,0 +1,8 @@ + + #142d00 + #496433 + #8a9680 + #e6ede1 + #F6BE2C + #ffffff + diff --git a/assets/translations/ko.json b/assets/translations/ko.json new file mode 100644 index 0000000..e4830e8 --- /dev/null +++ b/assets/translations/ko.json @@ -0,0 +1,11 @@ +{ + "appName": "위드유", + "temporarySave": "임시저장", + "inProgress": "진행", + "closed": "마감", + "photography": "촬영", + "catering": "케이터링", + "foodStyling": "푸드스타일링", + "florist": "플라워리스트", + "numberOfRecruits": "모집인원" +} diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..c043b1f --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cd84c44..1cf6d4d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + DBC749BBCEE1813469E9EBE9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54FC5508885571A5F027815E /* Pods_Runner.framework */; }; + FC19169D40D3B30B1C8285AB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E12ED764B4D86D43F555C363 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,11 +42,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0D82AA4422652D8004F2A305 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 54FC5508885571A5F027815E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5B89B99F570AD57467A5B205 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 64CD9938BCF89D57A5460C0C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -55,13 +61,26 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A8B58E44B1EB758AA8A2DCCD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + CD45469538CEE843A45C33C2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E12ED764B4D86D43F555C363 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F93BE9EC760304A94A11FACA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 93639A99A847B6F3892EE7F7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FC19169D40D3B30B1C8285AB /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DBC749BBCEE1813469E9EBE9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,15 @@ path = RunnerTests; sourceTree = ""; }; + 5940FA6FAF3401039AC3618C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 54FC5508885571A5F027815E /* Pods_Runner.framework */, + E12ED764B4D86D43F555C363 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +122,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + B592DA797F244265F115B786 /* Pods */, + 5940FA6FAF3401039AC3618C /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +151,20 @@ path = Runner; sourceTree = ""; }; + B592DA797F244265F115B786 /* Pods */ = { + isa = PBXGroup; + children = ( + CD45469538CEE843A45C33C2 /* Pods-Runner.debug.xcconfig */, + A8B58E44B1EB758AA8A2DCCD /* Pods-Runner.release.xcconfig */, + 0D82AA4422652D8004F2A305 /* Pods-Runner.profile.xcconfig */, + 64CD9938BCF89D57A5460C0C /* Pods-RunnerTests.debug.xcconfig */, + F93BE9EC760304A94A11FACA /* Pods-RunnerTests.release.xcconfig */, + 5B89B99F570AD57467A5B205 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + BAC011BA047135336964E80D /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 93639A99A847B6F3892EE7F7 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + DF2CA63EBF8244753EE19552 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E235F37719081D02864E5419 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -253,6 +301,67 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + BAC011BA047135336964E80D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DF2CA63EBF8244753EE19552 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E235F37719081D02864E5419 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -369,7 +478,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.withuApp; + PRODUCT_BUNDLE_IDENTIFIER = com.ios.withu; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 64CD9938BCF89D57A5460C0C /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F93BE9EC760304A94A11FACA /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5B89B99F570AD57467A5B205 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -549,7 +661,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.withuApp; + PRODUCT_BUNDLE_IDENTIFIER = com.ios.withu; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -572,7 +684,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.withuApp; + PRODUCT_BUNDLE_IDENTIFIER = com.ios.withu; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md index 89c2725..b5b843a 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -2,4 +2,4 @@ You can customize the launch screen with your own desired assets by replacing the image files in this directory. -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6d8c73a..39eac26 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +45,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - + CFBundleLocalizations + + en + ko + + diff --git a/lib/core/config/config.dart b/lib/core/config/config.dart new file mode 100644 index 0000000..c9e7d5c --- /dev/null +++ b/lib/core/config/config.dart @@ -0,0 +1 @@ +export 'env/env.dart'; diff --git a/lib/core/config/env/dev.dart b/lib/core/config/env/dev.dart new file mode 100644 index 0000000..898c09c --- /dev/null +++ b/lib/core/config/env/dev.dart @@ -0,0 +1,4 @@ +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/main.dart'; + +void main() => run(environment: EnvironmentType.dev); diff --git a/lib/core/config/env/env.dart b/lib/core/config/env/env.dart new file mode 100644 index 0000000..1706e63 --- /dev/null +++ b/lib/core/config/env/env.dart @@ -0,0 +1,2 @@ +export 'environment.dart'; +export 'environment_type.dart'; diff --git a/lib/core/config/env/environment.dart b/lib/core/config/env/environment.dart new file mode 100644 index 0000000..01a8117 --- /dev/null +++ b/lib/core/config/env/environment.dart @@ -0,0 +1,10 @@ +import 'package:withu_app/core/core.dart'; + +class Environment { + Environment._(); + + static late final EnvironmentType env; + + /// Prod 환경 여부 + static bool get isProd => env == EnvironmentType.prod; +} diff --git a/lib/core/config/env/environment_type.dart b/lib/core/config/env/environment_type.dart new file mode 100644 index 0000000..402b6f2 --- /dev/null +++ b/lib/core/config/env/environment_type.dart @@ -0,0 +1 @@ +enum EnvironmentType { prod, dev } diff --git a/lib/core/config/env/prod.dart b/lib/core/config/env/prod.dart new file mode 100644 index 0000000..7fad4e0 --- /dev/null +++ b/lib/core/config/env/prod.dart @@ -0,0 +1,4 @@ +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/main.dart'; + +void main() => run(environment: EnvironmentType.prod); diff --git a/lib/core/core.dart b/lib/core/core.dart index a1a5d85..e74f355 100644 --- a/lib/core/core.dart +++ b/lib/core/core.dart @@ -1,4 +1,5 @@ -export 'widgets/widgets.dart'; export 'network/network.dart'; export 'router/router.dart'; export 'utils/utils.dart'; +export 'types/type.dart'; +export 'config/config.dart'; diff --git a/lib/core/network/api.dart b/lib/core/network/api.dart new file mode 100644 index 0000000..71920be --- /dev/null +++ b/lib/core/network/api.dart @@ -0,0 +1,17 @@ +import 'package:dio/dio.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; + +class API { + final String url = 'https://example.com'; + + final _dio = Dio(BaseOptions()) + ..interceptors.add( + PrettyDioLogger( + requestBody: true, + responseBody: true, + // enabled: kDebugMode, + ), + ); + + Dio get dio => _dio; +} diff --git a/lib/core/network/mock_api.dart b/lib/core/network/mock_api.dart new file mode 100644 index 0000000..ebe0c8c --- /dev/null +++ b/lib/core/network/mock_api.dart @@ -0,0 +1,9 @@ +import 'package:http_mock_adapter/http_mock_adapter.dart'; + +import 'api.dart'; + +mixin MockAPI on API { + late final DioAdapter _dioAdapter = DioAdapter(dio: dio); + + DioAdapter get dioAdapter => _dioAdapter; +} diff --git a/lib/core/network/network.dart b/lib/core/network/network.dart index e69de29..5cd88cf 100644 --- a/lib/core/network/network.dart +++ b/lib/core/network/network.dart @@ -0,0 +1,2 @@ +export 'api.dart'; +export 'mock_api.dart'; diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index e69de29..5af27c9 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -0,0 +1,21 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:withu_app/core/router/router.gr.dart'; + +@AutoRouterConfig() +class AppRouter extends RootStackRouter { + @override + RouteType get defaultRouteType => const RouteType.material(); + + @override + List get routes => [ + AutoRoute( + page: SplashRoute.page, + path: '/', + initial: true, + ), + AutoRoute( + page: JobPostingsRoute.page, + path: '/job-postings', + ), + ]; +} diff --git a/lib/core/router/router.gr.dart b/lib/core/router/router.gr.dart new file mode 100644 index 0000000..edd8fe5 --- /dev/null +++ b/lib/core/router/router.gr.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:auto_route/auto_route.dart' as _i3; +import 'package:withu_app/feature/job_posting/presentation/pages/job_postings_page.dart' + as _i1; +import 'package:withu_app/feature/splash/presentation/pages/splash_page.dart' + as _i2; + +/// generated route for +/// [_i1.JobPostingsPage] +class JobPostingsRoute extends _i3.PageRouteInfo { + const JobPostingsRoute({List<_i3.PageRouteInfo>? children}) + : super( + JobPostingsRoute.name, + initialChildren: children, + ); + + static const String name = 'JobPostingsRoute'; + + static _i3.PageInfo page = _i3.PageInfo( + name, + builder: (data) { + return const _i1.JobPostingsPage(); + }, + ); +} + +/// generated route for +/// [_i2.SplashPage] +class SplashRoute extends _i3.PageRouteInfo { + const SplashRoute({List<_i3.PageRouteInfo>? children}) + : super( + SplashRoute.name, + initialChildren: children, + ); + + static const String name = 'SplashRoute'; + + static _i3.PageInfo page = _i3.PageInfo( + name, + builder: (data) { + return const _i2.SplashPage(); + }, + ); +} diff --git a/lib/core/types/job_category_type.dart b/lib/core/types/job_category_type.dart new file mode 100644 index 0000000..93d9490 --- /dev/null +++ b/lib/core/types/job_category_type.dart @@ -0,0 +1,21 @@ +import 'package:withu_app/core/core.dart'; + +/// 직업 종류 +enum JobCategoryType with L10nKeyProvider { + /// 촬영 + photography(l10nKey: 'photography'), + + /// 케이터링 + catering(l10nKey: 'catering'), + + /// 푸드스타일링 + foodStyling(l10nKey: 'foodStyling'), + + /// 플라워리스트 + florist(l10nKey: 'florist'); + + @override + final String l10nKey; + + const JobCategoryType({required this.l10nKey}); +} diff --git a/lib/core/types/job_posting_status_type.dart b/lib/core/types/job_posting_status_type.dart new file mode 100644 index 0000000..a4a71b4 --- /dev/null +++ b/lib/core/types/job_posting_status_type.dart @@ -0,0 +1,18 @@ +// 공고 상태 타입 +import 'package:withu_app/core/core.dart'; + +enum JobPostingStatusType with L10nKeyProvider { + /// 임시저장 + temporary(l10nKey: 'temporarySave'), + + /// 진행 + inProgress(l10nKey: 'inProgress'), + + /// 마감 + closed(l10nKey: 'closed'); + + @override + final String l10nKey; + + const JobPostingStatusType({required this.l10nKey}); +} diff --git a/lib/core/types/type.dart b/lib/core/types/type.dart new file mode 100644 index 0000000..fc86c34 --- /dev/null +++ b/lib/core/types/type.dart @@ -0,0 +1,2 @@ +export 'job_category_type.dart'; +export 'job_posting_status_type.dart'; diff --git a/lib/core/utils/extensions/date_ex.dart b/lib/core/utils/extensions/date_ex.dart new file mode 100644 index 0000000..9b4bc39 --- /dev/null +++ b/lib/core/utils/extensions/date_ex.dart @@ -0,0 +1,11 @@ + +import 'package:intl/intl.dart'; + +extension DateTimeEx on DateTime { + + /// 포맷팅한 날짜 얻기 + String format(String format) { + final DateFormat formatter = DateFormat(format); + return formatter.format(this); + } +} diff --git a/lib/core/utils/extensions/extensions.dart b/lib/core/utils/extensions/extensions.dart new file mode 100644 index 0000000..0a0b1c2 --- /dev/null +++ b/lib/core/utils/extensions/extensions.dart @@ -0,0 +1,2 @@ +export 'date_ex.dart'; +export 'theme_extension.dart'; diff --git a/lib/core/utils/extensions/theme_extension.dart b/lib/core/utils/extensions/theme_extension.dart new file mode 100644 index 0000000..1202208 --- /dev/null +++ b/lib/core/utils/extensions/theme_extension.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +extension BulidContextTextThemeEx on BuildContext { + TextTheme get textTheme => Theme.of(this).textTheme; +} + +extension TextThemeEx on TextTheme { + TextStyle? get bodyLargeBold => bodyLarge?.copyWith( + fontWeight: FontWeight.w700, + ); + + TextStyle? get bodyMediumBold => bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + ); + + TextStyle? get bodySmallBold => bodySmall?.copyWith( + fontWeight: FontWeight.w700, + ); +} diff --git a/lib/core/utils/injections.dart b/lib/core/utils/injections.dart index 94dbb79..356f6c5 100644 --- a/lib/core/utils/injections.dart +++ b/lib/core/utils/injections.dart @@ -1,7 +1,10 @@ import 'package:get_it/get_it.dart'; +import 'package:withu_app/feature/job_posting/init_injections.dart'; +import 'package:withu_app/feature/splash/splash.dart'; -final sl = GetIt.instance; +final getIt = GetIt.instance; Future initInjections() async { - + initSplashInjections(); + initJobPostingInjections(); } diff --git a/lib/core/utils/logger/logger.dart b/lib/core/utils/logger/logger.dart new file mode 100644 index 0000000..dfd0aad --- /dev/null +++ b/lib/core/utils/logger/logger.dart @@ -0,0 +1,18 @@ +import 'package:logger/logger.dart'; + +var logger = Logger( + printer: PrettyPrinter( + methodCount: 2, + // Number of method calls to be displayed + errorMethodCount: 8, + // Number of method calls if stacktrace is provided + lineLength: 120, + // Width of the output + colors: true, + // Colorful log messages + printEmojis: true, + // Print an emoji for each log message + // Should each log print contain a timestamp + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + ), +); diff --git a/lib/core/utils/mixins/l10n_key_provider.dart b/lib/core/utils/mixins/l10n_key_provider.dart new file mode 100644 index 0000000..8485e31 --- /dev/null +++ b/lib/core/utils/mixins/l10n_key_provider.dart @@ -0,0 +1,12 @@ +import 'package:easy_localization/easy_localization.dart' as easy; + +// Enum 타입들에 Localization 제고 +mixin L10nKeyProvider { + + /// Localization Key + String get l10nKey; +} + +extension L10nKeyProviderExt on L10nKeyProvider { + String get tr => easy.tr(l10nKey); +} diff --git a/lib/core/utils/mixins/mixins.dart b/lib/core/utils/mixins/mixins.dart new file mode 100644 index 0000000..b271d84 --- /dev/null +++ b/lib/core/utils/mixins/mixins.dart @@ -0,0 +1 @@ +export 'l10n_key_provider.dart'; diff --git a/lib/core/utils/resource/resource.dart b/lib/core/utils/resource/resource.dart new file mode 100644 index 0000000..fcd43ac --- /dev/null +++ b/lib/core/utils/resource/resource.dart @@ -0,0 +1 @@ +export 'string_res.dart'; diff --git a/lib/core/utils/resource/string_res.dart b/lib/core/utils/resource/string_res.dart new file mode 100644 index 0000000..cc67dac --- /dev/null +++ b/lib/core/utils/resource/string_res.dart @@ -0,0 +1,18 @@ +import 'package:easy_localization/easy_localization.dart' as easy; + +enum StringRes { + appName, + temporarySave, + inProgress, + closed, + photography, + catering, + foodStyling, + florist, + numberOfRecruits, +} + +extension StringResEx on StringRes { + /// localization + String get tr => easy.tr(name); +} diff --git a/lib/core/utils/theme/custom_theme.dart b/lib/core/utils/theme/custom_theme.dart new file mode 100644 index 0000000..3ee4303 --- /dev/null +++ b/lib/core/utils/theme/custom_theme.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:withu_app/gen/colors.gen.dart'; + +class CustomTheme { + const CustomTheme(); + + static ThemeData get theme { + return ThemeData( + primaryColor: ColorName.primary, + textTheme: textTheme, + ); + } + + static TextTheme get textTheme { + return const TextTheme( + headlineLarge: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + color: ColorName.primary, + height: 38.4 / 24, + letterSpacing: -0.5, + ), + headlineMedium: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorName.primary, + height: 32 / 20, + letterSpacing: -0.5, + ), + headlineSmall: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: ColorName.primary, + height: 28.8 / 18, + letterSpacing: -0.5, + ), + bodyLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: ColorName.primary, + height: 25.6 / 16, + letterSpacing: -0.5, + ), + bodyMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: ColorName.primary, + height: 22.4 / 14, + letterSpacing: -0.5, + ), + bodySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: ColorName.primary, + height: 19.2 / 12, + letterSpacing: -0.5, + ), + ); + } +} diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index cce6a3b..a3f8795 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -1 +1,6 @@ export 'injections.dart'; +export 'logger/logger.dart'; +export 'extensions/extensions.dart'; +export 'theme/custom_theme.dart'; +export 'resource/resource.dart'; +export 'mixins/mixins.dart'; diff --git a/lib/core/widgets/widgets.dart b/lib/core/widgets/widgets.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/data/data.dart b/lib/data/data.dart deleted file mode 100644 index 93816dd..0000000 --- a/lib/data/data.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'repositories/repositories.dart'; -export 'data_sources/data_sources.dart'; diff --git a/lib/data/data_sources/data_sources.dart b/lib/data/data_sources/data_sources.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/data/repositories/repositories.dart b/lib/data/repositories/repositories.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/domain/repositories/repositories.dart b/lib/domain/repositories/repositories.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/domain/usecases/usecases.dart b/lib/domain/usecases/usecases.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/feature/feature.dart b/lib/feature/feature.dart new file mode 100644 index 0000000..b5ad8ac --- /dev/null +++ b/lib/feature/feature.dart @@ -0,0 +1,2 @@ +export 'splash/splash.dart'; +export 'job_posting/job_posting.dart'; diff --git a/lib/feature/job_posting/data/data.dart b/lib/feature/job_posting/data/data.dart new file mode 100644 index 0000000..b516411 --- /dev/null +++ b/lib/feature/job_posting/data/data.dart @@ -0,0 +1,2 @@ +export 'data_sources/data_sources.dart'; +export 'repositories/repository_impl.dart'; diff --git a/lib/feature/job_posting/data/data_sources/data_sources.dart b/lib/feature/job_posting/data/data_sources/data_sources.dart new file mode 100644 index 0000000..91bacf0 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/data_sources.dart @@ -0,0 +1,3 @@ +export 'dto/dto.dart'; +export 'remote/remote.dart'; +export 'mock/mock.dart'; diff --git a/lib/feature/job_posting/data/data_sources/dto/dto.dart b/lib/feature/job_posting/data/data_sources/dto/dto.dart new file mode 100644 index 0000000..92e45ac --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/dto.dart @@ -0,0 +1,2 @@ +export 'job_posting_model.dart'; +export 'job_postings_model.dart'; diff --git a/lib/feature/job_posting/data/data_sources/dto/job_posting_model.dart b/lib/feature/job_posting/data/data_sources/dto/job_posting_model.dart new file mode 100644 index 0000000..f8c3329 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/job_posting_model.dart @@ -0,0 +1,23 @@ +import 'package:withu_app/core/core.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'job_posting_model.freezed.dart'; + +part 'job_posting_model.g.dart'; + +@freezed +class JobPostingModel with _$JobPostingModel { + const factory JobPostingModel({ + required String id, // Id + required String title, // 공고명 + required JobCategoryType category, // 직업 카테고리 + required JobPostingStatusType status, // 상태값 + required DateTime startDate, // 근무 시작 날짜 + required DateTime? endDate, // 근무 종료 날짜 + required int currentMemberCount, // 현재 모집 인원 + required int maxMemberCount, // 최대 모집 인원 + }) = _JobPostingModel; + + factory JobPostingModel.fromJson(Map json) => + _$JobPostingModelFromJson(json); +} diff --git a/lib/feature/job_posting/data/data_sources/dto/job_posting_model.freezed.dart b/lib/feature/job_posting/data/data_sources/dto/job_posting_model.freezed.dart new file mode 100644 index 0000000..339f986 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/job_posting_model.freezed.dart @@ -0,0 +1,321 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'job_posting_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +JobPostingModel _$JobPostingModelFromJson(Map json) { + return _JobPostingModel.fromJson(json); +} + +/// @nodoc +mixin _$JobPostingModel { + String get id => throw _privateConstructorUsedError; // Id + String get title => throw _privateConstructorUsedError; // 공고명 + JobCategoryType get category => throw _privateConstructorUsedError; // 직업 카테고리 + JobPostingStatusType get status => throw _privateConstructorUsedError; // 상태값 + DateTime get startDate => throw _privateConstructorUsedError; // 근무 시작 날짜 + DateTime? get endDate => throw _privateConstructorUsedError; // 근무 종료 날짜 + int get currentMemberCount => throw _privateConstructorUsedError; // 현재 모집 인원 + int get maxMemberCount => throw _privateConstructorUsedError; + + /// Serializes this JobPostingModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of JobPostingModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $JobPostingModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $JobPostingModelCopyWith<$Res> { + factory $JobPostingModelCopyWith( + JobPostingModel value, $Res Function(JobPostingModel) then) = + _$JobPostingModelCopyWithImpl<$Res, JobPostingModel>; + @useResult + $Res call( + {String id, + String title, + JobCategoryType category, + JobPostingStatusType status, + DateTime startDate, + DateTime? endDate, + int currentMemberCount, + int maxMemberCount}); +} + +/// @nodoc +class _$JobPostingModelCopyWithImpl<$Res, $Val extends JobPostingModel> + implements $JobPostingModelCopyWith<$Res> { + _$JobPostingModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of JobPostingModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? category = null, + Object? status = null, + Object? startDate = null, + Object? endDate = freezed, + Object? currentMemberCount = null, + Object? maxMemberCount = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + category: null == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as JobCategoryType, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as JobPostingStatusType, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: freezed == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + currentMemberCount: null == currentMemberCount + ? _value.currentMemberCount + : currentMemberCount // ignore: cast_nullable_to_non_nullable + as int, + maxMemberCount: null == maxMemberCount + ? _value.maxMemberCount + : maxMemberCount // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$JobPostingModelImplCopyWith<$Res> + implements $JobPostingModelCopyWith<$Res> { + factory _$$JobPostingModelImplCopyWith(_$JobPostingModelImpl value, + $Res Function(_$JobPostingModelImpl) then) = + __$$JobPostingModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String title, + JobCategoryType category, + JobPostingStatusType status, + DateTime startDate, + DateTime? endDate, + int currentMemberCount, + int maxMemberCount}); +} + +/// @nodoc +class __$$JobPostingModelImplCopyWithImpl<$Res> + extends _$JobPostingModelCopyWithImpl<$Res, _$JobPostingModelImpl> + implements _$$JobPostingModelImplCopyWith<$Res> { + __$$JobPostingModelImplCopyWithImpl( + _$JobPostingModelImpl _value, $Res Function(_$JobPostingModelImpl) _then) + : super(_value, _then); + + /// Create a copy of JobPostingModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? category = null, + Object? status = null, + Object? startDate = null, + Object? endDate = freezed, + Object? currentMemberCount = null, + Object? maxMemberCount = null, + }) { + return _then(_$JobPostingModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + category: null == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as JobCategoryType, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as JobPostingStatusType, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: freezed == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + currentMemberCount: null == currentMemberCount + ? _value.currentMemberCount + : currentMemberCount // ignore: cast_nullable_to_non_nullable + as int, + maxMemberCount: null == maxMemberCount + ? _value.maxMemberCount + : maxMemberCount // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$JobPostingModelImpl implements _JobPostingModel { + const _$JobPostingModelImpl( + {required this.id, + required this.title, + required this.category, + required this.status, + required this.startDate, + required this.endDate, + required this.currentMemberCount, + required this.maxMemberCount}); + + factory _$JobPostingModelImpl.fromJson(Map json) => + _$$JobPostingModelImplFromJson(json); + + @override + final String id; +// Id + @override + final String title; +// 공고명 + @override + final JobCategoryType category; +// 직업 카테고리 + @override + final JobPostingStatusType status; +// 상태값 + @override + final DateTime startDate; +// 근무 시작 날짜 + @override + final DateTime? endDate; +// 근무 종료 날짜 + @override + final int currentMemberCount; +// 현재 모집 인원 + @override + final int maxMemberCount; + + @override + String toString() { + return 'JobPostingModel(id: $id, title: $title, category: $category, status: $status, startDate: $startDate, endDate: $endDate, currentMemberCount: $currentMemberCount, maxMemberCount: $maxMemberCount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$JobPostingModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + (identical(other.category, category) || + other.category == category) && + (identical(other.status, status) || other.status == status) && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.currentMemberCount, currentMemberCount) || + other.currentMemberCount == currentMemberCount) && + (identical(other.maxMemberCount, maxMemberCount) || + other.maxMemberCount == maxMemberCount)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, title, category, status, + startDate, endDate, currentMemberCount, maxMemberCount); + + /// Create a copy of JobPostingModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$JobPostingModelImplCopyWith<_$JobPostingModelImpl> get copyWith => + __$$JobPostingModelImplCopyWithImpl<_$JobPostingModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$JobPostingModelImplToJson( + this, + ); + } +} + +abstract class _JobPostingModel implements JobPostingModel { + const factory _JobPostingModel( + {required final String id, + required final String title, + required final JobCategoryType category, + required final JobPostingStatusType status, + required final DateTime startDate, + required final DateTime? endDate, + required final int currentMemberCount, + required final int maxMemberCount}) = _$JobPostingModelImpl; + + factory _JobPostingModel.fromJson(Map json) = + _$JobPostingModelImpl.fromJson; + + @override + String get id; // Id + @override + String get title; // 공고명 + @override + JobCategoryType get category; // 직업 카테고리 + @override + JobPostingStatusType get status; // 상태값 + @override + DateTime get startDate; // 근무 시작 날짜 + @override + DateTime? get endDate; // 근무 종료 날짜 + @override + int get currentMemberCount; // 현재 모집 인원 + @override + int get maxMemberCount; + + /// Create a copy of JobPostingModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$JobPostingModelImplCopyWith<_$JobPostingModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/feature/job_posting/data/data_sources/dto/job_posting_model.g.dart b/lib/feature/job_posting/data/data_sources/dto/job_posting_model.g.dart new file mode 100644 index 0000000..691906d --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/job_posting_model.g.dart @@ -0,0 +1,48 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'job_posting_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$JobPostingModelImpl _$$JobPostingModelImplFromJson( + Map json) => + _$JobPostingModelImpl( + id: json['id'] as String, + title: json['title'] as String, + category: $enumDecode(_$JobCategoryTypeEnumMap, json['category']), + status: $enumDecode(_$JobPostingStatusTypeEnumMap, json['status']), + startDate: DateTime.parse(json['startDate'] as String), + endDate: json['endDate'] == null + ? null + : DateTime.parse(json['endDate'] as String), + currentMemberCount: (json['currentMemberCount'] as num).toInt(), + maxMemberCount: (json['maxMemberCount'] as num).toInt(), + ); + +Map _$$JobPostingModelImplToJson( + _$JobPostingModelImpl instance) => + { + 'id': instance.id, + 'title': instance.title, + 'category': _$JobCategoryTypeEnumMap[instance.category]!, + 'status': _$JobPostingStatusTypeEnumMap[instance.status]!, + 'startDate': instance.startDate.toIso8601String(), + 'endDate': instance.endDate?.toIso8601String(), + 'currentMemberCount': instance.currentMemberCount, + 'maxMemberCount': instance.maxMemberCount, + }; + +const _$JobCategoryTypeEnumMap = { + JobCategoryType.photography: 'photography', + JobCategoryType.catering: 'catering', + JobCategoryType.foodStyling: 'foodStyling', + JobCategoryType.florist: 'florist', +}; + +const _$JobPostingStatusTypeEnumMap = { + JobPostingStatusType.temporary: 'temporary', + JobPostingStatusType.inProgress: 'inProgress', + JobPostingStatusType.closed: 'closed', +}; diff --git a/lib/feature/job_posting/data/data_sources/dto/job_postings_model.dart b/lib/feature/job_posting/data/data_sources/dto/job_postings_model.dart new file mode 100644 index 0000000..b4d3eed --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/job_postings_model.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'job_posting_model.dart'; + +part 'job_postings_model.freezed.dart'; + +part 'job_postings_model.g.dart'; + +@freezed +class JobPostingsModel with _$JobPostingsModel { + const factory JobPostingsModel({ + required List? contents, + }) = _JobPostingsModel; + + factory JobPostingsModel.fromJson(Map json) => + _$JobPostingsModelFromJson(json); +} diff --git a/lib/feature/job_posting/data/data_sources/dto/job_postings_model.freezed.dart b/lib/feature/job_posting/data/data_sources/dto/job_postings_model.freezed.dart new file mode 100644 index 0000000..442562b --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/job_postings_model.freezed.dart @@ -0,0 +1,176 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'job_postings_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +JobPostingsModel _$JobPostingsModelFromJson(Map json) { + return _JobPostingsModel.fromJson(json); +} + +/// @nodoc +mixin _$JobPostingsModel { + List? get contents => throw _privateConstructorUsedError; + + /// Serializes this JobPostingsModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of JobPostingsModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $JobPostingsModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $JobPostingsModelCopyWith<$Res> { + factory $JobPostingsModelCopyWith( + JobPostingsModel value, $Res Function(JobPostingsModel) then) = + _$JobPostingsModelCopyWithImpl<$Res, JobPostingsModel>; + @useResult + $Res call({List? contents}); +} + +/// @nodoc +class _$JobPostingsModelCopyWithImpl<$Res, $Val extends JobPostingsModel> + implements $JobPostingsModelCopyWith<$Res> { + _$JobPostingsModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of JobPostingsModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? contents = freezed, + }) { + return _then(_value.copyWith( + contents: freezed == contents + ? _value.contents + : contents // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$JobPostingsModelImplCopyWith<$Res> + implements $JobPostingsModelCopyWith<$Res> { + factory _$$JobPostingsModelImplCopyWith(_$JobPostingsModelImpl value, + $Res Function(_$JobPostingsModelImpl) then) = + __$$JobPostingsModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List? contents}); +} + +/// @nodoc +class __$$JobPostingsModelImplCopyWithImpl<$Res> + extends _$JobPostingsModelCopyWithImpl<$Res, _$JobPostingsModelImpl> + implements _$$JobPostingsModelImplCopyWith<$Res> { + __$$JobPostingsModelImplCopyWithImpl(_$JobPostingsModelImpl _value, + $Res Function(_$JobPostingsModelImpl) _then) + : super(_value, _then); + + /// Create a copy of JobPostingsModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? contents = freezed, + }) { + return _then(_$JobPostingsModelImpl( + contents: freezed == contents + ? _value._contents + : contents // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$JobPostingsModelImpl implements _JobPostingsModel { + const _$JobPostingsModelImpl({required final List? contents}) + : _contents = contents; + + factory _$JobPostingsModelImpl.fromJson(Map json) => + _$$JobPostingsModelImplFromJson(json); + + final List? _contents; + @override + List? get contents { + final value = _contents; + if (value == null) return null; + if (_contents is EqualUnmodifiableListView) return _contents; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'JobPostingsModel(contents: $contents)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$JobPostingsModelImpl && + const DeepCollectionEquality().equals(other._contents, _contents)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_contents)); + + /// Create a copy of JobPostingsModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$JobPostingsModelImplCopyWith<_$JobPostingsModelImpl> get copyWith => + __$$JobPostingsModelImplCopyWithImpl<_$JobPostingsModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$JobPostingsModelImplToJson( + this, + ); + } +} + +abstract class _JobPostingsModel implements JobPostingsModel { + const factory _JobPostingsModel( + {required final List? contents}) = + _$JobPostingsModelImpl; + + factory _JobPostingsModel.fromJson(Map json) = + _$JobPostingsModelImpl.fromJson; + + @override + List? get contents; + + /// Create a copy of JobPostingsModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$JobPostingsModelImplCopyWith<_$JobPostingsModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/feature/job_posting/data/data_sources/dto/job_postings_model.g.dart b/lib/feature/job_posting/data/data_sources/dto/job_postings_model.g.dart new file mode 100644 index 0000000..dd080a8 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/dto/job_postings_model.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'job_postings_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$JobPostingsModelImpl _$$JobPostingsModelImplFromJson( + Map json) => + _$JobPostingsModelImpl( + contents: (json['contents'] as List?) + ?.map((e) => JobPostingModel.fromJson(e as Map)) + .toList(), + ); + +Map _$$JobPostingsModelImplToJson( + _$JobPostingsModelImpl instance) => + { + 'contents': instance.contents, + }; 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 new file mode 100644 index 0000000..b73018f --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/mock/job_posting_mock_api.dart @@ -0,0 +1,46 @@ +import 'dart:async'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/data/data.dart'; + +class JobPostingMockApi extends JobPostingApi with MockAPI { + @override + FutureOr> fetchList({ + required JobPostingStatusType status, + required int page, + }) async { + try { + final list = List.generate( + 30, + (int index) => JobPostingModel( + id: '$index', + title: '공고명 #${index + (page * 30)}', + status: status, + category: JobCategoryType.values[index % 4], + startDate: DateTime.now(), + endDate: DateTime.now(), + currentMemberCount: index % 3, + maxMemberCount: 3, + ), + ); + + dioAdapter.onGet( + url, + (server) => server.reply( + 200, + JobPostingsModel(contents: list), + delay: const Duration(seconds: 1), + ), + ); + + final response = await dio.get(url); + + if (response.statusCode == 200) { + return JobPostingsModel.fromJson(response.data).contents ?? []; + } + return []; + } catch (e) { + logger.e(e); + return []; + } + } +} diff --git a/lib/feature/job_posting/data/data_sources/mock/mock.dart b/lib/feature/job_posting/data/data_sources/mock/mock.dart new file mode 100644 index 0000000..46b8188 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/mock/mock.dart @@ -0,0 +1 @@ +export 'job_posting_mock_api.dart'; diff --git a/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart b/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart new file mode 100644 index 0000000..9217b74 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart @@ -0,0 +1,10 @@ +import 'dart:async'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/data/data.dart'; + +abstract class JobPostingApi extends API { + FutureOr> fetchList({ + required JobPostingStatusType status, + required int page, + }); +} diff --git a/lib/feature/job_posting/data/data_sources/remote/job_posting_api_impl.dart b/lib/feature/job_posting/data/data_sources/remote/job_posting_api_impl.dart new file mode 100644 index 0000000..fcac558 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/remote/job_posting_api_impl.dart @@ -0,0 +1,13 @@ +import 'dart:async'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/data/data.dart'; + +class JobPostingApiImpl extends JobPostingApi { + @override + FutureOr> fetchList({ + required JobPostingStatusType status, + required int page, + }) async { + return []; + } +} diff --git a/lib/feature/job_posting/data/data_sources/remote/remote.dart b/lib/feature/job_posting/data/data_sources/remote/remote.dart new file mode 100644 index 0000000..d0ac639 --- /dev/null +++ b/lib/feature/job_posting/data/data_sources/remote/remote.dart @@ -0,0 +1,2 @@ +export 'job_posting_api.dart'; +export 'job_posting_api_impl.dart'; diff --git a/lib/feature/job_posting/data/repositories/repository_impl.dart b/lib/feature/job_posting/data/repositories/repository_impl.dart new file mode 100644 index 0000000..5af521c --- /dev/null +++ b/lib/feature/job_posting/data/repositories/repository_impl.dart @@ -0,0 +1,24 @@ +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'; + +class JobPostingRepositoryImpl implements JobPostingRepository { + final JobPostingApi api; + + JobPostingRepositoryImpl({ + required this.api, + }); + + @override + Future?> searchJobPostings({ + required JobPostingStatusType status, + required int page, + }) async { + try { + return await api.fetchList(status: status, page: page); + } catch (e) { + logger.e(e); + return null; + } + } +} diff --git a/lib/domain/domain.dart b/lib/feature/job_posting/domain/domain.dart similarity index 100% rename from lib/domain/domain.dart rename to lib/feature/job_posting/domain/domain.dart diff --git a/lib/feature/job_posting/domain/entities/entities.dart b/lib/feature/job_posting/domain/entities/entities.dart new file mode 100644 index 0000000..17116e6 --- /dev/null +++ b/lib/feature/job_posting/domain/entities/entities.dart @@ -0,0 +1 @@ +export 'job_posting_entity.dart'; diff --git a/lib/feature/job_posting/domain/entities/job_posting_entity.dart b/lib/feature/job_posting/domain/entities/job_posting_entity.dart new file mode 100644 index 0000000..3fdf3ce --- /dev/null +++ b/lib/feature/job_posting/domain/entities/job_posting_entity.dart @@ -0,0 +1,63 @@ +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/data/data.dart'; + +class JobPostingEntity { + /// id + final String id; + + /// 공고명 + final String title; + + /// 카테고리 + final JobCategoryType category; + + /// 시작날짜 + final DateTime startDate; + + /// 종료날짜 + final DateTime? endDate; + + /// 상태 + final JobPostingStatusType status; + + /// 현재 지원한 인원 수 + final int currentMemberCount; + + /// 최대 인원 수 + final int maxMemberCount; + + JobPostingEntity({ + required this.id, + required this.title, + required this.category, + required this.startDate, + required this.endDate, + required this.status, + required this.currentMemberCount, + required this.maxMemberCount, + }); + + factory JobPostingEntity.fromModel(JobPostingModel model) { + final JobPostingModel( + :id, + :title, + :category, + :startDate, + :endDate, + :status, + :currentMemberCount, + :maxMemberCount, + ) = model; + + return JobPostingEntity( + id: id, + title: title, + category: category, + startDate: startDate, + endDate: endDate, + status: status, + currentMemberCount: currentMemberCount, + maxMemberCount: maxMemberCount, + ); + } +} diff --git a/lib/feature/job_posting/domain/repositories/repositories.dart b/lib/feature/job_posting/domain/repositories/repositories.dart new file mode 100644 index 0000000..4ad408a --- /dev/null +++ b/lib/feature/job_posting/domain/repositories/repositories.dart @@ -0,0 +1,10 @@ +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/data/data.dart'; + +abstract class JobPostingRepository { + /// 공고 목록 조회 + Future?> searchJobPostings({ + required JobPostingStatusType status, + required int page, + }); +} diff --git a/lib/feature/job_posting/domain/usecases/usecases.dart b/lib/feature/job_posting/domain/usecases/usecases.dart new file mode 100644 index 0000000..46ee97e --- /dev/null +++ b/lib/feature/job_posting/domain/usecases/usecases.dart @@ -0,0 +1,36 @@ +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'; + +abstract class JobPostingUseCase { + Future?> searchJobPostings({ + required JobPostingStatusType status, + required int page, + }); +} + +class JobPostingUseCaseImpl implements JobPostingUseCase { + final JobPostingRepository repository; + + JobPostingUseCaseImpl({ + required this.repository, + }); + + @override + Future> searchJobPostings({ + required JobPostingStatusType status, + required int page, + }) async { + try { + final List? result = await repository.searchJobPostings( + status: status, + page: page, + ); + + return result?.map(JobPostingEntity.fromModel).toList() ?? []; + } catch (e) { + logger.e(e); + return []; + } + } +} diff --git a/lib/feature/job_posting/init_injections.dart b/lib/feature/job_posting/init_injections.dart new file mode 100644 index 0000000..b435c75 --- /dev/null +++ b/lib/feature/job_posting/init_injections.dart @@ -0,0 +1,28 @@ +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'; + +initJobPostingInjections() { + getIt.registerSingleton( + Environment.isProd ? JobPostingApiImpl() : JobPostingMockApi(), + ); + getIt.registerSingleton( + JobPostingRepositoryImpl(api: getIt()), + ); + getIt.registerSingleton( + JobPostingUseCaseImpl(repository: getIt()), + ); + getIt.registerFactory( + () => JobPostingsTemporaryBloc(useCase: getIt()), + ); + getIt.registerFactory( + () => JobPostingsInProgressBloc(useCase: getIt()), + ); + getIt.registerFactory( + () => JobPostingsClosedBloc(useCase: getIt()), + ); + getIt.registerFactory( + () => JobPostingsTabBloc(), + ); +} diff --git a/lib/feature/job_posting/job_posting.dart b/lib/feature/job_posting/job_posting.dart new file mode 100644 index 0000000..28451b8 --- /dev/null +++ b/lib/feature/job_posting/job_posting.dart @@ -0,0 +1,3 @@ +export 'init_injections.dart'; + +export 'presentation/presentation.dart'; diff --git a/lib/feature/job_posting/presentation/blocs/blocs.dart b/lib/feature/job_posting/presentation/blocs/blocs.dart new file mode 100644 index 0000000..f5c8106 --- /dev/null +++ b/lib/feature/job_posting/presentation/blocs/blocs.dart @@ -0,0 +1,2 @@ +export 'job_postings/job_postings_bloc.dart'; +export 'job_postings_tab/job_postings_tab.dart'; diff --git a/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_bloc.dart b/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_bloc.dart new file mode 100644 index 0000000..7574206 --- /dev/null +++ b/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_bloc.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/domain/domain.dart'; + +part 'job_postings_state.dart'; + +part 'job_postings_event.dart'; + +class JobPostingsBloc extends Bloc { + final JobPostingUseCase useCase; + + JobPostingsBloc({ + required this.useCase, + }) : super(const JobPostingState()) { + on(_onGettingListEvent); + } + + _onGettingListEvent( + OnGettingListEvent event, + Emitter emit, + ) async { + emit(state.copyWith(status: JobPostingsStatus.loading)); + + final List entities = await useCase.searchJobPostings( + status: event.type, + page: event.page, + ) ?? + []; + + emit( + state.copyWith( + status: JobPostingsStatus.success, + isLast: false, + list: entities, + ), + ); + } +} + +/// 임시저장 Bloc +class JobPostingsTemporaryBloc extends JobPostingsBloc { + JobPostingsTemporaryBloc({required super.useCase}); +} + +/// 진행 Bloc +class JobPostingsInProgressBloc extends JobPostingsBloc { + JobPostingsInProgressBloc({required super.useCase}); +} + +/// 마감 Bloc +class JobPostingsClosedBloc extends JobPostingsBloc { + JobPostingsClosedBloc({required super.useCase}); +} diff --git a/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_event.dart b/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_event.dart new file mode 100644 index 0000000..b12f10c --- /dev/null +++ b/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_event.dart @@ -0,0 +1,15 @@ +part of 'job_postings_bloc.dart'; + +sealed class JobPostingsEvent {} + +/// 리스트 조회 이벤트 +class OnGettingListEvent extends JobPostingsEvent { + final JobPostingStatusType type; + + final int page; + + OnGettingListEvent({ + required this.type, + required this.page, + }); +} diff --git a/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_state.dart b/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_state.dart new file mode 100644 index 0000000..29ff1b7 --- /dev/null +++ b/lib/feature/job_posting/presentation/blocs/job_postings/job_postings_state.dart @@ -0,0 +1,35 @@ +part of 'job_postings_bloc.dart'; + +enum JobPostingsStatus { initial, loading, success, failure } + +class JobPostingState extends Equatable { + /// 상태값 + final JobPostingsStatus status; + + /// 공고 목록 + final List list; + + /// 마지막 페이지 여부 + final bool isLast; + + const JobPostingState({ + this.status = JobPostingsStatus.initial, + this.list = const [], + this.isLast = true, + }); + + JobPostingState copyWith({ + JobPostingsStatus? status, + List? list, + bool? isLast, + }) { + return JobPostingState( + status: status ?? this.status, + list: list ?? this.list, + isLast: isLast ?? this.isLast, + ); + } + + @override + List get props => [status, list, isLast]; +} diff --git a/lib/feature/job_posting/presentation/blocs/job_postings_tab/job_postings_tab.dart b/lib/feature/job_posting/presentation/blocs/job_postings_tab/job_postings_tab.dart new file mode 100644 index 0000000..704cb7f --- /dev/null +++ b/lib/feature/job_posting/presentation/blocs/job_postings_tab/job_postings_tab.dart @@ -0,0 +1,19 @@ +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/shared/shared.dart'; + +/// 상단 탭 Bloc +class JobPostingsTabBloc extends BaseTabBloc { + JobPostingsTabBloc() : super(tabs: _getTabs()); + + /// 탭 목록 + static List> _getTabs() { + return JobPostingStatusType.values + .map>( + (type) => BaseTabData( + text: type.tr, + value: type, + ), + ) + .toList(); + } +} diff --git a/lib/feature/job_posting/presentation/pages/job_postings_page.dart b/lib/feature/job_posting/presentation/pages/job_postings_page.dart new file mode 100644 index 0000000..b05a191 --- /dev/null +++ b/lib/feature/job_posting/presentation/pages/job_postings_page.dart @@ -0,0 +1,185 @@ +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/feature.dart'; +import 'package:withu_app/feature/job_posting/domain/domain.dart'; +import 'package:withu_app/shared/shared.dart'; + +/// 공고 목록 화면 +@RoutePage() +class JobPostingsPage extends StatelessWidget { + const JobPostingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), + ], + child: _JobPostingsPage(), + ); + } +} + +class _JobPostingsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Container( + width: double.infinity, + height: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 24), + child: const Column( + children: [ + SizedBox(height: 20), + _JobPostingsTabs(), + Expanded(child: JobPostingsListPage()), + ], + ), + ), + ), + ); + } +} + +/// 공고 목록 탭 +class _JobPostingsTabs extends StatelessWidget { + const _JobPostingsTabs(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return BaseTabs( + tabs: state.tabs, + selectedTab: state.selectedTab ?? state.tabs.first, + onTap: (BaseTabData tab) { + context.read().add(OnSelectTap(tab)); + }, + ); + }, + ); + } +} + +/// 공고 목록 페이지 +class JobPostingsListPage extends StatelessWidget { + const JobPostingsListPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final selectedType = + state.selectedTab?.value ?? JobPostingStatusType.temporary; + + return IndexedStack( + index: JobPostingStatusType.values.indexOf(selectedType), + children: const [ + JobPostingsList( + type: JobPostingStatusType.temporary, + ), + JobPostingsList( + type: JobPostingStatusType.inProgress, + ), + JobPostingsList( + type: JobPostingStatusType.closed, + ), + ], + ); + }, + ); + } +} + +/// 공고 목록 - 리스트 +class JobPostingsList extends StatefulWidget { + final JobPostingStatusType type; + + const JobPostingsList({ + super.key, + required this.type, + }); + + @override + State createState() => JobPostingsListState(); +} + +class JobPostingsListState + extends State with AutomaticKeepAliveClientMixin { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + _pagingController.addPageRequestListener( + (pageKey) { + context.read().add( + OnGettingListEvent( + type: widget.type, + page: pageKey, + ), + ); + }, + ); + super.initState(); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + if (state.status == JobPostingsStatus.success) { + final isLast = state.isLast; + if (isLast) { + _pagingController.appendLastPage(state.list); + } else { + final nextPageKey = (_pagingController.nextPageKey ?? 0) + 1; + _pagingController.appendPage(state.list, nextPageKey); + } + } + }, + ), + ], + child: PagedListView( + pagingController: _pagingController, + padding: const EdgeInsets.symmetric(vertical: 20), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => JobPostingsItem( + entity: item, + ), + firstPageProgressIndicatorBuilder: (context) => _emptyView(), + noItemsFoundIndicatorBuilder: (context) => _emptyView(), + ), + ), + ); + } + + /// 페이지 비어있을 때. + Widget _emptyView() { + return Center( + child: Text( + '저장된 공고가 없습니다!', + style: context.textTheme.bodyMedium, + ), + ); + } +} diff --git a/lib/feature/job_posting/presentation/pages/pages.dart b/lib/feature/job_posting/presentation/pages/pages.dart new file mode 100644 index 0000000..44e1abf --- /dev/null +++ b/lib/feature/job_posting/presentation/pages/pages.dart @@ -0,0 +1 @@ +export 'job_postings_page.dart'; diff --git a/lib/feature/job_posting/presentation/presentation.dart b/lib/feature/job_posting/presentation/presentation.dart new file mode 100644 index 0000000..b18dce8 --- /dev/null +++ b/lib/feature/job_posting/presentation/presentation.dart @@ -0,0 +1,3 @@ +export 'pages/pages.dart'; +export 'blocs/blocs.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/feature/job_posting/presentation/widgets/job_postings_item.dart b/lib/feature/job_posting/presentation/widgets/job_postings_item.dart new file mode 100644 index 0000000..acf5851 --- /dev/null +++ b/lib/feature/job_posting/presentation/widgets/job_postings_item.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/job_posting/domain/domain.dart'; +import 'package:withu_app/gen/colors.gen.dart'; +import 'package:withu_app/shared/shared.dart'; + +/// 공고 목록 아이템 +class JobPostingsItem extends StatelessWidget { + final JobPostingEntity entity; + + const JobPostingsItem({super.key, required this.entity}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorName.teritary, + width: 0.5, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: _Information(entity: entity), + ), + _RightView(entity: entity), + ], + ), + ); + } +} + +class _Information extends StatelessWidget { + final JobPostingEntity entity; + + const _Information({required this.entity}); + + String get _date => _getDisplayDate(); + + /// 표시 날짜 얻기. + String _getDisplayDate() { + const String format = 'yy.MM.dd(E)'; + String result = entity.startDate.format(format); + + if (entity.endDate != null) { + result += ' - ${entity.endDate?.format(format)}'; + } + return result; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + BaseBadge( + text: entity.category.tr, + textStyle: context.textTheme.bodySmall, + backgroundColor: ColorName.teritary, + ), + const SizedBox(width: 8), + Text( + entity.title, + style: context.textTheme.headlineSmall, + ), + ], + ), + const SizedBox(height: 4), + Text( + _date, + textAlign: TextAlign.start, + style: context.textTheme.bodyMedium, + ), + ], + ); + } +} + +class _RightView extends StatelessWidget { + final JobPostingEntity entity; + + const _RightView({required this.entity}); + + @override + Widget build(BuildContext context) { + switch (entity.status) { + case JobPostingStatusType.temporary: + return const _TemporaryView(); + case JobPostingStatusType.inProgress: + return _InProgressView( + max: entity.maxMemberCount, + current: entity.currentMemberCount, + ); + case JobPostingStatusType.closed: + return const _ClosedView(); + } + } +} + +/// 임시저장 +class _TemporaryView extends StatelessWidget { + const _TemporaryView(); + + @override + Widget build(BuildContext context) { + return Text( + StringRes.temporarySave.tr, + style: context.textTheme.bodySmall, + ); + } +} + +/// 진행 - 모집 인원 +class _InProgressView extends StatelessWidget { + /// 최대 인원 + final int max; + + /// 현재 인원 + final int current; + + /// 현재 / 최대 + String get counter => '$current/$max'; + + const _InProgressView({ + required this.max, + required this.current, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + StringRes.numberOfRecruits.tr, + style: context.textTheme.bodySmall, + ), + Text( + counter, + style: context.textTheme.headlineSmall, + ), + ], + ); + } +} + +/// 마감 +class _ClosedView extends StatelessWidget { + const _ClosedView(); + + @override + Widget build(BuildContext context) { + return Text( + StringRes.closed.tr, + style: context.textTheme.bodySmall, + ); + } +} diff --git a/lib/feature/job_posting/presentation/widgets/widgets.dart b/lib/feature/job_posting/presentation/widgets/widgets.dart new file mode 100644 index 0000000..e8c6909 --- /dev/null +++ b/lib/feature/job_posting/presentation/widgets/widgets.dart @@ -0,0 +1 @@ +export 'job_postings_item.dart'; diff --git a/lib/feature/splash/init_injections.dart b/lib/feature/splash/init_injections.dart new file mode 100644 index 0000000..df8c0fd --- /dev/null +++ b/lib/feature/splash/init_injections.dart @@ -0,0 +1,6 @@ +import 'package:withu_app/core/utils/injections.dart'; +import 'package:withu_app/feature/splash/presentation/bloc/splash_bloc.dart'; + +initSplashInjections() { + getIt.registerFactory(() => SplashBloc()); +} diff --git a/lib/feature/splash/presentation/bloc/splash_bloc.dart b/lib/feature/splash/presentation/bloc/splash_bloc.dart new file mode 100644 index 0000000..3d2bddf --- /dev/null +++ b/lib/feature/splash/presentation/bloc/splash_bloc.dart @@ -0,0 +1,23 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'splash_event.dart'; + +part 'splash_state.dart'; + +class SplashBloc extends Bloc { + SplashBloc() : super(LoadingState()) { + on(_onOnInitializeApp); + } + + FutureOr _onOnInitializeApp( + OnInitializeApp event, + Emitter emit, + ) async { + // 1초 대기 후 홈 화면으로 이동. + await Future.delayed(const Duration(seconds: 1)); + + // 공고 목록 화면으로 이동 + emit(NavigateToNextScreenState()); + } +} diff --git a/lib/feature/splash/presentation/bloc/splash_event.dart b/lib/feature/splash/presentation/bloc/splash_event.dart new file mode 100644 index 0000000..0d83a4c --- /dev/null +++ b/lib/feature/splash/presentation/bloc/splash_event.dart @@ -0,0 +1,5 @@ +part of 'splash_bloc.dart'; + +sealed class SplashEvent {} + +class OnInitializeApp extends SplashEvent {} diff --git a/lib/feature/splash/presentation/bloc/splash_state.dart b/lib/feature/splash/presentation/bloc/splash_state.dart new file mode 100644 index 0000000..9a426d1 --- /dev/null +++ b/lib/feature/splash/presentation/bloc/splash_state.dart @@ -0,0 +1,9 @@ +part of 'splash_bloc.dart'; + +sealed class SplashState {} + +/// 로딩 상태 +class LoadingState extends SplashState {} + +/// 다음 화면으로 넘어가기 +class NavigateToNextScreenState extends SplashState {} diff --git a/lib/feature/splash/presentation/pages/splash_page.dart b/lib/feature/splash/presentation/pages/splash_page.dart new file mode 100644 index 0000000..160a850 --- /dev/null +++ b/lib/feature/splash/presentation/pages/splash_page.dart @@ -0,0 +1,41 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/core/router/router.gr.dart'; +import 'package:withu_app/feature/splash/presentation/bloc/splash_bloc.dart'; + +/// Splash 화면. +@RoutePage() +class SplashPage extends StatelessWidget { + const SplashPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt()..add(OnInitializeApp()), + child: _SplashPage(), + ); + } +} + +class _SplashPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocListener( + listener: (context, state) { + if (state is NavigateToNextScreenState) { + context.router.push(const JobPostingsRoute()); + } + }, + child: Center( + child: Text( + StringRes.appName.tr, + style: context.textTheme.headlineLarge, + ), + ), + ), + ); + } +} diff --git a/lib/feature/splash/splash.dart b/lib/feature/splash/splash.dart new file mode 100644 index 0000000..c247cd5 --- /dev/null +++ b/lib/feature/splash/splash.dart @@ -0,0 +1,2 @@ +export 'init_injections.dart'; +export 'presentation/pages/splash_page.dart'; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart new file mode 100644 index 0000000..afb6430 --- /dev/null +++ b/lib/gen/assets.gen.dart @@ -0,0 +1,27 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +class $AssetsTranslationsGen { + const $AssetsTranslationsGen(); + + /// File path: assets/translations/en.json + String get en => 'assets/translations/en.json'; + + /// File path: assets/translations/ko.json + String get ko => 'assets/translations/ko.json'; + + /// List of all assets + List get values => [en, ko]; +} + +class Assets { + Assets._(); + + static const $AssetsTranslationsGen translations = $AssetsTranslationsGen(); +} diff --git a/lib/gen/colors.gen.dart b/lib/gen/colors.gen.dart new file mode 100644 index 0000000..f5a6926 --- /dev/null +++ b/lib/gen/colors.gen.dart @@ -0,0 +1,33 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +import 'package:flutter/painting.dart'; +import 'package:flutter/material.dart'; + +class ColorName { + ColorName._(); + + /// Color: #F6BE2C + static const Color annotations = Color(0xFFF6BE2C); + + /// Color: #142d00 + static const Color primary = Color(0xFF142D00); + + /// Color: #496433 + static const Color primary80 = Color(0xFF496433); + + /// Color: #8a9680 + static const Color secondary = Color(0xFF8A9680); + + /// Color: #e6ede1 + static const Color teritary = Color(0xFFE6EDE1); + + /// Color: #ffffff + static const Color white = Color(0xFFFFFFFF); +} diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart new file mode 100644 index 0000000..9117013 --- /dev/null +++ b/lib/generated/assets.dart @@ -0,0 +1,8 @@ +///This file is automatically generated. DO NOT EDIT, all your changes would be lost. +class Assets { + Assets._(); + + static const String colorColors = 'assets/color/colors.xml'; + static const String translationsKo = 'assets/translations/ko.json'; + +} diff --git a/lib/main.dart b/lib/main.dart index 7e5c8e3..33faef2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,43 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:withu_app/core/core.dart'; -void main() async { +void run({ + required EnvironmentType environment, +}) async { + Environment.env = environment; + WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); + // Init DI await initInjections(); - runApp(const App()); + runApp( + EasyLocalization( + supportedLocales: const [Locale('ko')], + fallbackLocale: const Locale('ko'), + startLocale: const Locale('ko'), + path: 'assets/translations', + child: App(), + ), + ); } class App extends StatelessWidget { - const App({super.key}); + final _appRouter = AppRouter(); + + App({super.key}); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'withu', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: Scaffold( - appBar: AppBar( - title: const Text('with'), - ), - body: const Text('init project'), - ), + return MaterialApp.router( + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + theme: CustomTheme.theme, + routerConfig: _appRouter.config(), ); } } diff --git a/lib/presentation/presentation.dart b/lib/presentation/presentation.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/shared/shared.dart b/lib/shared/shared.dart new file mode 100644 index 0000000..e691bba --- /dev/null +++ b/lib/shared/shared.dart @@ -0,0 +1 @@ +export 'widgets/widgets.dart'; diff --git a/lib/shared/widgets/base_badge/base_badge.dart b/lib/shared/widgets/base_badge/base_badge.dart new file mode 100644 index 0000000..019084f --- /dev/null +++ b/lib/shared/widgets/base_badge/base_badge.dart @@ -0,0 +1,34 @@ +import 'package:flutter/cupertino.dart'; + +class BaseBadge extends StatelessWidget { + final String text; + + final TextStyle? textStyle; + + final Color backgroundColor; + + const BaseBadge({ + super.key, + required this.text, + this.textStyle, + required this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 6, + ), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + text, + style: textStyle, + ), + ); + } +} diff --git a/lib/shared/widgets/tab/bloc/base_tab_bloc.dart b/lib/shared/widgets/tab/bloc/base_tab_bloc.dart new file mode 100644 index 0000000..83aa4ef --- /dev/null +++ b/lib/shared/widgets/tab/bloc/base_tab_bloc.dart @@ -0,0 +1,22 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:withu_app/shared/shared.dart'; + +part 'base_tab_event.dart'; + +part 'base_tab_state.dart'; + +// TabBloc 정의 +class BaseTabBloc extends Bloc { + BaseTabBloc({ + required List tabs, + }) : super(BaseTabState(tabs: tabs, selectedTab: null)) { + on( + (event, emit) => emit( + state.copyWith( + tabs: tabs, + selectedTab: event.selectedTab, + ), + ), + ); + } +} diff --git a/lib/shared/widgets/tab/bloc/base_tab_event.dart b/lib/shared/widgets/tab/bloc/base_tab_event.dart new file mode 100644 index 0000000..8956777 --- /dev/null +++ b/lib/shared/widgets/tab/bloc/base_tab_event.dart @@ -0,0 +1,10 @@ +part of 'base_tab_bloc.dart'; + +abstract class BaseTabEvent {} + +/// 탭 클릭 이벤트 +class OnSelectTap extends BaseTabEvent { + final BaseTabData selectedTab; + + OnSelectTap(this.selectedTab); +} diff --git a/lib/shared/widgets/tab/bloc/base_tab_state.dart b/lib/shared/widgets/tab/bloc/base_tab_state.dart new file mode 100644 index 0000000..4586f9e --- /dev/null +++ b/lib/shared/widgets/tab/bloc/base_tab_state.dart @@ -0,0 +1,21 @@ +part of 'base_tab_bloc.dart'; + +class BaseTabState { + final List> tabs; + final BaseTabData? selectedTab; + + BaseTabState({ + required this.tabs, + this.selectedTab, + }); + + BaseTabState copyWith({ + List>? tabs, + BaseTabData? selectedTab, + }) { + return BaseTabState( + tabs: tabs ?? this.tabs, + selectedTab: selectedTab ?? this.selectedTab, + ); + } +} diff --git a/lib/shared/widgets/tab/tab.dart b/lib/shared/widgets/tab/tab.dart new file mode 100644 index 0000000..2111bc5 --- /dev/null +++ b/lib/shared/widgets/tab/tab.dart @@ -0,0 +1,4 @@ +export 'widget/base_tabs.dart'; +export 'widget/base_tab.dart'; +export 'widget/base_tab_data.dart'; +export 'bloc/base_tab_bloc.dart'; diff --git a/lib/shared/widgets/tab/widget/base_tab.dart b/lib/shared/widgets/tab/widget/base_tab.dart new file mode 100644 index 0000000..b76764e --- /dev/null +++ b/lib/shared/widgets/tab/widget/base_tab.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:withu_app/core/utils/extensions/theme_extension.dart'; +import 'package:withu_app/gen/colors.gen.dart'; +import 'package:withu_app/shared/shared.dart'; + +/// 탭 아이템 +class BaseTab extends StatelessWidget { + final BaseTabData data; + + final bool isSelected; + + final VoidCallback onTap; + + const BaseTab({ + super.key, + required this.data, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final textColor = isSelected ? ColorName.primary : ColorName.secondary; + final borderColor = isSelected ? ColorName.primary : Colors.transparent; + + return InkWell( + onTap: onTap, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: borderColor, + width: 3, + ), + ), + ), + child: Text( + data.text, + textAlign: TextAlign.center, + style: context.textTheme.bodyLargeBold?.copyWith( + color: textColor, + ), + ), + ), + ); + } +} diff --git a/lib/shared/widgets/tab/widget/base_tab_data.dart b/lib/shared/widgets/tab/widget/base_tab_data.dart new file mode 100644 index 0000000..eef30f3 --- /dev/null +++ b/lib/shared/widgets/tab/widget/base_tab_data.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +/// BaseTab 에서 사용될 Data +class BaseTabData extends Equatable { + final String text; + + final T value; + + const BaseTabData({ + required this.text, + required this.value, + }); + + @override + List get props => [text, value]; +} diff --git a/lib/shared/widgets/tab/widget/base_tabs.dart b/lib/shared/widgets/tab/widget/base_tabs.dart new file mode 100644 index 0000000..318602a --- /dev/null +++ b/lib/shared/widgets/tab/widget/base_tabs.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:withu_app/gen/colors.gen.dart'; +import 'package:withu_app/shared/shared.dart'; + +/// Dynamic Tab +class BaseTabs extends StatelessWidget { + final List tabs; + + final BaseTabData? selectedTab; + + final void Function(BaseTabData tab) onTap; + + const BaseTabs({ + super.key, + required this.tabs, + required this.selectedTab, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorName.teritary, + ), + ), + ), + child: Row( + children: tabs + .map( + (tab) => Expanded( + child: BaseTab( + data: tab, + isSelected: selectedTab == tab, + onTap: () => onTap(tab), + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart new file mode 100644 index 0000000..233ceb2 --- /dev/null +++ b/lib/shared/widgets/widgets.dart @@ -0,0 +1,2 @@ +export 'tab/tab.dart'; +export 'base_badge/base_badge.dart'; diff --git a/pubspec.lock b/pubspec.lock index ec7b3b4..c64b0e3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -174,6 +174,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + color: + dependency: transitive + description: + name: color + sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb + url: "https://pub.dev" + source: hosted + version: "3.0.0" convert: dependency: transitive description: @@ -206,6 +214,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" dio: dependency: "direct main" description: @@ -222,6 +238,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + url: "https://pub.dev" + source: hosted + version: "3.0.7" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" equatable: dependency: "direct main" description: @@ -238,6 +270,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: @@ -267,6 +307,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_gen_core: + dependency: transitive + description: + name: flutter_gen_core + sha256: "638d518897f1aefc55a24278968027591d50223a6943b6ae9aa576fe1494d99d" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + flutter_gen_runner: + dependency: "direct dev" + description: + name: flutter_gen_runner + sha256: "7f2f02d95e3ec96cf70a1c515700c0dd3ea905af003303a55d6fb081240e6b8a" + url: "https://pub.dev" + source: hosted + version: "5.7.0" flutter_lints: dependency: "direct dev" description: @@ -275,11 +331,29 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_staggered_grid_view: + dependency: transitive + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" freezed: dependency: "direct dev" description: @@ -328,6 +402,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hashcodes: + dependency: transitive + description: + name: hashcodes + sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + http_mock_adapter: + dependency: "direct dev" + description: + name: http_mock_adapter + sha256: "46399c78bd4a0af071978edd8c502d7aeeed73b5fb9860bca86b5ed647a63c1b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" http_multi_server: dependency: transitive description: @@ -344,6 +434,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_size_getter: + dependency: transitive + description: + name: image_size_getter + sha256: f98c4246144e9b968899d2dfde69091e22a539bb64bc9b0bea51505fbb490e57 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + infinite_scroll_pagination: + dependency: "direct main" + description: + name: infinite_scroll_pagination + sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a + url: "https://pub.dev" + source: hosted + version: "4.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: @@ -408,6 +522,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" logging: dependency: "direct main" description: @@ -480,6 +602,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -488,6 +642,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -496,6 +666,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" provider: dependency: transitive description: @@ -520,6 +698,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -541,6 +775,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" source_gen: dependency: transitive description: @@ -613,6 +855,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + time: + dependency: transitive + description: + name: time + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 + url: "https://pub.dev" + source: hosted + version: "2.1.4" timing: dependency: transitive description: @@ -629,6 +879,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -677,6 +943,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -695,4 +969,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.1 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6c151eb..312c0fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,11 @@ dependencies: get_it: 8.0.0 # Being able to compare objects in Dart - equatable: 2.0.5 + equatable: ^2.0.5 # Fetch Data From Api dio: 5.7.0 + pretty_dio_logger: ^1.4.0 # Logging Interceptor logging: 1.2.0 @@ -36,6 +37,16 @@ dependencies: # Router auto_route: 9.2.2 + # Logging + logger: ^2.4.0 + + # Localization + intl: ^0.19.0 + easy_localization: ^3.0.7 + + # + infinite_scroll_pagination: ^4.0.0 + dev_dependencies: flutter_test: sdk: flutter @@ -45,13 +56,38 @@ dev_dependencies: json_serializable: 6.8.0 freezed: 2.5.7 + # lint flutter_lints: ^4.0.0 + + # router auto_route_generator: ^9.0.0 + # Resource manager + flutter_gen_runner: + + # api mocking + http_mock_adapter: ^0.6.1 + flutter: uses-material-design: true + assets: + - assets/translations/ + # Derry script manager scripts: + # build runner 실행 build: dart run build_runner build --delete-conflicting-outputs + + # 전체 파일 대상으로 pre-commit hooks 스크립트 실행 + pre-commit: pre-commit run --all-files + +# Resource manager +flutter_gen: + output: lib/gen/ + line_length: 80 + + colors: + inputs: + - assets/color/colors.xml