├── README.adoc ├── server ├── gradle.properties ├── gradle │ ├── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ └── scripts │ │ ├── jacoco.gradle │ │ ├── mavenPublishing.gradle │ │ ├── kotlin.gradle │ │ ├── detekt.gradle │ │ ├── java.gradle │ │ └── console.gradle ├── settings.gradle ├── api-core-infra-impl │ ├── src │ │ ├── main │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── sirloin │ │ │ │ │ └── sandbox │ │ │ │ │ └── server │ │ │ │ │ └── core │ │ │ │ │ ├── CoreApplicationImpl.kt │ │ │ │ │ ├── domain │ │ │ │ │ └── user │ │ │ │ │ │ └── repository │ │ │ │ │ │ ├── jdbc │ │ │ │ │ │ └── UserEntityDao.kt │ │ │ │ │ │ ├── UserRepositoryImpl.kt │ │ │ │ │ │ └── UserReadonlyRepositoryImpl.kt │ │ │ │ │ ├── appconfig │ │ │ │ │ ├── LoggerConfig.kt │ │ │ │ │ └── SpringDataJdbcConfig.kt │ │ │ │ │ └── converter │ │ │ │ │ ├── BytesUuidConverters.kt │ │ │ │ │ └── InstantStringConverters.kt │ │ │ └── resources │ │ │ │ └── sql │ │ │ │ └── v1.0 │ │ │ │ └── schema │ │ │ │ └── 00-users.sql │ │ └── test │ │ │ ├── resources │ │ │ ├── logback-test.xml │ │ │ └── application.yml │ │ │ └── kotlin │ │ │ └── testcase │ │ │ ├── medium │ │ │ └── SpringDataJdbcMediumTestBase.kt │ │ │ ├── large │ │ │ ├── domain │ │ │ │ └── user │ │ │ │ │ ├── UserRepositoryLargeTestBase.kt │ │ │ │ │ ├── DeleteAndFindUserTest.kt │ │ │ │ │ ├── SaveAndFindUserTest.kt │ │ │ │ │ └── UpdateAndFindUserTest.kt │ │ │ └── SpringDataJdbcLargeTestBase.kt │ │ │ └── small │ │ │ └── converter │ │ │ └── BytesUuidConverterSpec.kt │ └── build.gradle ├── api-main │ ├── src │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── sirloin │ │ │ │ └── sandbox │ │ │ │ └── server │ │ │ │ └── api │ │ │ │ ├── endpoint │ │ │ │ ├── v1 │ │ │ │ │ ├── OkResponseV1.kt │ │ │ │ │ ├── ApiPathsV1.kt │ │ │ │ │ ├── ResponseV1.kt │ │ │ │ │ ├── user │ │ │ │ │ │ ├── response │ │ │ │ │ │ │ ├── DeletedUserResponse.kt │ │ │ │ │ │ │ └── UserResponse.kt │ │ │ │ │ │ ├── request │ │ │ │ │ │ │ ├── CreateUserRequest.kt │ │ │ │ │ │ │ └── UpdateUserRequest.kt │ │ │ │ │ │ ├── GetUserController.kt │ │ │ │ │ │ ├── CreateUserController.kt │ │ │ │ │ │ └── DeleteUserController.kt │ │ │ │ │ ├── ErrorResponseV1.kt │ │ │ │ │ └── ResponseEnvelopeV1.kt │ │ │ │ ├── ApiPaths.kt │ │ │ │ └── util │ │ │ │ │ └── ConversionUtils.kt │ │ │ │ ├── advice │ │ │ │ ├── errorHandler │ │ │ │ │ └── ApiV1ExceptionHandlerContract.kt │ │ │ │ ├── requestDecorator │ │ │ │ │ └── StringRequestTrimmer.kt │ │ │ │ ├── responseDecorator │ │ │ │ │ ├── InstantResponseDecorator.kt │ │ │ │ │ └── V1ResponseDecorator.kt │ │ │ │ ├── AcceptLanguageLocaleProvider.kt │ │ │ │ └── ExceptionCodeToHttpStatusConverter.kt │ │ │ │ ├── AppProfile.kt │ │ │ │ └── appconfig │ │ │ │ ├── AppConfig.kt │ │ │ │ ├── JsonCodecConfig.kt │ │ │ │ ├── WebMvcConfig.kt │ │ │ │ └── domain │ │ │ │ └── user │ │ │ │ └── UserBeanConfig.kt │ │ ├── test │ │ │ ├── resources │ │ │ │ ├── logback-test.xml │ │ │ │ ├── org │ │ │ │ │ └── springframework │ │ │ │ │ │ └── restdocs │ │ │ │ │ │ └── templates │ │ │ │ │ │ ├── request-fields.snippet │ │ │ │ │ │ └── response-fields.snippet │ │ │ │ └── application.yml │ │ │ └── kotlin │ │ │ │ ├── testcase │ │ │ │ └── large │ │ │ │ │ ├── LargeTestConfig.kt │ │ │ │ │ └── endpoint │ │ │ │ │ └── v1 │ │ │ │ │ └── UserTestBaseV1.kt │ │ │ │ └── test │ │ │ │ └── large │ │ │ │ └── endpoint │ │ │ │ └── v1 │ │ │ │ ├── user │ │ │ │ ├── UserApiScenarioUtils.kt │ │ │ │ └── UserApiRequestUtils.kt │ │ │ │ └── RestDocsFieldSnippets.kt │ │ ├── asciidoc │ │ │ └── index.adoc │ │ ├── alpha │ │ │ └── resources │ │ │ │ └── logback-spring.xml │ │ ├── beta │ │ │ └── resources │ │ │ │ └── logback-spring.xml │ │ ├── release │ │ │ └── resources │ │ │ │ └── logback-spring.xml │ │ └── local │ │ │ └── resources │ │ │ └── logback-spring.xml │ ├── sourceSets.gradle │ └── largeTest.gradle ├── api-core │ ├── build.gradle │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── sirloin │ │ │ └── sandbox │ │ │ └── server │ │ │ └── core │ │ │ ├── model │ │ │ ├── Editable.kt │ │ │ ├── DateAuditable.kt │ │ │ └── Versioned.kt │ │ │ ├── CoreApplication.kt │ │ │ ├── domain │ │ │ └── user │ │ │ │ ├── repository │ │ │ │ ├── UserRepository.kt │ │ │ │ └── UserReadonlyRepository.kt │ │ │ │ ├── exception │ │ │ │ └── UserNotFoundException.kt │ │ │ │ ├── common │ │ │ │ └── UserServiceMixin.kt │ │ │ │ └── service │ │ │ │ ├── DeleteUserService.kt │ │ │ │ ├── GetUserService.kt │ │ │ │ ├── CreateUserService.kt │ │ │ │ └── UpdateUserService.kt │ │ │ ├── exception │ │ │ ├── ClientException.kt │ │ │ ├── ServerException.kt │ │ │ ├── client │ │ │ │ ├── MalformedInputException.kt │ │ │ │ ├── RequestedServiceNotFoundException.kt │ │ │ │ ├── WrongPresentationRequestException.kt │ │ │ │ └── WrongInputException.kt │ │ │ ├── server │ │ │ │ └── UnhandledException.kt │ │ │ ├── MtException.kt │ │ │ └── MtExceptionCode.kt │ │ │ └── i18n │ │ │ └── LocaleProvider.kt │ │ └── test │ │ └── kotlin │ │ ├── test │ │ └── small │ │ │ └── domain │ │ │ ├── user │ │ │ └── MockUserRepository.kt │ │ │ └── UserTestUtils.kt │ │ └── testcase │ │ └── small │ │ └── domain │ │ └── user │ │ ├── UserSpec.kt │ │ └── service │ │ └── DeleteUserServiceSpec.kt ├── .gitignore ├── sirloin-coding-sytles.xml ├── index.adoc └── application.yml ├── .gitignore ├── client ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── AppDelegate.swift │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ └── Main.storyboard │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile.lock │ ├── .gitignore │ └── Podfile ├── lib │ ├── data │ │ ├── data_exceptions.dart │ │ ├── remote │ │ │ └── http │ │ │ │ ├── common │ │ │ │ ├── json_serializable.dart │ │ │ │ ├── json_exportable.dart │ │ │ │ └── dto │ │ │ │ │ └── response_error.dart │ │ │ │ ├── user │ │ │ │ └── dto │ │ │ │ │ ├── request_update_user.dart │ │ │ │ │ ├── request_create_user.dart │ │ │ │ │ ├── response_deleted_user.dart │ │ │ │ │ └── response_user.dart │ │ │ │ └── http_exceptions.dart │ │ └── local │ │ │ └── seraialised_data.dart │ ├── domain │ │ ├── user │ │ │ ├── user_exceptions.dart │ │ │ ├── repository │ │ │ │ ├── repo_ro_user.dart │ │ │ │ └── repo_user.dart │ │ │ ├── bloc │ │ │ │ ├── deregister │ │ │ │ │ ├── message_deregister_user.dart │ │ │ │ │ ├── bloc_deregister_user.dart │ │ │ │ │ └── state_deregister_user.dart │ │ │ │ ├── register │ │ │ │ │ ├── message_register_user.dart │ │ │ │ │ ├── bloc_register_user.dart │ │ │ │ │ └── state_register_user.dart │ │ │ │ ├── get_profile │ │ │ │ │ ├── message_get_profile.dart │ │ │ │ │ ├── bloc_get_profile.dart │ │ │ │ │ └── state_get_profile.dart │ │ │ │ └── edit_profile │ │ │ │ │ ├── message_edit_profile.dart │ │ │ │ │ └── state_edit_profile.dart │ │ │ └── user.dart │ │ └── base_bloc_state.dart │ ├── screen │ │ ├── message_screen_splash.dart │ │ └── state_screen_splash.dart │ ├── util │ │ └── numerics.dart │ ├── widget │ │ ├── extensions_build_context.dart │ │ └── dialog │ │ │ └── alert_dialog_exceptions.dart │ ├── di │ │ ├── app │ │ │ ├── di_app_constants.dart │ │ │ └── di_app_components.dart │ │ └── domain │ │ │ └── user │ │ │ ├── di_user_repository.dart │ │ │ ├── di_user_data.dart │ │ │ └── di_user_bloc.dart │ ├── main.dart │ └── client_application.dart ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ └── manifest.json ├── android │ ├── gradle.properties │ ├── app │ │ └── src │ │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── sirloin │ │ │ │ │ └── sandbox │ │ │ │ │ └── client │ │ │ │ │ └── sirloin_sandbox_client │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── test │ ├── lib │ │ ├── test_components.dart │ │ ├── util │ │ │ └── randomiser.dart │ │ ├── domain │ │ │ └── user │ │ │ │ └── randomiser.dart │ │ └── data │ │ │ └── remote │ │ │ └── http │ │ │ ├── randomiser.dart │ │ │ ├── common │ │ │ ├── randomiser.dart │ │ │ └── dto │ │ │ │ ├── mock_error_response.dart │ │ │ │ ├── test_support.dart │ │ │ │ ├── randomiser.dart │ │ │ │ └── mock_response_envelope.dart │ │ │ └── user │ │ │ └── dto │ │ │ ├── randomiser_response_deleted_user.dart │ │ │ └── randomiser_response_user.dart │ ├── mock │ │ ├── @localstorage │ │ │ └── local_storage.dart │ │ ├── lib │ │ │ ├── data │ │ │ │ ├── remote │ │ │ │ │ └── http │ │ │ │ │ │ └── user │ │ │ │ │ │ └── api_user.dart │ │ │ │ └── local │ │ │ │ │ └── user │ │ │ │ │ └── localstorage_user.dart │ │ │ └── domain │ │ │ │ └── user │ │ │ │ └── domain_user.dart │ │ └── @http │ │ │ └── http.dart │ └── testcase │ │ ├── medium │ │ └── widget_test.dart │ │ └── small │ │ └── data │ │ ├── local │ │ ├── serialised_data_test.dart │ │ └── user │ │ │ ├── localstorage_user_test_saveMyUuid.dart │ │ │ ├── localstorage_user_test_save.dart │ │ │ ├── localstorage_user_test.dart │ │ │ └── localstorage_user_test_findMyUuid.dart │ │ └── repository │ │ └── user │ │ ├── repo_user_impl_test.dart │ │ └── repo_user_impl_test_deregister.dart ├── .metadata ├── .gitignore ├── assets │ └── i18n │ │ └── en_us.yaml └── analysis_options.yaml ├── LICENCE.adoc └── CONTRIBUTING.adoc /README.adoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE specific files 2 | .idea 3 | .project 4 | *.iml 5 | *.ipr 6 | *.iws -------------------------------------------------------------------------------- /client/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /client/lib/data/data_exceptions.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | -------------------------------------------------------------------------------- /client/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/web/favicon.png -------------------------------------------------------------------------------- /client/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /client/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/web/icons/Icon-192.png -------------------------------------------------------------------------------- /client/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/web/icons/Icon-512.png -------------------------------------------------------------------------------- /client/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /client/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /client/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /client/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /client/lib/domain/user/user_exceptions.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | class UserNotFoundException implements Exception {} 6 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /client/lib/data/remote/http/common/json_serializable.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | abstract class JsonSerializable { 6 | String toJsonString(); 7 | } 8 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/common/json_exportable.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | abstract class JsonExportable { 6 | Map toJson(); 7 | } 8 | -------------------------------------------------------------------------------- /server/gradle/scripts/jacoco.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply plugin: "jacoco" 6 | 7 | jacoco { 8 | toolVersion = "$version_jacoco" 9 | } 10 | -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirloin-bondaero/meatplatform-sandbox/HEAD/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/test/lib/test_components.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:logger/logger.dart'; 6 | 7 | Logger newTestLogger() { 8 | return Logger(printer: SimplePrinter()); 9 | } 10 | -------------------------------------------------------------------------------- /server/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | rootProject.name = "com.sirloin.sandbox.server" 6 | 7 | include ":api-core", 8 | ":api-core-infra-impl", 9 | ":api-main" 10 | -------------------------------------------------------------------------------- /server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /client/android/app/src/main/kotlin/com/sirloin/sandbox/client/sirloin_sandbox_client/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sirloin.sandbox.client.sirloin_sandbox_client 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/CoreApplicationImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core 6 | 7 | internal class CoreApplicationImpl : CoreApplication 8 | -------------------------------------------------------------------------------- /client/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /client/lib/screen/message_screen_splash.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | abstract class SplashScreenMessage {} 6 | 7 | class ScreenReadyMessage implements SplashScreenMessage {} 8 | 9 | class InitProgrammeMessage implements SplashScreenMessage {} 10 | -------------------------------------------------------------------------------- /client/test/mock/@localstorage/local_storage.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:localstorage/localstorage.dart'; 6 | import 'package:mockito/annotations.dart'; 7 | 8 | @GenerateMocks([LocalStorage]) 9 | class LocalStorageMockGenerator {} 10 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/OkResponseV1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1 6 | 7 | data class OkResponseV1(override val body: T?) : ResponseEnvelopeV1(Type.OK) 8 | -------------------------------------------------------------------------------- /client/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/lib/util/numerics.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | 6 | /// The smallest possible value of an int within 32 bits. 7 | const int int32MinValue = -2147483648; 8 | 9 | /// The biggest possible value of an int within 32 bits. 10 | const int int32MaxValue = 2147483647; 11 | -------------------------------------------------------------------------------- /server/api-core/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | version "0.1.1" 6 | 7 | apply plugin: "kotlin-allopen" 8 | 9 | apply from: "${project.rootDir}/gradle/scripts/mavenPublishing.gradle" 10 | 11 | jar { 12 | enabled = true 13 | } 14 | 15 | dependencies { 16 | } 17 | -------------------------------------------------------------------------------- /client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/test/mock/lib/data/remote/http/user/api_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:sirloin_sandbox_client/data/remote/http/user/api_user.dart'; 7 | 8 | @GenerateMocks([UserApi]) 9 | class UserApiMockGenerator {} 10 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # System-dependent autogen file 2 | .DS_Store 3 | desktop.ini 4 | 5 | # Build outputs 6 | .gradle 7 | build 8 | out 9 | bin 10 | 11 | # IDE specific files 12 | .idea 13 | .project 14 | 15 | # Automatic build number generation 16 | build_number.properties 17 | api-main/src/main/resources/build_api-main.json 18 | 19 | -------------------------------------------------------------------------------- /server/api-main/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENCE.adoc: -------------------------------------------------------------------------------- 1 | = Sir.LOIN Sandbox Project 라이선스 안내 2 | 3 | 이 소프트웨어는 크리에이티브커먼즈 CC BY-NC-SA 라이선스를 적용받습니다. 즉, 4 | 5 | * 저작자와 출처를 반드시 표시하셔야 합니다. 6 | * 비영리 목적으로만 사용하실 수 있습니다. 7 | * 2차 창작믈이 BY-NC-SA 라이선스를 유지하는 한, 자유로운 변경이 가능합니다. 8 | 9 | 라이선스에 관한 상세한 설명은 link:http://ccl.cckorea.org/about/[크리에이티브 커먼즈] 홈페이지를 참고하세요. 10 | 11 | 저작권 (c) 2022, Sir.LOIN. 모든 권리 보유. 12 | -------------------------------------------------------------------------------- /client/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 097d3313d8e2c7f901932d63e537c1acefb87800 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /client/test/mock/lib/data/local/user/localstorage_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 7 | 8 | @GenerateMocks([UserLocalStorage]) 9 | class UserLocalStorageMockGenerator {} 10 | -------------------------------------------------------------------------------- /client/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/model/Editable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.model 6 | 7 | /** 8 | * Model object 를 편집할 필요가 있을 때 활용할 타입. 9 | * 10 | * @since 2022-02-14 11 | */ 12 | interface Editable { 13 | fun edit(): T 14 | } 15 | -------------------------------------------------------------------------------- /client/lib/domain/user/repository/repo_ro_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 6 | 7 | abstract class UserReadonlyRepository { 8 | Future findSavedSelf(); 9 | 10 | Future getUser({required final String uuid, final bool forceRefresh = false}); 11 | } 12 | -------------------------------------------------------------------------------- /client/lib/widget/extensions_build_context.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_i18n/flutter_i18n.dart'; 7 | 8 | extension FlutterI18nContextExtension on BuildContext { 9 | String i18n(final String key) { 10 | return FlutterI18n.translate(this, key); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /client/test/mock/@http/http.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:http/http.dart' as http; 6 | import 'package:mockito/annotations.dart'; 7 | 8 | @GenerateMocks([HttpClient]) 9 | class HttpGenerator {} 10 | 11 | // 'Client' 라는 이름은 너무 일반적이라 Name conflict 피하기 위해서 'HttpClient' 라는 이름으로 변경 12 | abstract class HttpClient extends http.BaseClient {} 13 | -------------------------------------------------------------------------------- /server/api-main/src/test/kotlin/testcase/large/LargeTestConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large 6 | 7 | import org.springframework.boot.test.context.TestConfiguration 8 | 9 | /** 10 | * Large Test 들에 활용할 Spring Test Application 환경 설정 모음 11 | * 12 | * @since 2022-02-14 13 | */ 14 | @TestConfiguration 15 | class LargeTestConfig { 16 | } 17 | -------------------------------------------------------------------------------- /client/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/CoreApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core 6 | 7 | interface CoreApplication { 8 | companion object { 9 | // Compile-time constant 가 되어야 하기 때문에 어쩔 수 없이 문자열로 지정 10 | const val PACKAGE_NAME = "com.sirloin.sandbox.server.core" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/test/mock/lib/domain/user/domain_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/annotations.dart'; 2 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_ro_user.dart'; 3 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_user.dart'; 4 | 5 | @GenerateMocks([UserReadonlyRepository]) 6 | class UserReadonlyRepositoryMockGenerator {} 7 | 8 | @GenerateMocks([UserRepository]) 9 | class UserRepositoryMockGenerator {} 10 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/ApiPaths.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint 6 | 7 | import com.sirloin.sandbox.server.api.endpoint.v1.ApiPathsV1 8 | 9 | object ApiPaths { 10 | /** Used by Spring default */ 11 | const val ERROR = "/error" 12 | 13 | const val LATEST_VERSION = ApiPathsV1.V1 14 | } 15 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/model/DateAuditable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.model 6 | 7 | import java.time.Instant 8 | 9 | /** 10 | * Model object 의 생성 시기와 변경 이력을 추적할 때 활용할 수 있다. 11 | * 12 | * @since 2022-02-14 13 | */ 14 | interface DateAuditable { 15 | val createdAt: Instant 16 | 17 | val updatedAt: Instant 18 | } 19 | -------------------------------------------------------------------------------- /server/api-main/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet: -------------------------------------------------------------------------------- 1 | ==== 요청 필드 2 | |=== 3 | |필드명|타입|생략가능?|설명 4 | 5 | {{#fields}} 6 | |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 7 | |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} 8 | |{{#tableCellContent}}{{#optional}}O{{/optional}}{{^optional}}X{{/optional}}{{/tableCellContent}} 9 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 10 | 11 | {{/fields}} 12 | 13 | |=== -------------------------------------------------------------------------------- /server/api-main/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet: -------------------------------------------------------------------------------- 1 | ==== 응답 필드 2 | |=== 3 | |필드명|타입|생략가능?|설명 4 | 5 | {{#fields}} 6 | |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 7 | |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} 8 | |{{#tableCellContent}}{{#optional}}O{{/optional}}{{^optional}}X{{/optional}}{{/tableCellContent}} 9 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 10 | 11 | {{/fields}} 12 | 13 | |=== -------------------------------------------------------------------------------- /client/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/gradle/scripts/mavenPublishing.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply plugin: "maven-publish" 6 | 7 | publishing { 8 | publications { 9 | maven(MavenPublication) { 10 | groupId = "com.sirloin.sandbox.server" 11 | version = project.version 12 | artifactId = project.name 13 | 14 | from components.java 15 | artifact sourcesJar 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/sirloin-coding-sytles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /client/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/model/Versioned.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.model 6 | 7 | /** 8 | * Model object 에 optimistic locking 을 구현할 때 활용할 수 있습니다. 9 | * 10 | * @since 2022-02-14 11 | */ 12 | interface Versioned> { 13 | val version: T 14 | 15 | companion object { 16 | const val DEFAULT_INT = 1 17 | const val DEFAULT_LONG_INT = 1L 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "LaunchImage.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "filename": "LaunchImage@2x.png", 11 | "scale": "2x" 12 | }, 13 | { 14 | "idiom": "universal", 15 | "filename": "LaunchImage@3x.png", 16 | "scale": "3x" 17 | } 18 | ], 19 | "info": { 20 | "version": 1, 21 | "author": "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/ApiPathsV1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1 6 | 7 | object ApiPathsV1 { 8 | const val PATH_VAR_UUID = "uuid" 9 | 10 | const val V1 = "/v1" 11 | 12 | const val USER = "$V1/user" 13 | const val USER_UUID = "$V1/user/{$PATH_VAR_UUID}" 14 | 15 | fun userWithUuid(uuid: Any) = 16 | USER_UUID.replaceFirst("{$PATH_VAR_UUID}", uuid.toString()) 17 | } 18 | -------------------------------------------------------------------------------- /client/test/lib/util/randomiser.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:math'; 6 | 7 | extension IterableExtension on Iterable { 8 | T random() { 9 | final randomIndex = Random().nextInt(length); 10 | final iterator = this.iterator; 11 | 12 | int iterations = 0; 13 | while (iterator.moveNext()) { 14 | if (iterations == randomIndex) { 15 | return iterator.current; 16 | } 17 | ++iterations; 18 | } 19 | 20 | return iterator.current; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/gradle/scripts/kotlin.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply plugin: "kotlin" 6 | 7 | compileKotlin { 8 | kotlinOptions.allWarningsAsErrors = true 9 | kotlinOptions.jvmTarget = "$version_target_jvm" 10 | } 11 | compileTestKotlin { 12 | kotlinOptions.allWarningsAsErrors = true 13 | kotlinOptions.jvmTarget = "$version_target_jvm" 14 | } 15 | 16 | dependencies { 17 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 18 | implementation "org.jetbrains.kotlin:kotlin-reflect" 19 | } 20 | -------------------------------------------------------------------------------- /client/lib/di/app/di_app_constants.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:sirloin_sandbox_client/di/di_modules_helper.dart'; 6 | 7 | class AppConstantsModule { 8 | static const String _namespace = "app"; 9 | 10 | static const String qualifierBaseApiUrl = "BASE_API_URL"; 11 | 12 | static Future registerComponents(final DiRuleHolder ruleHolder) async { 13 | ruleHolder 14 | .withNamespace(_namespace) 15 | .registerSingleton(String, () => "http://10.0.2.2:8080", qualifier: qualifierBaseApiUrl); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.repository 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | 9 | /** 10 | * User 도메인 모델의 읽기 및 쓰기 가능 저장소 인터페이스 11 | * 12 | * @since 2022-02-14 13 | */ 14 | interface UserRepository : UserReadonlyRepository { 15 | fun save(user: User): User 16 | 17 | companion object { 18 | const val NAME = "UserRepository" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/deregister/message_deregister_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | abstract class DeregisterUserBlocMessage {} 9 | 10 | @immutable 11 | class ProceedDeregisterMessage extends Equatable implements DeregisterUserBlocMessage { 12 | final String uuid; 13 | 14 | const ProceedDeregisterMessage(this.uuid); 15 | 16 | @override 17 | List get props => [uuid]; 18 | 19 | @override 20 | bool get stringify => true; 21 | } 22 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/UserReadonlyRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.repository 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import java.util.* 9 | 10 | /** 11 | * User 도메인 모델의 읽기 전용 저장소 인터페이스 12 | * 13 | * @since 2022-02-14 14 | */ 15 | interface UserReadonlyRepository { 16 | fun findByUuid(uuid: UUID): User? 17 | 18 | companion object { 19 | const val NAME = "UserReadonlyRepository" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_ios (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | path_provider_ios: 14 | :path: ".symlinks/plugins/path_provider_ios/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 18 | path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 19 | 20 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 21 | 22 | COCOAPODS: 1.11.3 23 | -------------------------------------------------------------------------------- /client/test/lib/domain/user/randomiser.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | import 'package:faker/faker.dart'; 8 | 9 | User randomUser({ 10 | final String? uuid, 11 | final String? nickname, 12 | final String? profileImageUrl, 13 | }) { 14 | final faker = Faker(); 15 | 16 | return User.create( 17 | uuid: uuid ?? const Uuid().v4().toString(), 18 | nickname: nickname ?? faker.person.name(), 19 | profileImageUrl: profileImageUrl ?? faker.image.image()); 20 | } 21 | -------------------------------------------------------------------------------- /server/api-main/sourceSets.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply from: "$project.rootDir/gradle/scripts/packaging.gradle" 6 | apply from: "$project.rootDir/gradle/scripts/console.gradle" 7 | 8 | sourceSets { 9 | def buildConfig = getBuildConfig() 10 | println("Building for '" + CC(LIGHT_GREEN, buildConfig) + "' environment") 11 | 12 | main { 13 | java { 14 | srcDirs += ["src/$buildConfig/java", "src/$buildConfig/kotlin"] 15 | } 16 | resources { 17 | srcDirs += ["src/$buildConfig/resources"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/randomiser.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:faker/faker.dart'; 6 | import 'package:sirloin_sandbox_client/data/remote/http/http_exceptions.dart'; 7 | import 'package:sirloin_sandbox_client/util/numerics.dart'; 8 | 9 | UnexpectedResponseException randomUnexpectedResponseException({ 10 | final String? message, 11 | final String? code, 12 | }) { 13 | final faker = Faker(); 14 | 15 | return UnexpectedResponseException( 16 | message ?? faker.lorem.sentence(), code ?? faker.randomGenerator.integer(int32MaxValue).toString()); 17 | } 18 | -------------------------------------------------------------------------------- /client/lib/domain/base_bloc_state.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | abstract class BaseBlocState {} 9 | 10 | @immutable 11 | abstract class BaseEmptyState implements BaseBlocState {} 12 | 13 | @immutable 14 | abstract class BaseExceptionalState extends Equatable implements BaseBlocState { 15 | final Exception? exception; 16 | 17 | const BaseExceptionalState(this.exception); 18 | 19 | @override 20 | List get props => [exception]; 21 | 22 | @override 23 | bool get stringify => true; 24 | } 25 | -------------------------------------------------------------------------------- /client/lib/main.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_i18n/flutter_i18n.dart'; 7 | import 'package:sirloin_sandbox_client/client_application.dart'; 8 | 9 | Future main() async { 10 | final flutterI18nDelegate = FlutterI18nDelegate( 11 | translationLoader: FileTranslationLoader( 12 | useCountryCode: true, 13 | fallbackFile: "en_us", 14 | basePath: "assets/i18n", 15 | ), 16 | ); 17 | WidgetsFlutterBinding.ensureInitialized(); 18 | runApp(ClientApplication(flutterI18nDelegate: flutterI18nDelegate)); 19 | } 20 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/ClientException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception 6 | 7 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 8 | 9 | /** 10 | * 사용측의 잘못되거나, 지원할 수 없는 요청 등을 받았을 때 발생시킬 예외의 상위 타입입니다. 11 | * 12 | * @since 2022-02-14 13 | */ 14 | open class ClientException constructor( 15 | localeProvider: LocaleProvider, 16 | code: MtExceptionCode, 17 | details: Any? = null, 18 | override val cause: Throwable? = null 19 | ) : MtException(localeProvider, code, details, cause) 20 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/ServerException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception 6 | 7 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 8 | 9 | /** 10 | * 서비스 내부의 문제로 인해 요청을 처리할 수 없을 때 발생시키는 예외의 상위 타입입니다. 11 | * 12 | * @since 2022-02-14 13 | */ 14 | open class ServerException constructor( 15 | localeProvider: LocaleProvider, 16 | code: MtExceptionCode, 17 | details: Any? = null, 18 | override val cause: Throwable? = null 19 | ) : MtException(localeProvider, code, details, cause) 20 | -------------------------------------------------------------------------------- /server/index.adoc: -------------------------------------------------------------------------------- 1 | = Sir.LOIN Sandbox Server API Documentation 2 | Hwan Jo 3 | // Metadata: 4 | :description: Sir.LOIN Sandbox Server API 문서 5 | :keywords: kotlin, spring 6 | // Settings: 7 | :doctype: book 8 | :toc: left 9 | :toclevels: 4 10 | :sectlinks: 11 | :icons: font 12 | :idprefix: 13 | :idseparator: - 14 | 15 | [[overview]] 16 | == Overview 17 | 18 | Sir.LOIN Sandbox Server 의 API 문서입니다. 19 | 20 | 이 문서는 Spring RESTDocs 를 이용해 자동 작성되었습니다. 21 | 22 | [[api-domain]] 23 | == API Domain 24 | 25 | Sandbox Server 는 다음과 같이 API domain 목록을 제공하고 있습니다. 26 | 27 | - link:user/index.adoc[V1 User API] 28 | 29 | API 상세 목록은 개별 Hyperlink 로 이동해 확인하실 수 있습니다. 30 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/advice/errorHandler/ApiV1ExceptionHandlerContract.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.advice.errorHandler 6 | 7 | import com.sirloin.sandbox.server.api.endpoint.v1.ErrorResponseV1 8 | import org.springframework.http.ResponseEntity 9 | import javax.servlet.http.HttpServletRequest 10 | 11 | /** 12 | * V1 API 들의 공통 예외처리 규칙 13 | * 14 | * @since 2022-02-14 15 | */ 16 | interface ApiV1ExceptionHandlerContract { 17 | fun onException(req: HttpServletRequest, exception: T): ResponseEntity? 18 | } 19 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/register/message_register_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | abstract class RegisterUserBlocMessage {} 9 | 10 | @immutable 11 | class ProceedRegistrationMessage extends Equatable implements RegisterUserBlocMessage { 12 | final String nickname; 13 | final String profileImageUrl; 14 | 15 | const ProceedRegistrationMessage(this.nickname, this.profileImageUrl); 16 | 17 | @override 18 | List get props => [nickname, profileImageUrl]; 19 | 20 | @override 21 | bool get stringify => true; 22 | } 23 | -------------------------------------------------------------------------------- /server/api-main/src/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = Sir.LOIN Sandbox Server API Documentation 2 | Hwan Jo 3 | // Metadata: 4 | :description: Sir.LOIN Sandbox Server API 문서 5 | :keywords: kotlin, spring 6 | // Settings: 7 | :doctype: book 8 | :toc: left 9 | :toclevels: 4 10 | :sectlinks: 11 | :icons: font 12 | :idprefix: 13 | :idseparator: - 14 | 15 | [[overview]] 16 | == Overview 17 | 18 | Sir.LOIN Sandbox Server 의 API 문서입니다. 19 | 20 | 이 문서는 Spring RESTDocs 를 이용해 자동 작성되었습니다. 21 | 22 | [[api-domain]] 23 | == API Domain 24 | 25 | Sandbox Server 는 다음과 같이 API domain 목록을 제공하고 있습니다. 26 | 27 | - link:user/index.html[V1 User API] 28 | 29 | API 상세 목록은 개별 Hyperlink 로 이동해 확인하실 수 있습니다. 30 | -------------------------------------------------------------------------------- /client/lib/data/local/seraialised_data.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | 6 | // POINT: mixin 과 abstract class(extends), interface(implements) 의 차이가 뭘까요? 7 | mixin SerialisedData { 8 | static const int defaultExpiredSeconds = 5 * 60; 9 | 10 | /// 반드시 Constructor 에서 초기화해야 합니다. 11 | late final DateTime savedAt; 12 | 13 | // POINT: expiration check 를 외부에서 주입받는게 나을까요, 여기서 구현하는게 좋을까요? 14 | bool isExpired({final int expirationSeconds = defaultExpiredSeconds}) { 15 | final now = DateTime.now(); 16 | final then = savedAt; 17 | 18 | return now.difference(then).inSeconds > expirationSeconds; 19 | } 20 | 21 | T get(); 22 | } 23 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/resources/sql/v1.0/schema/00-users.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */; 5 | 6 | CREATE TABLE IF NOT EXISTS `users` 7 | ( 8 | `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, 9 | `uuid` BINARY(16) NOT NULL, 10 | `nickname` VARCHAR(64) NOT NULL, 11 | `profile_image_url` TEXT NOT NULL, 12 | `deleted_at` DATETIME, 13 | `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | `version` BIGINT NOT NULL, 16 | 17 | UNIQUE KEY UK_USERS_UUID (`uuid`) 18 | ); 19 | -------------------------------------------------------------------------------- /server/gradle/scripts/detekt.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply plugin: "io.gitlab.arturbosch.detekt" 6 | 7 | apply from: "$project.rootDir/gradle/scripts/packaging.gradle" 8 | 9 | detekt { 10 | def buildConfig = getBuildConfig() 11 | 12 | toolVersion = "$version_detekt" 13 | config = files("$project.rootDir/gradle/scripts/settings/detekt.yml") 14 | input = files( 15 | "src/main/java", 16 | "src/main/kotlin", 17 | "src/$buildConfig/java", 18 | "src/$buildConfig/kotlin" 19 | ) 20 | } 21 | 22 | dependencies { 23 | detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$version_detekt" 24 | } 25 | -------------------------------------------------------------------------------- /client/lib/domain/user/repository/repo_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_ro_user.dart'; 6 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 7 | 8 | abstract class UserRepository extends UserReadonlyRepository { 9 | Future register({required final String nickname, required final String profileImageUrl}); 10 | 11 | Future updateProfile({ 12 | required final String uuid, 13 | final String? nickname, 14 | final String? profileImageUrl, 15 | }); 16 | 17 | Future deregister(final String uuid); 18 | 19 | Future deleteSavedUser(final String uuid); 20 | } 21 | -------------------------------------------------------------------------------- /client/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /client/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/client/MalformedInputException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception.client 6 | 7 | import com.sirloin.sandbox.server.core.exception.ClientException 8 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | 11 | /** 12 | * 내용 해독 불가능한 입력을 받았을 때 발생시킬 예외 13 | * 14 | * @since 2022-02-14 15 | * @see WrongInputException 16 | */ 17 | class MalformedInputException( 18 | localeProvider: LocaleProvider, 19 | override val cause: Throwable? = null 20 | ) : ClientException(localeProvider, MtExceptionCode.MALFORMED_INPUT, null, cause) 21 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/client/RequestedServiceNotFoundException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception.client 6 | 7 | import com.sirloin.sandbox.server.core.exception.ClientException 8 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | 11 | /** 12 | * 요청을 처리할 Domain Service 를 찾지 못했을 때 발생시킬 예외 13 | * 14 | * @since 2022-02-14 15 | */ 16 | class RequestedServiceNotFoundException( 17 | localeProvider: LocaleProvider, 18 | override val cause: Throwable? = null 19 | ) : ClientException(localeProvider, MtExceptionCode.SERVICE_NOT_FOUND, null, cause) 20 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/client/WrongPresentationRequestException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception.client 6 | 7 | import com.sirloin.sandbox.server.core.exception.ClientException 8 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | 11 | /** 12 | * 지원하지 않는 형식의 결과를 반환하라는 요청을 받았을 때 발생시킬 예외 13 | * 14 | * @since 2022-02-14 15 | */ 16 | class WrongPresentationRequestException( 17 | localeProvider: LocaleProvider, 18 | override val cause: Throwable? = null 19 | ) : ClientException(localeProvider, MtExceptionCode.WRONG_PRESENTATION, null, cause) 20 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/ResponseV1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1 6 | 7 | import com.sirloin.sandbox.server.api.advice.responseDecorator.V1ResponseDecorator 8 | 9 | /** 10 | * [V1ResponseDecorator] 는 이 Annotation 을 붙인 클래스들을 JSON 형태로 직렬화합니다. 11 | * 12 | * 클라이언트 응답으로 내려줄 클래스는 가급적 불변 `data class` 으로 선언해 주시기 바랍니다. 13 | * 14 | * @since 2022-02-14 15 | * @see V1ResponseDecorator 16 | */ 17 | @Target( 18 | AnnotationTarget.CLASS, 19 | AnnotationTarget.FIELD, 20 | AnnotationTarget.PROPERTY, 21 | AnnotationTarget.PROPERTY_GETTER, 22 | ) 23 | @Retention(AnnotationRetention.RUNTIME) 24 | @MustBeDocumented 25 | annotation class ResponseV1 26 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/exception/UserNotFoundException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.exception 6 | 7 | import com.sirloin.sandbox.server.core.exception.ClientException 8 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | 11 | /** 12 | * User 도메인 모델이 필요하지만, 없는 상황에 발생하는 예외. 13 | * 14 | * @since 2022-02-14 15 | */ 16 | class UserNotFoundException constructor( 17 | localeProvider: LocaleProvider, 18 | parameter: Any? = null, 19 | override val cause: Throwable? = null 20 | ) : ClientException(localeProvider, MtExceptionCode.USER_NOT_FOUND, parameter, cause) 21 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/client/WrongInputException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception.client 6 | 7 | import com.sirloin.sandbox.server.core.exception.ClientException 8 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | 11 | /** 12 | * 내용은 해독했지만, 문맥이나 타입에 맞지 않는 입력을 받았을 때 발생시킬 예외 13 | * 14 | * @since 2022-02-14 15 | * @see MalformedInputException 16 | */ 17 | class WrongInputException constructor( 18 | localeProvider: LocaleProvider, 19 | value: Any, 20 | override val cause: Throwable? = null 21 | ) : ClientException(localeProvider, MtExceptionCode.WRONG_INPUT, value, cause) 22 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/AppProfile.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api 6 | 7 | /** 8 | * build.gradle 과 연동한 App Build Profile. 9 | * [profileName] 으로 정의하는 문자열은 모두 packaging.gradle 파일의 내용과 일치해야 합니다. 10 | * 11 | * @since 2022-02-14 12 | */ 13 | enum class AppProfile(val profileName: String) { 14 | LOCAL(profileName = "local"), 15 | ALPHA(profileName = "alpha"), 16 | BETA(profileName = "beta"), 17 | RELEASE(profileName = "release"); 18 | 19 | companion object { 20 | fun from( 21 | profileName: String?, 22 | defaultValue: AppProfile = LOCAL 23 | ) = values().firstOrNull { it.profileName == profileName } ?: defaultValue 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/server/UnhandledException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception.server 6 | 7 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 8 | import com.sirloin.sandbox.server.core.exception.ServerException 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | 11 | /** 12 | * 미처 try - catch 로 처리하지 못한 상황에서 발생시킬 예외. 13 | * 개발자의 실수로 발생할 확률이 높으므로 이 예외 상황을 발견한다면 재현경로를 최대한 빨리 찾고, 문제를 수정해야 합니다. 14 | * 15 | * @since 2022-02-14 16 | */ 17 | class UnhandledException( 18 | localeProvider: LocaleProvider, 19 | override val cause: Throwable? = null 20 | ) : ServerException(localeProvider, MtExceptionCode.UNHANDLED_EXCEPTION, null, cause) 21 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/util/ConversionUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.util 6 | 7 | /** 8 | * HTTP Message 처리에 공통으로 활용할 로직 모음입니다. 9 | * 10 | * @since 2022-02-14 11 | */ 12 | import com.sirloin.sandbox.server.core.exception.client.WrongInputException 13 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 14 | import org.slf4j.Logger 15 | import java.util.* 16 | 17 | fun uuidStringToUuid(uuidStr: String, localeProvider: LocaleProvider, log: Logger? = null): UUID = try { 18 | UUID.fromString(uuidStr) 19 | } catch (e: IllegalArgumentException) { 20 | log?.debug("Not a uuid: {}", uuidStr) 21 | throw WrongInputException(localeProvider, uuidStr, e) 22 | } 23 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/response/DeletedUserResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1.user.response 6 | 7 | import com.fasterxml.jackson.annotation.JsonProperty 8 | import com.fasterxml.jackson.annotation.JsonPropertyDescription 9 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 10 | import com.sirloin.sandbox.server.api.endpoint.v1.ResponseV1 11 | import java.util.* 12 | 13 | /** 14 | * @since 2022-02-14 15 | */ 16 | @ResponseV1 17 | @JsonSerialize 18 | data class DeletedUserResponse( 19 | @JsonProperty 20 | @JsonPropertyDescription(DESC_UUID) 21 | val uuid: UUID 22 | ) { 23 | companion object { 24 | const val DESC_UUID = "삭제된 이용자의 고유 id." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/api-core/src/test/kotlin/test/small/domain/user/MockUserRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package test.small.domain.user 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 9 | import java.util.* 10 | 11 | /** 12 | * Small/Medium Test 단계에서 필요시 활용할 UserRepository 의 메모리 구현체 13 | * 14 | * @since 2022-02-14 15 | */ 16 | class MockUserRepository : UserRepository { 17 | private val store = HashMap() 18 | 19 | override fun save(user: User): User { 20 | val model = User.from(user) 21 | store[user.uuid] = model 22 | 23 | return model 24 | } 25 | 26 | override fun findByUuid(uuid: UUID): User? = 27 | store[uuid]?.let { User.from(it) } 28 | } 29 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # System-dependent autogen file 2 | .DS_Store 3 | desktop.ini 4 | 5 | # IDE specific files 6 | .idea 7 | .project 8 | *.iml 9 | *.ipr 10 | *.iws 11 | 12 | # Dart what not commit 13 | # https://dart.dev/guides/libraries/private-files 14 | # by dart pub 15 | .dart_tool/ 16 | .packages 17 | build/ 18 | pubspec.lock 19 | 20 | # by dart doc 21 | doc/api/ 22 | 23 | # By code generator 24 | *.g.dart 25 | *.mocks.dart 26 | 27 | # By Flutter pub 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | 31 | ### Xcode ### 32 | ## User settings 33 | xcuserdata/ 34 | 35 | ## Xcode 8 and earlier 36 | *.xcscmblueprint 37 | *.xccheckout 38 | 39 | ### Xcode Patch ### 40 | *.xcodeproj/* 41 | !*.xcodeproj/project.pbxproj 42 | !*.xcodeproj/xcshareddata/ 43 | !*.xcworkspace/contents.xcworkspacedata 44 | /*.gcno 45 | **/xcshareddata/WorkspaceSettings.xcsettings -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/medium/SpringDataJdbcMediumTestBase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.medium 6 | 7 | import com.sirloin.sandbox.server.core.CoreApplication 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 9 | import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest 10 | import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories 11 | import test.com.sirloin.annotation.LargeTest 12 | 13 | /** 14 | * Repository 관련 로직을 테스트할 때, 번거로운 환경설정을 상속으로 해결할 수 있도록 하는 Template Class 15 | * 코드 공유를 위한 상속이므로 좋은 패턴은 아님 16 | * 17 | * @since 2022-02-14 18 | */ 19 | @LargeTest 20 | @DataJdbcTest 21 | @EnableAutoConfiguration 22 | @EnableJdbcRepositories(CoreApplication.PACKAGE_NAME) 23 | class SpringDataJdbcMediumTestBase 24 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/advice/requestDecorator/StringRequestTrimmer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.advice.requestDecorator 6 | 7 | import org.springframework.beans.propertyeditors.StringTrimmerEditor 8 | import org.springframework.web.bind.WebDataBinder 9 | import org.springframework.web.bind.annotation.InitBinder 10 | import org.springframework.web.bind.annotation.RestControllerAdvice 11 | 12 | /** 13 | * String 타입 요청 앞뒤의 공백 문자열을 모두 제거합니다. 14 | * 15 | * @since 2022-02-14 16 | */ 17 | @RestControllerAdvice 18 | class StringRequestTrimmer { 19 | @InitBinder 20 | fun registerStringRequestTrimmer(binder: WebDataBinder) { 21 | binder.registerCustomEditor(String::class.java, StringTrimmerEditor(true)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/api-core/src/test/kotlin/test/small/domain/UserTestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package test.small.domain 6 | 7 | import com.github.javafaker.Faker 8 | import com.sirloin.sandbox.server.core.domain.user.User 9 | import java.time.Instant 10 | import java.util.* 11 | 12 | fun randomUser( 13 | uuid: UUID? = null, 14 | nickname: String? = null, 15 | profileImageUrl: String? = null, 16 | createdAt: Instant? = null, 17 | updatedAt: Instant? = null, 18 | version: Long? = null 19 | ): User = with(Faker()) { 20 | User.create( 21 | uuid = uuid, 22 | nickname = nickname ?: name().username(), 23 | profileImageUrl = profileImageUrl ?: internet().image(), 24 | createdAt = createdAt, 25 | updatedAt = updatedAt, 26 | version = version 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/user/dto/request_update_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:convert'; 6 | 7 | import 'package:equatable/equatable.dart'; 8 | import 'package:meta/meta.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_serializable.dart'; 10 | 11 | @immutable 12 | class UpdateUserRequest extends Equatable implements JsonSerializable { 13 | final String? nickname; 14 | final String? profileImageUrl; 15 | 16 | const UpdateUserRequest({final this.nickname, final this.profileImageUrl}); 17 | 18 | @override 19 | List get props => [nickname, profileImageUrl]; 20 | 21 | @override 22 | bool get stringify => true; 23 | 24 | @override 25 | String toJsonString() { 26 | return jsonEncode({"nickname": nickname, "profileImageUrl": profileImageUrl}); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/large/domain/user/UserRepositoryLargeTestBase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large.domain.user 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.test.context.ContextConfiguration 10 | import testcase.large.SpringDataJdbcLargeTestBase 11 | 12 | /** 13 | * UserRepository 테스트 로직에 반복되는 부분을 추출한 Template Class 14 | * 15 | * @since 2022-02-14 16 | */ 17 | @ContextConfiguration(classes = [UserRepository::class]) 18 | class UserRepositoryLargeTestBase : SpringDataJdbcLargeTestBase() { 19 | @Autowired 20 | private lateinit var _userRepository: UserRepository 21 | 22 | protected val sut: UserRepository 23 | get() = _userRepository 24 | } 25 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/user/dto/request_create_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:convert'; 6 | 7 | import 'package:equatable/equatable.dart'; 8 | import 'package:meta/meta.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_serializable.dart'; 10 | 11 | @immutable 12 | class CreateUserRequest extends Equatable implements JsonSerializable { 13 | final String nickname; 14 | final String profileImageUrl; 15 | 16 | const CreateUserRequest({required final this.nickname, required final this.profileImageUrl}); 17 | 18 | @override 19 | List get props => [nickname, profileImageUrl]; 20 | 21 | @override 22 | bool get stringify => true; 23 | 24 | @override 25 | String toJsonString() { 26 | return jsonEncode({"nickname": nickname, "profileImageUrl": profileImageUrl}); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/user/dto/response_deleted_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/data/remote/http/http_exceptions.dart'; 8 | 9 | @immutable 10 | class DeletedUserResponse extends Equatable { 11 | final String uuid; 12 | 13 | @visibleForTesting 14 | static const keyUuid = "uuid"; 15 | 16 | const DeletedUserResponse({required this.uuid}); 17 | 18 | @override 19 | List get props => [uuid]; 20 | 21 | @override 22 | bool get stringify => true; 23 | 24 | static DeletedUserResponse fromJson(final Map jsonMap) { 25 | final uuid = jsonMap[keyUuid]; 26 | 27 | if (uuid == null) { 28 | throw JsonParseException(); 29 | } 30 | 31 | return DeletedUserResponse(uuid: uuid); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/get_profile/message_get_profile.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 8 | 9 | abstract class GetProfileBlocMessage {} 10 | 11 | @immutable 12 | class UserDataSetMessage extends Equatable implements GetProfileBlocMessage { 13 | final User user; 14 | 15 | const UserDataSetMessage(this.user); 16 | 17 | @override 18 | List get props => [user]; 19 | 20 | @override 21 | bool get stringify => true; 22 | } 23 | 24 | @immutable 25 | class ErrorMessage extends Equatable implements GetProfileBlocMessage { 26 | final Exception? exception; 27 | 28 | const ErrorMessage(this.exception); 29 | 30 | @override 31 | List get props => [exception]; 32 | 33 | @override 34 | bool get stringify => true; 35 | } 36 | -------------------------------------------------------------------------------- /server/api-main/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | # 2 | # sirloin-sandbox-server 3 | # Distributed under CC BY-NC-SA 4 | # 5 | spring: 6 | main: 7 | allow-bean-definition-overriding: true 8 | application: 9 | name: sirloin-sandbox-server 10 | profiles: 11 | active: test 12 | sql: 13 | init: 14 | mode: 15 | schema-locations: classpath:/sql/v1.0/schema/*.sql 16 | # data-locations: classpath:/sql/v1.0/data/*.sql 17 | datasource: 18 | # Automatic database initialisation. Maybe conflict to hibernate. 19 | # https://docs.spring.io/spring-boot/docs/current/reference/html/howto-database-initialization.html 20 | type: com.zaxxer.hikari.HikariDataSource 21 | driver-class-name: org.h2.Driver 22 | url: jdbc:h2:mem:testdb;MODE=MySQL;CASE_INSENSITIVE_IDENTIFIERS=TRUE; 23 | username: sa 24 | password: password 25 | 26 | logging: 27 | level: 28 | ROOT: INFO 29 | com.sirloin.sandbox.server: DEBUG 30 | -------------------------------------------------------------------------------- /client/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sirloin_sandbox_client", 3 | "short_name": "sirloin_sandbox_client", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Sir.LOIN Sandbox Client", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /client/lib/di/app/di_app_components.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:logger/logger.dart'; 6 | import 'package:sirloin_sandbox_client/data/remote/http/debug_logging_http_client.dart'; 7 | import 'package:sirloin_sandbox_client/data/remote/http/http_client.dart'; 8 | import 'package:sirloin_sandbox_client/di/di_modules_helper.dart'; 9 | 10 | class AppComponentsModule { 11 | static const String _namespace = "app.components.internal"; 12 | 13 | static Future registerComponents(final DiRuleHolder ruleHolder) async { 14 | final logger = Logger(); 15 | 16 | ruleHolder.withNamespace(_namespace).registerSingleton(Logger, () => logger).registerPrototype( 17 | MtHttpClient, 18 | () => MtHttpClient.newInstance(factory: () { 19 | // POINT: Debug 모드에서만 이 httpClient 를 쓰고 싶습니다. 어떻게 해야 할까요? 20 | return DebugLoggingHttpClient(logger); 21 | })); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/http_exceptions.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:io'; 6 | 7 | class HttpStatusCodeException implements HttpException { 8 | @override 9 | final String message; 10 | @override 11 | final Uri? uri; 12 | final int statusCode; 13 | 14 | HttpStatusCodeException(this.message, this.uri, this.statusCode); 15 | } 16 | 17 | class JsonParseException extends IOException implements Exception { 18 | final String message; 19 | 20 | JsonParseException([final this.message = ""]); 21 | 22 | @override 23 | String toString() { 24 | if (message.isEmpty) { 25 | return "JsonParseException"; 26 | } else { 27 | return "JsonParseException: $message"; 28 | } 29 | } 30 | } 31 | 32 | class UnexpectedResponseException implements Exception { 33 | final String message; 34 | final String code; 35 | 36 | UnexpectedResponseException(final this.message, final this.code); 37 | } 38 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/appconfig/AppConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.appconfig 6 | 7 | import com.sirloin.sandbox.server.api.Application 8 | import org.slf4j.Logger 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | 12 | /** 13 | * Application 전체에 활용할 환경 설정 처리 로직 14 | * 15 | * @since 2022-02-14 16 | */ 17 | @Configuration 18 | class AppConfig( 19 | private val log: Logger, 20 | ) { 21 | @Bean 22 | fun buildConfig(): Application.BuildConfig { 23 | return Application.buildConfig.also { 24 | this.log.info("Build configurations - ") 25 | this.log.info(" Version: {}", it.version) 26 | this.log.info(" Fingerprint: {}", it.fingerprint) 27 | this.log.info(" Profile: {}", it.profile) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/large/domain/user/DeleteAndFindUserTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large.domain.user 6 | 7 | import org.hamcrest.CoreMatchers.`is` 8 | import org.hamcrest.CoreMatchers.nullValue 9 | import org.hamcrest.MatcherAssert.assertThat 10 | import org.junit.jupiter.api.DisplayName 11 | import org.junit.jupiter.api.Test 12 | import test.small.domain.randomUser 13 | 14 | class DeleteAndFindUserTest : UserRepositoryLargeTestBase() { 15 | @DisplayName("삭제한 Entity 는 더 이상 조회할 수 없다") 16 | @Test 17 | fun `Cannot query already deleted entity`() { 18 | // given: 19 | val savedUser = sut.save(randomUser()) 20 | 21 | // when: 22 | sut.save(savedUser.edit().delete()) 23 | 24 | // then: 25 | val nonExistentUser = sut.findByUuid(savedUser.uuid) 26 | 27 | // expect: 28 | assertThat(nonExistentUser, `is`(nullValue())) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/MtException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception 6 | 7 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 8 | import com.sirloin.sandbox.server.core.i18n.MessageProvider 9 | 10 | /** 11 | * sandbox-server 에서 발생시키는 모든 예외의 최상위 타입입니다. 12 | * 13 | * @since 2022-02-14 14 | */ 15 | open class MtException protected constructor( 16 | val localeProvider: LocaleProvider, 17 | val code: MtExceptionCode, 18 | details: Any? = null, 19 | override val cause: Throwable? = null, 20 | ) : RuntimeException( 21 | MessageProvider.hardcodedInstance().provide( 22 | /* ^^^^ 23 | * POINT: 여기서 주입하는 MessageProvider 는 하드코딩된 메시지를 가지고 있습니다. 24 | * 이 구현의 문제점을 진단하고, 개선해 보시기 바랍니다. 25 | */ 26 | localeProvider.locale, 27 | code.msgKey, 28 | details, 29 | ), 30 | cause 31 | ) 32 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/jdbc/UserEntityDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.repository.jdbc 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.UserEntity 8 | import org.springframework.data.jdbc.repository.query.Query 9 | import org.springframework.data.repository.CrudRepository 10 | import org.springframework.data.repository.query.Param 11 | import org.springframework.stereotype.Repository 12 | import java.util.* 13 | 14 | /** 15 | * User Entity 를 입출력하는 Data Access Object. 16 | * 17 | * @since 2022-02-14 18 | */ 19 | @Repository 20 | internal interface UserEntityDao : CrudRepository { 21 | @Query(""" 22 | SELECT * 23 | FROM `users` u 24 | WHERE u.`uuid` = :uuid 25 | AND u.`deleted_at` IS NULL 26 | """) 27 | fun findByUuid(@Param("uuid") uuid: UUID): UserEntity? 28 | } 29 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/common/UserServiceMixin.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.common 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.exception.UserNotFoundException 9 | import com.sirloin.sandbox.server.core.domain.user.repository.UserReadonlyRepository 10 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 11 | import java.util.* 12 | 13 | /** 14 | * User 도메인 모델 처리에 필요한 공통 로직 모음 15 | * 16 | * @since 2022-02-14 17 | */ 18 | // POINT: Mixin 이란 무엇일까요? 왜 추상 클래스가 아니라 Interface 로 구현했을까요? 19 | internal interface UserServiceMixin { 20 | val userRepo: UserReadonlyRepository 21 | val localeProvider: LocaleProvider 22 | 23 | fun getUserByUuid(uuid: UUID): User { 24 | return userRepo.findByUuid(uuid) ?: throw UserNotFoundException(localeProvider, uuid) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/api-main/src/test/kotlin/testcase/large/endpoint/v1/UserTestBaseV1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large.endpoint.v1 6 | 7 | import io.restassured.specification.RequestSpecification 8 | import org.springframework.restdocs.payload.FieldDescriptor 9 | import test.large.endpoint.v1.errorResponseFieldsSnippet 10 | 11 | class UserTestBaseV1 : LargeTestBaseV1() { 12 | fun RequestSpecification.withDocumentation( 13 | reqFields: List? = null, 14 | respFields: List? = null, 15 | ): RequestSpecification = 16 | this.withDocumentation(DOCUMENT_PREFIX, reqFields, respFields) 17 | 18 | 19 | fun RequestSpecification.withErrorDocumentation( 20 | reqFields: List? = null 21 | ): RequestSpecification = 22 | withDocumentation(DOCUMENT_PREFIX, reqFields, errorResponseFieldsSnippet()) 23 | 24 | companion object { 25 | const val DOCUMENT_PREFIX = "user" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/edit_profile/message_edit_profile.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 8 | 9 | abstract class EditProfileBlocMessage {} 10 | 11 | @immutable 12 | class UserDataSetMessage extends Equatable implements EditProfileBlocMessage { 13 | final User user; 14 | 15 | const UserDataSetMessage(this.user); 16 | 17 | @override 18 | List get props => [user]; 19 | 20 | @override 21 | bool get stringify => true; 22 | } 23 | 24 | @immutable 25 | class ProceedEditProfileMessage extends Equatable implements EditProfileBlocMessage { 26 | final String nickname; 27 | final String profileImageUrl; 28 | 29 | const ProceedEditProfileMessage(this.nickname, this.profileImageUrl); 30 | 31 | @override 32 | List get props => [nickname, profileImageUrl]; 33 | 34 | @override 35 | bool get stringify => true; 36 | } 37 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/appconfig/JsonCodecConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.appconfig 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper 8 | import com.fasterxml.jackson.databind.module.SimpleModule 9 | import com.sirloin.sandbox.server.api.advice.responseDecorator.InstantResponseDecorator 10 | import org.springframework.beans.factory.InitializingBean 11 | import org.springframework.context.annotation.Configuration 12 | import java.time.Instant 13 | 14 | /** 15 | * JSON 직렬화에 활용할 추가 처리 로직 모음 16 | * 17 | * @since 2022-02-14 18 | */ 19 | @Configuration 20 | class JsonCodecConfig( 21 | private val defaultObjectMapper: ObjectMapper 22 | ) : InitializingBean { 23 | override fun afterPropertiesSet() { 24 | val simpleModule = SimpleModule() 25 | simpleModule.addSerializer(Instant::class.java, InstantResponseDecorator()) 26 | 27 | defaultObjectMapper.registerModules(simpleModule); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/common/dto/response_error.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/data/remote/http/http_exceptions.dart'; 8 | 9 | @immutable 10 | class ErrorResponse extends Equatable { 11 | final String message; 12 | final String code; 13 | 14 | @visibleForTesting 15 | static const keyMessage = "message"; 16 | @visibleForTesting 17 | static const keyCode = "code"; 18 | 19 | const ErrorResponse(this.message, this.code); 20 | 21 | @override 22 | List get props => [message, code]; 23 | 24 | @override 25 | bool get stringify => true; 26 | 27 | static ErrorResponse fromJson(final Map jsonMap) { 28 | final message = jsonMap[keyMessage]; 29 | final code = jsonMap[keyCode]; 30 | 31 | if (message == null || code == null) { 32 | throw JsonParseException(); 33 | } 34 | 35 | return ErrorResponse(message, code); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/common/randomiser.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:faker/faker.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_serializable.dart'; 9 | 10 | typedef OnToJsonString = String Function(); 11 | 12 | JsonSerializable anyJsonSerializable({final OnToJsonString? toJsonString}) { 13 | if (toJsonString == null) { 14 | final word = Faker().lorem.word(); 15 | return _SimpleJsonSerializable(() => word); 16 | } else { 17 | return _SimpleJsonSerializable(toJsonString); 18 | } 19 | } 20 | 21 | @immutable 22 | class _SimpleJsonSerializable extends Equatable implements JsonSerializable { 23 | final OnToJsonString onToJsonString; 24 | 25 | const _SimpleJsonSerializable(this.onToJsonString); 26 | 27 | @override 28 | String toJsonString() { 29 | return onToJsonString.call(); 30 | } 31 | 32 | @override 33 | List get props => [onToJsonString]; 34 | } 35 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/user/dto/randomiser_response_deleted_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_exportable.dart'; 6 | import 'package:sirloin_sandbox_client/data/remote/http/user/dto/response_deleted_user.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | DeletedUserResponse randomDeletedUserResponse({final String? uuid}) { 10 | return DeletedUserResponse(uuid: uuid ?? const Uuid().v4().toString()); 11 | } 12 | 13 | MockDeletedUserResponse mockDeletedUserResponse() { 14 | return MockDeletedUserResponse(randomDeletedUserResponse()); 15 | } 16 | 17 | class MockDeletedUserResponse implements JsonExportable { 18 | String? _uuid; 19 | 20 | MockDeletedUserResponse(final DeletedUserResponse src) : _uuid = src.uuid; 21 | 22 | MockDeletedUserResponse uuid(final String? uuid) { 23 | _uuid = uuid; 24 | return this; 25 | } 26 | 27 | @override 28 | Map toJson() { 29 | return {DeletedUserResponse.keyUuid: _uuid}; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/UserRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.repository 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.UserEntity 9 | import com.sirloin.sandbox.server.core.domain.user.repository.jdbc.UserEntityDao 10 | import org.springframework.beans.factory.annotation.Qualifier 11 | import org.springframework.stereotype.Component 12 | 13 | /** 14 | * @since 2022-02-14 15 | * @see UserReadonlyRepositoryImpl 16 | */ 17 | @Component(UserRepository.NAME) 18 | internal class UserRepositoryImpl( 19 | @Qualifier(UserReadonlyRepository.NAME) private val userReadonlyRepository: UserReadonlyRepository, 20 | private val userDao: UserEntityDao 21 | ) : UserReadonlyRepository by userReadonlyRepository, UserRepository { 22 | override fun save(user: User): User { 23 | return userDao.save(UserEntity.from(user)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/large/domain/user/SaveAndFindUserTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large.domain.user 6 | 7 | import com.sirloin.jvmlib.time.truncateToSeconds 8 | import org.hamcrest.CoreMatchers.`is` 9 | import org.hamcrest.MatcherAssert.assertThat 10 | import org.junit.jupiter.api.DisplayName 11 | import org.junit.jupiter.api.Test 12 | import test.small.domain.randomUser 13 | import java.time.Instant 14 | 15 | class SaveAndFindUserTest : UserRepositoryLargeTestBase() { 16 | @DisplayName("저장한 Entity model 을 검색할 수 있다") 17 | @Test 18 | fun `Should find by query after entity model is saved`() { 19 | // given: 20 | val now = Instant.now().truncateToSeconds() 21 | val user = randomUser(createdAt = now, updatedAt = now) 22 | 23 | // when: 24 | val savedUser = sut.save(user) 25 | 26 | // then: 27 | val foundUser = sut.findByUuid(savedUser.uuid) 28 | 29 | // expect: 30 | assertThat(foundUser, `is`(savedUser)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/lib/client_application.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_i18n/flutter_i18n.dart'; 7 | import 'package:flutter_localizations/flutter_localizations.dart'; 8 | import 'package:sirloin_sandbox_client/screen/screen_splash.dart'; 9 | 10 | class ClientApplication extends StatelessWidget { 11 | final FlutterI18nDelegate flutterI18nDelegate; 12 | 13 | const ClientApplication({Key? key, required this.flutterI18nDelegate}) : super(key: key); 14 | 15 | @override 16 | Widget build(final BuildContext context) { 17 | return MaterialApp( 18 | // POINT 여기 App 이름을 Locale 에 맞게 바꾸려면 어떻게 해야 할까요? 19 | title: "sirloin-sandbox-client", 20 | theme: ThemeData( 21 | primarySwatch: Colors.blue, 22 | ), 23 | home: const SplashScreen(), 24 | localizationsDelegates: [ 25 | flutterI18nDelegate, 26 | GlobalMaterialLocalizations.delegate, 27 | GlobalWidgetsLocalizations.delegate 28 | ], 29 | builder: FlutterI18n.rootAppBuilder(), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/advice/responseDecorator/InstantResponseDecorator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.advice.responseDecorator 6 | 7 | import com.fasterxml.jackson.core.JsonGenerator 8 | import com.fasterxml.jackson.databind.JsonSerializer 9 | import com.fasterxml.jackson.databind.SerializerProvider 10 | import com.sirloin.jvmlib.time.truncateToSeconds 11 | import java.time.Instant 12 | import java.time.ZoneOffset 13 | import java.time.format.DateTimeFormatter 14 | 15 | /** 16 | * [Instant] 의 값을 ISO format 에 맞게 초 단위로 자르고, UTC 기준으로 변환합니다. 17 | * 외부 시스템에서 Microseconds 이하 단위를 유용하게 사용하는 일반적인 경우가 많지 않기 때문입니다. 18 | * 19 | * @since 2022-02-14 20 | */ 21 | class InstantResponseDecorator : JsonSerializer() { 22 | override fun serialize(value: Instant?, gen: JsonGenerator?, serializers: SerializerProvider?) { 23 | gen?.writeString( 24 | DateTimeFormatter.ISO_DATE_TIME.format( 25 | value?.truncateToSeconds() 26 | ?.atOffset(ZoneOffset.UTC) 27 | ) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/appconfig/LoggerConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.appconfig 6 | 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import org.springframework.beans.factory.InjectionPoint 10 | import org.springframework.beans.factory.config.ConfigurableBeanFactory 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | import org.springframework.context.annotation.Scope 14 | 15 | /** 16 | * [org.slf4j.Logger] 를 필요로 하는 Spring component 들에, 17 | * 매번 새로운 Logger 인스턴스를 생성해서 주입하는 환경 설정 18 | * 19 | * 이 설정으로 인해 자동으로 주입받는 [org.slf4j.Logger] 인스턴스는 모두 싱글턴이 아닙니다. 20 | * 21 | * @since 2022-02-14 22 | */ 23 | @Configuration 24 | class LoggerConfig { 25 | @Bean 26 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 27 | fun logger(injectionPoint: InjectionPoint): Logger { 28 | return LoggerFactory.getLogger( 29 | injectionPoint.methodParameter?.containingClass ?: injectionPoint.field?.declaringClass 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/small/converter/BytesUuidConverterSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.small.converter 6 | 7 | import com.sirloin.sandbox.server.core.converter.BytesUuidConverters 8 | import org.hamcrest.CoreMatchers.`is` 9 | import org.hamcrest.MatcherAssert.assertThat 10 | import org.junit.jupiter.api.DisplayName 11 | import org.junit.jupiter.api.Test 12 | import test.com.sirloin.annotation.SmallTest 13 | import java.util.* 14 | 15 | @SmallTest 16 | class BytesUuidConverterSpec { 17 | @DisplayName("UUID 를 Byte array 로, 결과를 다시 UUID 로 변환할 수 있다") 18 | @Test 19 | fun `Can convert UUID as ByteArray and vice versa`() { 20 | // given: 21 | val uuidToByteArray = BytesUuidConverters.WRITE_CONVERTER 22 | val byteArrayToUuid = BytesUuidConverters.READ_CONVERTER 23 | 24 | // when: 25 | val uuid = UUID.randomUUID() 26 | 27 | // then: 28 | val byteArray = uuidToByteArray.convert(uuid) 29 | val maybeUuid = byteArrayToUuid.convert(byteArray!!) 30 | 31 | // expect: 32 | assertThat(maybeUuid, `is`(uuid)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/appconfig/WebMvcConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.appconfig 6 | 7 | import com.sirloin.sandbox.server.api.advice.AcceptLanguageLocaleProvider 8 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | import org.springframework.context.annotation.Scope 12 | import org.springframework.context.annotation.ScopedProxyMode 13 | import org.springframework.http.HttpHeaders 14 | import org.springframework.web.context.WebApplicationContext 15 | import javax.servlet.http.HttpServletRequest 16 | 17 | /** 18 | * Spring WebMVC 환경 설정 모음 19 | * 20 | * @since 2022-02-14 21 | */ 22 | @Configuration 23 | class WebMvcConfig { 24 | @Bean 25 | @Scope(WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 26 | fun acceptLanguageLocaleProvider( 27 | request: HttpServletRequest 28 | ): LocaleProvider = 29 | AcceptLanguageLocaleProvider(request.getHeader(HttpHeaders.ACCEPT_LANGUAGE)) 30 | } 31 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/advice/AcceptLanguageLocaleProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.advice 6 | 7 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 8 | import java.util.* 9 | 10 | /** 11 | * 요청의 `Accept-Language` 헤더를 분석해 요청과 가장 유사한 Locale 을 생성하는 [LocaleProvider] 구현체 12 | * 13 | * @since 2022-02-14 14 | */ 15 | class AcceptLanguageLocaleProvider( 16 | acceptLanguage: String?, 17 | ) : LocaleProvider { 18 | private val _locale: Locale = acceptLanguage?.let { 19 | // Java locale code 는 BCP 47 형식을 따르지 않기 때문에, _(underscore) 를 -(dash) 로 변환 20 | val maybeBcp47Format = it.replace("_", "-") 21 | 22 | @Suppress("SwallowedException") // 치명적인 오류가 아니기 때문에 예외 전파 하지 않는다. 23 | return@let try { 24 | Locale.LanguageRange.parse(maybeBcp47Format) 25 | } catch (e: IllegalArgumentException) { 26 | null 27 | }?.run { 28 | LocaleProvider.matchSupportedLocale(this) 29 | } 30 | } ?: LocaleProvider.DEFAULT 31 | 32 | override val locale: Locale 33 | get() = _locale 34 | } 35 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | # 2 | # sirloin-sandbox-server 3 | # Distributed under CC BY-NC-SA 4 | # 5 | spring: 6 | main: 7 | allow-bean-definition-overriding: true 8 | application: 9 | name: sirloin-sandbox-server 10 | profiles: 11 | active: test 12 | sql: 13 | init: 14 | # 원래는 ALWAYS 로 설정하는게 맞지만, @DataJdbcTest 가 생성하는 h2 database 초기 설정은 multi-line SQL 을 제대로 읽지 못해 15 | # Test 가 실패하는 문제가 있다. 이 문제로 인해 설정값을 직접 testcase.large.SpringDataJdbcTestConfig 에서 초기화 하도록 구현했다. 16 | # 자세한 내용은 SpringDataJdbcTestConfig 의 KDoc 을 참고하세요. 17 | mode: NEVER 18 | schema-locations: classpath:/sql/v1.0/schema/*.sql 19 | data-locations: classpath:/sql/v1.0/data/*.sql 20 | datasource: 21 | # Automatic database initialisation. Maybe conflict to hibernate. 22 | # https://docs.spring.io/spring-boot/docs/current/reference/html/howto-database-initialization.html 23 | type: com.zaxxer.hikari.HikariDataSource 24 | driver-class-name: org.h2.Driver 25 | url: jdbc:h2:mem:testdb;MODE=MySQL;CASE_INSENSITIVE_IDENTIFIERS=TRUE; 26 | username: sa 27 | password: password 28 | 29 | logging: 30 | level: 31 | ROOT: INFO 32 | com.sirloin.sandbox.server: DEBUG 33 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/common/dto/mock_error_response.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:convert'; 6 | 7 | import 'package:sirloin_sandbox_client/data/remote/http/common/dto/response_error.dart'; 8 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_exportable.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_serializable.dart'; 10 | 11 | class MockErrorResponse implements JsonSerializable, JsonExportable { 12 | String? _message; 13 | String? _code; 14 | 15 | MockErrorResponse(final ErrorResponse errorResponse) 16 | : _message = errorResponse.message, 17 | _code = errorResponse.code; 18 | 19 | MockErrorResponse message(final String? message) { 20 | _message = message; 21 | return this; 22 | } 23 | 24 | MockErrorResponse code(final String? code) { 25 | _code = code; 26 | return this; 27 | } 28 | 29 | @override 30 | Map toJson() { 31 | return { 32 | ErrorResponse.keyMessage: _message, 33 | ErrorResponse.keyCode: _code, 34 | }; 35 | } 36 | 37 | @override 38 | String toJsonString() { 39 | return jsonEncode(toJson()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/converter/BytesUuidConverters.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.converter 6 | 7 | import com.sirloin.jvmlib.util.toByteArray 8 | import com.sirloin.jvmlib.util.toUUID 9 | import org.springframework.core.convert.converter.Converter 10 | import org.springframework.data.convert.ReadingConverter 11 | import org.springframework.data.convert.WritingConverter 12 | import java.util.* 13 | 14 | /** 15 | * Uuid 를 16 바이트 길이의 ByteArray 로, 또 반대로 변환합니다. 16 | * Database 저장 또는 전송 로직에 활용할 수 있습니다. 17 | * 18 | * @since 2022-02-14 19 | */ 20 | object BytesUuidConverters { 21 | val READ_CONVERTER: Converter = BytesToUuidConverter() 22 | val WRITE_CONVERTER: Converter = UuidToBytesConverter() 23 | } 24 | 25 | @ReadingConverter 26 | private class BytesToUuidConverter : Converter { 27 | override fun convert(source: ByteArray): UUID = source.toUUID() 28 | } 29 | 30 | @WritingConverter 31 | private class UuidToBytesConverter : Converter { 32 | override fun convert(source: UUID): ByteArray = source.toByteArray() 33 | } 34 | -------------------------------------------------------------------------------- /server/api-core/src/test/kotlin/testcase/small/domain/user/UserSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.small.domain.user 6 | 7 | import com.sirloin.jvmlib.time.truncateToSeconds 8 | import org.hamcrest.CoreMatchers.`is` 9 | import org.hamcrest.MatcherAssert.assertThat 10 | import org.hamcrest.Matchers.greaterThanOrEqualTo 11 | import org.junit.jupiter.api.DisplayName 12 | import org.junit.jupiter.api.Test 13 | import org.junit.jupiter.api.assertAll 14 | import test.com.sirloin.annotation.SmallTest 15 | import test.small.domain.randomUser 16 | import java.time.Instant 17 | 18 | @SmallTest 19 | class UserSpec { 20 | @DisplayName("생성한 User 를 삭제하면 삭제 시점의 시간이 기록된다") 21 | @Test 22 | fun `Timestamp is saved when deleting a user`() { 23 | // given: 24 | val user = randomUser() 25 | 26 | // when: 27 | val beforeDelete = Instant.now().truncateToSeconds() 28 | val deletedUser = user.edit().delete() 29 | 30 | // then: 31 | assertAll( 32 | { assertThat(deletedUser.isDeleted, `is`(true)) }, 33 | { assertThat(deletedUser.deletedAt, greaterThanOrEqualTo(beforeDelete)) } 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/lib/domain/user/user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | 8 | abstract class User { 9 | final String uuid; 10 | final String nickname; 11 | final String profileImageUrl; 12 | 13 | User({ 14 | required this.uuid, 15 | required this.nickname, 16 | required this.profileImageUrl, 17 | }); 18 | 19 | static User create({ 20 | required final String uuid, 21 | required final String nickname, 22 | required final String profileImageUrl, 23 | }) { 24 | return _UserImpl(uuid: uuid, nickname: nickname, profileImageUrl: profileImageUrl); 25 | } 26 | } 27 | 28 | @immutable 29 | class _UserImpl extends Equatable implements User { 30 | @override 31 | final String uuid; 32 | @override 33 | final String nickname; 34 | @override 35 | final String profileImageUrl; 36 | 37 | const _UserImpl({ 38 | required this.uuid, 39 | required this.nickname, 40 | required this.profileImageUrl, 41 | }) : super(); 42 | 43 | @override 44 | List get props => [uuid, nickname, profileImageUrl]; 45 | 46 | @override 47 | bool get stringify => true; 48 | } 49 | -------------------------------------------------------------------------------- /server/api-main/src/test/kotlin/test/large/endpoint/v1/user/UserApiScenarioUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package test.large.endpoint.v1.user 6 | 7 | import com.sirloin.sandbox.server.api.endpoint.v1.ApiPathsV1 8 | import com.sirloin.sandbox.server.api.endpoint.v1.user.request.CreateUserRequest 9 | import com.sirloin.sandbox.server.api.endpoint.v1.user.response.DeletedUserResponse 10 | import com.sirloin.sandbox.server.api.endpoint.v1.user.response.UserResponse 11 | import testcase.large.endpoint.v1.LargeTestBaseV1 12 | import java.util.* 13 | 14 | fun LargeTestBaseV1.createRandomUser( 15 | nickname: String? = null, 16 | profileImageUrl: String? = null 17 | ): UserResponse = 18 | jsonRequest() 19 | .body( 20 | CreateUserRequest.random( 21 | nickname = nickname, 22 | profileImageUrl = profileImageUrl 23 | ) 24 | ) 25 | .post(ApiPathsV1.USER) 26 | .expect2xx(UserResponse::class) 27 | 28 | fun LargeTestBaseV1.deleteUser( 29 | uuid: UUID 30 | ): DeletedUserResponse = 31 | jsonRequest() 32 | .delete(ApiPathsV1.userWithUuid(uuid)) 33 | .expect2xx(DeletedUserResponse::class) 34 | -------------------------------------------------------------------------------- /server/gradle/scripts/java.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply plugin: "java" 6 | 7 | sourceCompatibility = version_target_jvm 8 | targetCompatibility = version_target_jvm 9 | [compileJava, compileTestJava]*.options*.encoding = "UTF-8" 10 | 11 | task sourcesJar(type: Jar, dependsOn: classes) { 12 | archiveClassifier = "sources" 13 | from sourceSets.main.allSource 14 | } 15 | 16 | task testSourcesJar(type: Jar, dependsOn: classes) { 17 | archiveClassifier = "test-sources" 18 | from sourceSets.test.allSource 19 | } 20 | 21 | artifacts { 22 | archives sourcesJar 23 | archives testSourcesJar 24 | } 25 | 26 | dependencies { 27 | // JSR 303: Bean validation 28 | implementation "javax.validation:validation-api:$version_javax_validation" 29 | 30 | // JSR 330: Dependency Injection for Java 31 | implementation "javax.inject:javax.inject:$version_javax_inject" 32 | 33 | // JSR 305: Annotations for Software Defect Detection 34 | implementation "com.google.code.findbugs:jsr305:$version_jsr305" 35 | } 36 | 37 | gradle.projectsEvaluated { 38 | tasks.withType(JavaCompile) { 39 | options.compilerArgs << "-Xlint:all" << "-Werror" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/UserReadonlyRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.repository 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.UserEntity 9 | import com.sirloin.sandbox.server.core.domain.user.repository.jdbc.UserEntityDao 10 | import org.springframework.stereotype.Component 11 | import java.util.* 12 | 13 | /** 14 | * UserReadonlyRepository 구현체입니다. 15 | * Repository 는 여러 DAO 의 조합으로 구성할 수 있습니다. 16 | * 각 DAO 들은 다양한 data source 에 접근해 domain model 을 구성하도록 동작합니다. 17 | * 18 | * 가령 RDBMS + NoSQL 조합으로 Domain model 을 구성할 수도 있고, 19 | * Data source + Cache 조합으로 조회 성능을 끌어올리는 등의 구현도 가능합니다. 20 | * 21 | * @since 2022-02-14 22 | */ 23 | @Component(UserReadonlyRepository.NAME) 24 | internal class UserReadonlyRepositoryImpl( 25 | private val userDao: UserEntityDao 26 | ) : UserReadonlyRepository { 27 | fun findById(id: Long): UserEntity { 28 | return userDao.findById(id).get() 29 | } 30 | 31 | override fun findByUuid(uuid: UUID): User? { 32 | return userDao.findByUuid(uuid) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/appconfig/SpringDataJdbcConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.appconfig 6 | 7 | import com.sirloin.sandbox.server.core.CoreApplication 8 | import com.sirloin.sandbox.server.core.converter.BytesUuidConverters 9 | import com.sirloin.sandbox.server.core.converter.InstantStringConverters 10 | import org.springframework.context.annotation.Configuration 11 | import org.springframework.core.convert.converter.Converter 12 | import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration 13 | import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories 14 | 15 | /** 16 | * Spring data-jdbc 환경 설정입니다. 17 | * 18 | * @since 2022-02-14 19 | */ 20 | @Configuration 21 | @EnableJdbcRepositories(CoreApplication.PACKAGE_NAME) 22 | class SpringDataJdbcConfig : AbstractJdbcConfiguration() { 23 | override fun userConverters(): List> { 24 | return listOf( 25 | BytesUuidConverters.READ_CONVERTER, BytesUuidConverters.WRITE_CONVERTER, 26 | InstantStringConverters.READ_CONVERTER, InstantStringConverters.WRITE_CONVERTER 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/test/testcase/medium/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | // 자연스러운 UX 를 고려한 UI 구현 후, 새로 작성해 주시기 바랍니다. 10 | // ex) "xxx 를 하면 화면에 yyy 가 나타난다" 11 | void main() { 12 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 13 | // // Build our app and trigger a frame. 14 | // await tester.pumpWidget(const ClientApplication()); 15 | // 16 | // // Verify that our counter starts at 0. 17 | // expect(find.text('0'), findsOneWidget); 18 | // expect(find.text('1'), findsNothing); 19 | // 20 | // // Tap the '+' icon and trigger a frame. 21 | // await tester.tap(find.byIcon(Icons.add)); 22 | // await tester.pump(); 23 | // 24 | // // Verify that our counter has incremented. 25 | // expect(find.text('0'), findsNothing); 26 | // expect(find.text('1'), findsOneWidget); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/local/serialised_data_test.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:sirloin_sandbox_client/data/local/seraialised_data.dart'; 7 | 8 | void main() { 9 | test("Expiration 을 초과한 SerialisedData 는 expired = true 다.", () { 10 | // given: 11 | final savedAt = DateTime.now().subtract(const Duration(seconds: SerialisedData.defaultExpiredSeconds + 1)); 12 | final data = SerialisedDataImpl(savedAt); 13 | 14 | // then: 15 | final isExpired = data.isExpired(); 16 | 17 | // expect: 18 | expect(isExpired, equals(true)); 19 | }); 20 | 21 | test("Expiration 을 초과하지 않은 SerialisedData 는 expired = false 다.", () { 22 | // given: 23 | final savedAt = DateTime.now(); 24 | final data = SerialisedDataImpl(savedAt); 25 | 26 | // then: 27 | final isExpired = data.isExpired(); 28 | 29 | // expect: 30 | expect(isExpired, equals(false)); 31 | }); 32 | } 33 | 34 | class SerialisedDataImpl with SerialisedData { 35 | SerialisedDataImpl(final DateTime savedAt) : super() { 36 | super.savedAt = savedAt; 37 | } 38 | 39 | @override 40 | SerialisedDataImpl get() { 41 | return this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/assets/i18n/en_us.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # sirloin-sandbox-client 3 | # Distributed under CC BY-NC-SA 4 | # 5 | 6 | # Fallback 언어를 영어로 설정했습니다. 7 | # 이 앱은 한국어만 지원하니, 한국어로 작성해주세요. 8 | title: 9 | main: "Sirloin-sandbox Client" 10 | splash: "Splash" 11 | registration: "회원 가입" 12 | myProfile: "내 프로필" 13 | editProfile: "프로필 편집" 14 | 15 | splash: 16 | btn: 17 | register: "가입하기" 18 | 19 | registration: 20 | btn: 21 | register: "가입하기" 22 | text: 23 | nickname: "Nickname" 24 | nicknameHint: "최소 2자, 최대 32자 이내" 25 | profileImageUrl: "프로필 이미지 URL" 26 | error: "가입 오류" 27 | 28 | profile: 29 | btn: 30 | editProfile: "프로필 수정" 31 | deregister: "회원 탈퇴" 32 | text: 33 | userId: "User Id: {userId}" 34 | nickname: "Nickname: {nickname}" 35 | profileImageUrl: "Profile Image: {profileImageUrl}" 36 | editText: 37 | nickname: "Nickname" 38 | nicknameHint: "최소 2자, 최대 32자 이내" 39 | profileImageUrl: "프로필 이미지 URL" 40 | 41 | deregister: 42 | title: "회원 탈퇴" 43 | message: "회원 탈퇴시 모든 정보가 삭제됩니다. 정말 탈퇴하시겠습니까?" 44 | btn: 45 | deregister: "회원 탈퇴" 46 | 47 | text: 48 | loading: "불러오는 중" 49 | btn: 50 | confirm: "확인" 51 | cancel: "취소" 52 | 53 | error: 54 | common: 55 | message: "API 서버와의 통신 중 오류가 발생했습니다." 56 | messageWithFormat: "API 서버와의 통신 중 오류가 발생했습니다. ({message})" 57 | -------------------------------------------------------------------------------- /client/lib/di/domain/user/di_user_repository.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:logger/logger.dart'; 6 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 7 | import 'package:sirloin_sandbox_client/data/remote/http/user/api_user.dart'; 8 | import 'package:sirloin_sandbox_client/data/repository/user/repo_user_impl.dart'; 9 | import 'package:sirloin_sandbox_client/di/di_modules_helper.dart'; 10 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_ro_user.dart'; 11 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_user.dart'; 12 | 13 | class UserRepositoryModule { 14 | static const String _namespace = "domain.user"; 15 | 16 | static Future registerComponents(final DiRuleHolder ruleHolder) async { 17 | final Logger logger = ruleHolder.getSingleton(Logger); 18 | final userLocalStorage = ruleHolder.getSingleton(UserLocalStorage); 19 | final userApi = ruleHolder.getSingleton(UserApi); 20 | 21 | final userRepositoryImpl = UserRepositoryImpl(userLocalStorage, userApi, logger); 22 | 23 | ruleHolder 24 | .withNamespace(_namespace) 25 | .registerSingleton(UserReadonlyRepository, () => userRepositoryImpl) 26 | .registerSingleton(UserRepository, () => userRepositoryImpl); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/lib/di/domain/user/di_user_data.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:localstorage/localstorage.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 8 | import 'package:sirloin_sandbox_client/data/remote/http/http_client.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/user/api_user.dart'; 10 | import 'package:sirloin_sandbox_client/di/app/di_app_constants.dart'; 11 | import 'package:sirloin_sandbox_client/di/di_modules_helper.dart'; 12 | 13 | class UserDataModule { 14 | static const String _namespace = "data.user"; 15 | static const String _userLocalStorage = "local_users"; 16 | 17 | static Future registerComponents(final DiRuleHolder ruleHolder) async { 18 | final logger = ruleHolder.getSingleton(Logger); 19 | final baseApiUrl = ruleHolder.getSingleton(String, AppConstantsModule.qualifierBaseApiUrl); 20 | final httpClient = ruleHolder.getPrototype(MtHttpClient); 21 | 22 | ruleHolder 23 | .withNamespace(_namespace) 24 | .registerSingleton(UserApi, () => UserApi.newInstance("$baseApiUrl/v1/user", httpClient)) 25 | .registerSingleton( 26 | UserLocalStorage, () => UserLocalStorage.newInstance(LocalStorage(_userLocalStorage), logger)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/main/kotlin/com/sirloin/sandbox/server/core/converter/InstantStringConverters.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.converter 6 | 7 | import com.sirloin.jvmlib.time.truncateToSeconds 8 | import org.springframework.core.convert.converter.Converter 9 | import org.springframework.data.convert.ReadingConverter 10 | import org.springframework.data.convert.WritingConverter 11 | import java.time.Instant 12 | import java.time.format.DateTimeFormatter 13 | 14 | /** 15 | * Instant 타입 데이터를 문자열 <-> Instant 로 변환할 때 활용합니다. 16 | * 밀리초 이하 단위를 제거할 때도 사용합니다. 17 | * 18 | * @since 2022-02-14 19 | */ 20 | object InstantStringConverters { 21 | val READ_CONVERTER: Converter = StringToInstantConverter() 22 | val WRITE_CONVERTER: Converter = InstantToStringConverter() 23 | } 24 | 25 | @ReadingConverter 26 | private class StringToInstantConverter : Converter { 27 | override fun convert(source: String): Instant = Instant.parse(source).truncateToSeconds() 28 | } 29 | 30 | @WritingConverter 31 | private class InstantToStringConverter : Converter { 32 | override fun convert(source: Instant): String = DateTimeFormatter.ISO_DATE_TIME.format( 33 | source.truncateToSeconds() 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/request/CreateUserRequest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1.user.request 6 | 7 | import com.fasterxml.jackson.annotation.JsonProperty 8 | import com.fasterxml.jackson.annotation.JsonPropertyDescription 9 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize 10 | import com.sirloin.sandbox.server.api.validation.UnicodeCharsLength 11 | import com.sirloin.sandbox.server.core.domain.user.User 12 | 13 | /** 14 | * @since 2022-02-14 15 | */ 16 | @JsonDeserialize 17 | data class CreateUserRequest( 18 | @get:UnicodeCharsLength( 19 | min = User.NICKNAME_SIZE_MIN, 20 | max = User.NICKNAME_SIZE_MAX, 21 | message = "`nickname` must between ${User.NICKNAME_SIZE_MIN} and ${User.NICKNAME_SIZE_MAX} characters." 22 | ) 23 | @JsonProperty 24 | @JsonPropertyDescription(DESC_NICKNAME) 25 | val nickname: String, 26 | 27 | @JsonProperty 28 | @JsonPropertyDescription(DESC_PROFILE_IMAGE_URL) 29 | val profileImageUrl: String 30 | ) { 31 | companion object { 32 | const val DESC_NICKNAME = "${User.NICKNAME_SIZE_MIN}자 이상, ${User.NICKNAME_SIZE_MAX}자 이하의 이용자 닉네임." 33 | const val DESC_PROFILE_IMAGE_URL = "Profile image URL" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/api-main/src/test/kotlin/test/large/endpoint/v1/user/UserApiRequestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package test.large.endpoint.v1.user 6 | 7 | import com.github.javafaker.Faker 8 | import com.sirloin.sandbox.server.api.endpoint.v1.user.request.CreateUserRequest 9 | import com.sirloin.sandbox.server.api.endpoint.v1.user.request.UpdateUserRequest 10 | 11 | private const val DEFAULT_STRING = "__DEFAULT__" 12 | 13 | fun CreateUserRequest.Companion.random( 14 | nickname: String? = null, 15 | profileImageUrl: String? = null 16 | ): CreateUserRequest = with(Faker()) { 17 | return CreateUserRequest( 18 | nickname = nickname ?: name().username(), 19 | profileImageUrl = profileImageUrl ?: internet().image() 20 | ) 21 | } 22 | 23 | fun UpdateUserRequest.Companion.random( 24 | nickname: String? = DEFAULT_STRING, 25 | profileImageUrl: String? = DEFAULT_STRING 26 | ): UpdateUserRequest = with(Faker()) { 27 | return UpdateUserRequest( 28 | nickname = if (nickname == DEFAULT_STRING) { 29 | name().username() 30 | } else { 31 | nickname 32 | }, 33 | profileImageUrl = if (profileImageUrl == DEFAULT_STRING) { 34 | internet().image() 35 | } else { 36 | profileImageUrl 37 | } 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/request/UpdateUserRequest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1.user.request 6 | 7 | import com.fasterxml.jackson.annotation.JsonProperty 8 | import com.fasterxml.jackson.annotation.JsonPropertyDescription 9 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize 10 | import com.sirloin.sandbox.server.api.validation.UnicodeCharsLength 11 | import com.sirloin.sandbox.server.core.domain.user.User 12 | 13 | /** 14 | * @since 2022-02-14 15 | */ 16 | @JsonDeserialize 17 | data class UpdateUserRequest( 18 | @get:UnicodeCharsLength( 19 | min = User.NICKNAME_SIZE_MIN, 20 | max = User.NICKNAME_SIZE_MAX, 21 | message = "`nickname` must between ${User.NICKNAME_SIZE_MIN} and ${User.NICKNAME_SIZE_MAX} characters.", 22 | ) 23 | @JsonProperty 24 | @JsonPropertyDescription(DESC_NICKNAME) 25 | val nickname: String?, 26 | 27 | @JsonProperty 28 | @JsonPropertyDescription(DESC_PROFILE_IMAGE_URL) 29 | val profileImageUrl: String? 30 | ) { 31 | companion object { 32 | const val DESC_NICKNAME = "${User.NICKNAME_SIZE_MIN}자 이상, ${User.NICKNAME_SIZE_MAX}자 이하의 이용자 닉네임." 33 | const val DESC_PROFILE_IMAGE_URL = "Profile image URL" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/exception/MtExceptionCode.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.exception 6 | 7 | /** 8 | * Business logic 에서 발생한 예외 상황을 코드로 표현합니다. 9 | * 여기 새 코드를 추가한 경우, MessageProvider 가 코드에 맞는 오류 메시지를 출력하도록 추가 작업을 해야 합니다. 10 | * 11 | * @since 2022-02-14 12 | */ 13 | enum class MtExceptionCode( 14 | val value: Long, 15 | val msgKey: String 16 | ) { 17 | // region 비즈니스 독립적인 일반 예외 코드 (0x00000001 - 0x0fffffff) 18 | WRONG_INPUT(value = 0x00000001, msgKey = "ERR_WRONG_INPUT"), 19 | SERVICE_NOT_FOUND(value = 0x00000002, msgKey = "ERR_SERVICE_NOT_FOUND"), 20 | WRONG_PRESENTATION(value = 0x00000003, msgKey = "ERR_WRONG_PRESENTATION"), 21 | MALFORMED_INPUT(value = 0x00000004, msgKey = "ERR_MALFORMED_INPUT"), 22 | // endregion 23 | 24 | // region 특정 비즈니스 종속 예외 코드 (0x10000000 - 0xfffffffe) 25 | USER_NOT_FOUND(value = 0x10000000, msgKey = "ERR_USER_NOT_FOUND"), 26 | // endregion 27 | 28 | // region 미처리 오류 29 | UNHANDLED_EXCEPTION(value = 0xffffffff, msgKey = "ERR_UNHANDLED_EXCEPTION"), 30 | UNDEFINED(value = 0x00000000, msgKey = ""); 31 | // endregion 32 | 33 | companion object { 34 | fun from(value: Long?): MtExceptionCode = values().firstOrNull { it.value == value } ?: UNDEFINED 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/service/DeleteUserService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.service 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.common.UserServiceMixin 9 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 10 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 11 | import java.util.* 12 | 13 | /** 14 | * User 도메인 모델 제거 관련 로직을 담당하는 Service interface 15 | * 16 | * @since 2022-02-14 17 | */ 18 | interface DeleteUserService { 19 | fun deleteUserByUuid(uuid: UUID): User 20 | 21 | companion object { 22 | fun newInstance( 23 | userRepo: UserRepository, 24 | localeProvider: LocaleProvider 25 | ): DeleteUserService = DeleteUserServiceImpl(userRepo, localeProvider) 26 | } 27 | } 28 | 29 | class DeleteUserServiceImpl( 30 | override val userRepo: UserRepository, 31 | override val localeProvider: LocaleProvider 32 | ) : DeleteUserService, UserServiceMixin { 33 | override fun deleteUserByUuid(uuid: UUID): User { 34 | val user = super.getUserByUuid(uuid) 35 | 36 | return userRepo.save(user.edit().apply { 37 | this.delete() 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/api-main/src/alpha/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID} [%16.-16t] %-40.40logger{39} : %msg%n 8 | 9 | 10 | ${LOG_PATH}/${spring.application.name}.log 11 | 12 | ${LOG_PATH}/archives/${spring.application.name}-%d{yyyy-MM-dd}.%i.log.gz 13 | 200MB 14 | 30 15 | 5GB 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server/api-main/src/beta/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID} [%16.-16t] %-40.40logger{39} : %msg%n 8 | 9 | 10 | ${LOG_PATH}/${spring.application.name}.log 11 | 12 | ${LOG_PATH}/archives/${spring.application.name}-%d{yyyy-MM-dd}.%i.log.gz 13 | 200MB 14 | 30 15 | 5GB 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server/api-main/src/release/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID} [%16.-16t] %-40.40logger{39} : %msg%n 8 | 9 | 10 | ${LOG_PATH}/${spring.application.name}.log 11 | 12 | ${LOG_PATH}/archives/${spring.application.name}-%d{yyyy-MM-dd}.%i.log.gz 13 | 200MB 14 | 30 15 | 5GB 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/ErrorResponseV1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1 6 | 7 | import com.fasterxml.jackson.annotation.JsonPropertyDescription 8 | import com.sirloin.sandbox.server.core.exception.MtException 9 | 10 | data class ErrorResponseV1(override val body: Body) : ResponseEnvelopeV1(Type.ERROR) { 11 | data class Body( 12 | @JsonPropertyDescription(DESC_BODY_MESSAGE) 13 | val message: String, 14 | 15 | @JsonPropertyDescription(DESC_BODY_CODE) 16 | val code: String 17 | ) 18 | 19 | companion object { 20 | private const val CODE_LENGTH = "8" 21 | 22 | const val DESC_BODY_MESSAGE = "클라이언트의 accept-language 헤더에 맞는 언어로 출력한 오류 메시지입니다. " + 23 | "accept-language 헤더가 없거나 메시지가 준비되지 않았다면 영문 메시지를 포함합니다." 24 | const val DESC_BODY_CODE = "${CODE_LENGTH}자리 Hex 문자열 에러 코드입니다. " + 25 | "클라이언트는 이 값에 따라 오류를 처리할 수 있습니다. " + 26 | "에러 코드 목록은 MtExceptionCode 를 참고하시기 바랍니다." 27 | 28 | private const val HEX_CODE_FORMAT = "0x%0${CODE_LENGTH}x" 29 | 30 | fun create(exception: MtException) = ErrorResponseV1( 31 | Body(exception.message ?: "", HEX_CODE_FORMAT.format(exception.code.value)) 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/application.yml: -------------------------------------------------------------------------------- 1 | # 2 | # sirloin-sandbox-server 3 | # Distributed under CC BY-NC-SA 4 | # 5 | server: 6 | port: 8080 7 | error: 8 | whitelabel: 9 | enabled: false 10 | 11 | spring: 12 | application: 13 | name: sirloin-sandbox-server 14 | profiles: 15 | # Empty if 'default'. 16 | active: 17 | datasource: 18 | # Automatic database initialisation. Maybe conflict to hibernate. 19 | # https://docs.spring.io/spring-boot/docs/current/reference/html/howto-database-initialization.html 20 | initialization-mode: ALWAYS 21 | schema: classpath:/sql/v1.0/schema/*.sql 22 | # data: classpath:/sql/v1.0/data/*.sql 23 | type: com.zaxxer.hikari.HikariDataSource 24 | driver-class-name: com.mysql.cj.jdbc.Driver 25 | url: jdbc:mysql://localhost:8306/sirloin_sandbox?serverTimezone=UTC&useUnicode=true&character_set_server=utf8mb4; 26 | username: root 27 | # docker/run_mysql.sh 를 실행할 때 입력한 root password 를 여기 적어주세요. 28 | password: ${PLATFORM_SANDBOX_SECRET} 29 | web: 30 | resources: 31 | add-mappings: false 32 | mvc: 33 | throw-exception-if-no-handler-found: true 34 | 35 | management: 36 | server: 37 | add-application-context-header: false 38 | 39 | logging: 40 | # file: 41 | # path: /var/log/meatplatform-api/ 42 | level: 43 | ROOT: INFO 44 | # org.springframework.web.servlet: TRACE 45 | com.sirloin.sandbox.server: DEBUG 46 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/advice/ExceptionCodeToHttpStatusConverter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.advice 6 | 7 | import com.sirloin.sandbox.server.core.exception.MtExceptionCode 8 | import org.springframework.http.HttpStatus 9 | import org.springframework.stereotype.Component 10 | 11 | /** 12 | * 비즈니스 로직의 오류 코드인 [MtExceptionCode] 를 Http Status Code 로 변경하는 규칙 인터페이스 13 | * 14 | * @since 2022-02-14 15 | */ 16 | interface ExceptionCodeToHttpStatusConverter { 17 | fun convert(code: MtExceptionCode): HttpStatus? 18 | } 19 | 20 | @Component 21 | internal class ExceptionCodeToHttpStatusConverterImpl : ExceptionCodeToHttpStatusConverter { 22 | private val conversionMap = HashMap().apply { 23 | // HTTP 400: Bad request 24 | put(MtExceptionCode.WRONG_INPUT, HttpStatus.BAD_REQUEST) 25 | 26 | // HTTP 404: Not Found 27 | put(MtExceptionCode.USER_NOT_FOUND, HttpStatus.NOT_FOUND) 28 | put(MtExceptionCode.MALFORMED_INPUT, HttpStatus.NOT_FOUND) 29 | 30 | // HTTP 415: Unsupported Media Type 31 | put(MtExceptionCode.WRONG_PRESENTATION, HttpStatus.UNSUPPORTED_MEDIA_TYPE) 32 | } as Map 33 | 34 | override fun convert(code: MtExceptionCode): HttpStatus? = 35 | conversionMap[code] 36 | } 37 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/service/GetUserService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.service 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.common.UserServiceMixin 9 | import com.sirloin.sandbox.server.core.domain.user.repository.UserReadonlyRepository 10 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 11 | import java.util.* 12 | 13 | /** 14 | * User 도메인 모델 조회 관련 로직을 담당하는 Service interface 15 | * 16 | * @since 2022-02-14 17 | */ 18 | interface GetUserService { 19 | fun findUserByUuid(uuid: UUID): User? 20 | 21 | fun getUserByUuid(uuid: UUID): User 22 | 23 | companion object { 24 | fun newInstance( 25 | userRepo: UserReadonlyRepository, 26 | localeProvider: LocaleProvider 27 | ): GetUserService = GetUserServiceImpl(userRepo, localeProvider) 28 | } 29 | } 30 | 31 | internal class GetUserServiceImpl( 32 | override val userRepo: UserReadonlyRepository, 33 | override val localeProvider: LocaleProvider 34 | ) : GetUserService, UserServiceMixin { 35 | override fun findUserByUuid(uuid: UUID): User? { 36 | return userRepo.findByUuid(uuid) 37 | } 38 | 39 | override fun getUserByUuid(uuid: UUID): User = 40 | super.getUserByUuid(uuid) 41 | } 42 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/local/user/localstorage_user_test_saveMyUuid.dart: -------------------------------------------------------------------------------- 1 | // test target 의 method name 표현을 위한 naming rule 위반 2 | // ignore_for_file: file_names 3 | /* 4 | * sirloin-sandbox-client 5 | * Distributed under CC BY-NC-SA 6 | */ 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:logger/logger.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 11 | import 'package:uuid/uuid.dart'; 12 | 13 | import '../../../../../mock/@localstorage/local_storage.mocks.dart'; 14 | import 'localstorage_user_test.dart'; 15 | 16 | void saveMyUuidTestCases(final SutSupplier supplier, final MockLocalStorage localStorage, final Logger logger) { 17 | late UserLocalStorage sut; 18 | 19 | setUp(() { 20 | sut = supplier.call(); 21 | }); 22 | 23 | test("null 을 입력받으면, 저장된 uuid 를 삭제한다.", () async { 24 | // given: 25 | const String? uuid = null; 26 | 27 | // when: 28 | await sut.saveMyUuid(uuid); 29 | 30 | // expect: 31 | verify(localStorage.deleteItem(UserLocalStorageImpl.keyMyUuid)).called(1); 32 | }); 33 | 34 | test("uuid 를 입력받으면, 입력받은 uuid 를 저장한다.", () async { 35 | // given: 36 | final String? uuid = const Uuid().v4().toString(); 37 | 38 | // when: 39 | await sut.saveMyUuid(uuid); 40 | 41 | // expect: 42 | verify(localStorage.setItem(UserLocalStorageImpl.keyMyUuid, uuid)).called(1); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /client/lib/di/domain/user/di_user_bloc.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:logger/logger.dart'; 6 | import 'package:sirloin_sandbox_client/di/di_modules_helper.dart'; 7 | import 'package:sirloin_sandbox_client/domain/user/bloc/deregister/bloc_deregister_user.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/bloc/get_profile/bloc_get_profile.dart'; 9 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_user.dart'; 10 | import 'package:sirloin_sandbox_client/domain/user/bloc/edit_profile/bloc_edit_profile.dart'; 11 | import 'package:sirloin_sandbox_client/domain/user/bloc/register/bloc_register_user.dart'; 12 | 13 | class UserBlocModule { 14 | static const String _namespace = "bloc.user.registration"; 15 | 16 | static Future registerComponents(final DiRuleHolder ruleHolder) async { 17 | final Logger logger = ruleHolder.getSingleton(Logger); 18 | final userRepository = ruleHolder.getSingleton(UserRepository); 19 | 20 | ruleHolder 21 | .withNamespace(_namespace) 22 | .registerPrototype(RegisterUserBloc, () => RegisterUserBloc(userRepository, logger)) 23 | .registerPrototype(GetProfileBloc, () => GetProfileBloc(userRepository, logger)) 24 | .registerPrototype(EditProfileBloc, () => EditProfileBloc(userRepository, logger)) 25 | .registerPrototype(DeregisterUserBloc, () => DeregisterUserBloc(userRepository, logger)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/response/UserResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1.user.response 6 | 7 | import com.fasterxml.jackson.annotation.JsonProperty 8 | import com.fasterxml.jackson.annotation.JsonPropertyDescription 9 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 10 | import com.sirloin.sandbox.server.api.endpoint.v1.ResponseV1 11 | import com.sirloin.sandbox.server.core.domain.user.User 12 | import java.util.* 13 | 14 | /** 15 | * @since 2022-02-14 16 | */ 17 | @ResponseV1 18 | @JsonSerialize 19 | data class UserResponse( 20 | @JsonProperty 21 | @JsonPropertyDescription(DESC_UUID) 22 | val uuid: UUID, 23 | 24 | @JsonProperty 25 | @JsonPropertyDescription(DESC_NICKNAME) 26 | val nickname: String, 27 | 28 | @JsonProperty 29 | @JsonPropertyDescription(DESC_PROFILE_IMAGE_URL) 30 | val profileImageUrl: String, 31 | ) { 32 | companion object { 33 | const val DESC_UUID = "이용자의 고유 id." 34 | const val DESC_NICKNAME = "이용자가 가입 요청 단계에 입력한 닉네임." 35 | const val DESC_PROFILE_IMAGE_URL = "이용자가 가입 요청에 입력한 프로파일 URL." 36 | 37 | fun from(src: User): UserResponse = UserResponse( 38 | uuid = src.uuid, 39 | nickname = src.nickname, 40 | profileImageUrl = src.profileImageUrl 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/common/dto/test_support.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:convert'; 6 | 7 | import 'package:sirloin_sandbox_client/data/remote/http/common/dto/response_envelope.dart'; 8 | import 'package:sirloin_sandbox_client/data/remote/http/common/dto/response_error.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_exportable.dart'; 10 | 11 | extension ErrorResponseExtension on ErrorResponse { 12 | Map toJson() { 13 | return { 14 | ErrorResponse.keyMessage: message, 15 | ErrorResponse.keyCode: code, 16 | }; 17 | } 18 | 19 | String toJsonString() { 20 | return jsonEncode(toJson()); 21 | } 22 | } 23 | 24 | extension ResponseEnvelopeExtension on ResponseEnvelope { 25 | Map toJson() { 26 | final dynamic jsonBody; 27 | if (body is JsonExportable) { 28 | jsonBody = body.toJson(); 29 | } else if (body is ErrorResponse) { 30 | jsonBody = (body as ErrorResponse).toJson(); 31 | } else { 32 | // 매우 높은 확률로 여기서 오류 발생 (타입 추가될 때 마다 테스트에서 처리 필요) 33 | jsonBody = body; 34 | } 35 | 36 | return { 37 | ResponseEnvelope.keyType: type.value, 38 | ResponseEnvelope.keyTimestamp: timestamp.toIso8601String(), 39 | ResponseEnvelope.keyBody: jsonBody 40 | }; 41 | } 42 | 43 | String toJsonString() { 44 | return jsonEncode(toJson()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/service/CreateUserService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.service 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.User 8 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 9 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 10 | import java.util.* 11 | 12 | /** 13 | * User 도메인 모델 생성 관련 로직을 담당하는 Service interface 14 | * 15 | * @since 2022-02-14 16 | */ 17 | interface CreateUserService { 18 | fun createUser( 19 | nickname: String, 20 | profileImageUrl: String 21 | ): User 22 | 23 | companion object { 24 | fun newInstance( 25 | userRepo: UserRepository, 26 | localeProvider: LocaleProvider 27 | ): CreateUserService = CreateUserServiceImpl(userRepo, localeProvider) 28 | } 29 | } 30 | 31 | internal class CreateUserServiceImpl( 32 | private val userRepo: UserRepository, 33 | private val localeProvider: LocaleProvider 34 | ) : CreateUserService { 35 | override fun createUser(nickname: String, profileImageUrl: String): User { 36 | return userRepo.save( 37 | User.create( 38 | uuid = UUID.randomUUID(), 39 | nickname = nickname, 40 | profileImageUrl = profileImageUrl 41 | ) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/large/domain/user/UpdateAndFindUserTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large.domain.user 6 | 7 | import com.sirloin.jvmlib.time.truncateToSeconds 8 | import org.hamcrest.CoreMatchers.`is` 9 | import org.hamcrest.CoreMatchers.not 10 | import org.hamcrest.MatcherAssert.assertThat 11 | import org.junit.jupiter.api.DisplayName 12 | import org.junit.jupiter.api.Test 13 | import org.junit.jupiter.api.assertAll 14 | import test.small.domain.randomUser 15 | import java.time.Instant 16 | 17 | class UpdateAndFindUserTest : UserRepositoryLargeTestBase() { 18 | @DisplayName("Entity 를 수정한 내용은 그대로 반영된다") 19 | @Test 20 | fun `Updated information is stored and retrieved intact`() { 21 | // given: 22 | val now = Instant.now().truncateToSeconds() 23 | val savedUser = sut.save(randomUser(createdAt = now, updatedAt = now)) 24 | val originalName = savedUser.nickname 25 | 26 | // when: 27 | val updatedUser = sut.save(savedUser.edit().apply { 28 | nickname = "__TESTER" 29 | updatedAt = now 30 | }) 31 | 32 | // then: 33 | val foundUser = sut.findByUuid(updatedUser.uuid) 34 | 35 | // expect: 36 | assertAll( 37 | { assertThat(foundUser, `is`(updatedUser)) }, 38 | { assertThat(foundUser!!.nickname, not(originalName)) } 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/common/dto/randomiser.dart: -------------------------------------------------------------------------------- 1 | // test 만 따로 package 형태로 import 할 방법이 없음 2 | // ignore_for_file: avoid_relative_lib_imports 3 | /* 4 | * sirloin-sandbox-client 5 | * Distributed under CC BY-NC-SA 6 | */ 7 | import 'package:faker/faker.dart'; 8 | import 'package:sirloin_sandbox_client/data/remote/http/common/dto/response_envelope.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/common/dto/response_error.dart'; 10 | 11 | import '../../../../../util/randomiser.dart'; 12 | import 'mock_error_response.dart'; 13 | import 'mock_response_envelope.dart'; 14 | 15 | ErrorResponse randomErrorResponse({final String? message, final String? code}) { 16 | final faker = Faker(); 17 | 18 | return ErrorResponse(message ?? faker.lorem.sentence(), code ?? faker.lorem.word()); 19 | } 20 | 21 | ResponseEnvelope anyErrorResponseWithEnvelope({final ErrorResponse? errorResponse}) { 22 | return ResponseEnvelope(ResponseType.error, DateTime.now(), errorResponse ?? randomErrorResponse()); 23 | } 24 | 25 | ResponseEnvelope> randomResponseEnvelope() { 26 | final Map body = {"message": Faker().lorem.sentence()}; 27 | 28 | return ResponseEnvelope(ResponseType.values.random(), DateTime.now(), body); 29 | } 30 | 31 | MockResponseEnvelope mockResponseEnvelope() { 32 | return MockResponseEnvelope(randomResponseEnvelope()); 33 | } 34 | 35 | MockErrorResponse mockErrorResponse() { 36 | return MockErrorResponse(randomErrorResponse()); 37 | } 38 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/deregister/bloc_deregister_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:sirloin_sandbox_client/domain/user/bloc/deregister/message_deregister_user.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/bloc/deregister/state_deregister_user.dart'; 9 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_user.dart'; 10 | 11 | class DeregisterUserBloc extends Bloc { 12 | final UserRepository _userRepo; 13 | final Logger _logger; 14 | 15 | DeregisterUserBloc(final this._userRepo, final this._logger) : super(InitialState()) { 16 | on(_onProceedDeregisterMessage); 17 | } 18 | 19 | Future deregister(final String userId) async { 20 | add(ProceedDeregisterMessage(userId)); 21 | } 22 | 23 | Future _onProceedDeregisterMessage( 24 | final ProceedDeregisterMessage message, final Emitter emit) async { 25 | try { 26 | await _userRepo.deregister(message.uuid); 27 | } catch (e) { 28 | _logger.d("User deregistration via API has failed", e); 29 | if (e is Exception) { 30 | emit(ApiErrorState(e)); 31 | } else { 32 | emit(const ApiErrorState.empty()); 33 | } 34 | return; 35 | } 36 | 37 | emit(UserDeregisteredState()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | // POINT: 왜 굳이, 불편하게 core-infra-impl 모듈을 별도로 나눴을까요? 6 | version "0.1.1" 7 | 8 | apply plugin: "kotlin-spring" 9 | apply plugin: "kotlin-allopen" 10 | 11 | apply from: "${project.rootDir}/gradle/scripts/mavenPublishing.gradle" 12 | 13 | jar { 14 | enabled = true 15 | } 16 | 17 | def MODULE_CORE = "api-core" 18 | 19 | dependencies { 20 | // Spring dependencies 21 | implementation "org.springframework.boot:spring-boot-starter:$version_springBoot" 22 | implementation "org.springframework.boot:spring-boot-starter-validation:$version_springBoot" 23 | implementation "org.springframework.data:spring-data-jdbc:$version_springDataJdbc" 24 | 25 | // Spring Data 26 | implementation "org.springframework:spring-tx" 27 | implementation "com.zaxxer:HikariCP:$version_hikariCP" 28 | 29 | testImplementation("org.springframework.boot:spring-boot-starter-test:$version_springBootTest") { 30 | exclude group: "com.vaadin.external.google", module: "android-json" 31 | } 32 | 33 | /* 34 | * Test 에서만 사용할 메모리 database 이지만 test runtime 에서도 쓸 수 있어야 한다. 35 | * 실제 빌드 결과에 반영되면 안되기 때문에 runtimeOnly 로 의존성 선언해 준다. 36 | */ 37 | runtimeOnly "com.h2database:h2:$version_h2database" 38 | 39 | // Project dependencies 40 | implementation project(":${MODULE_CORE}") 41 | 42 | testImplementation project(path: ":${MODULE_CORE}", configuration: "testArtifacts") 43 | } 44 | -------------------------------------------------------------------------------- /client/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/local/user/localstorage_user_test_save.dart: -------------------------------------------------------------------------------- 1 | // test 만 따로 package 형태로 import 할 방법이 없음 2 | // ignore_for_file: avoid_relative_lib_imports 3 | /* 4 | * sirloin-sandbox-client 5 | * Distributed under CC BY-NC-SA 6 | */ 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:logger/logger.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 11 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 12 | 13 | import '../../../../../lib/domain/user/randomiser.dart'; 14 | import '../../../../../mock/@localstorage/local_storage.mocks.dart'; 15 | import 'localstorage_user_test.dart'; 16 | 17 | void saveTestCases(final SutSupplier supplier, final MockLocalStorage localStorage, final Logger logger) { 18 | late UserLocalStorage sut; 19 | late User user; 20 | 21 | setUp(() { 22 | sut = supplier.call(); 23 | user = randomUser(); 24 | }); 25 | 26 | test("User 를 로컬 캐시에 저장한다", () async { 27 | // then: 28 | final actual = await sut.save(user); 29 | 30 | // expect: 31 | expect(actual, isNot(null)); 32 | verify(localStorage.setItem(user.uuid, any)).called(1); 33 | }); 34 | 35 | test("로컬 캐시 저장이 실패하더라도 save 는 항상 성공한다", () async { 36 | // when: 37 | when(localStorage.setItem(any, any)).thenThrow(Exception("TEST!!")); 38 | 39 | // then: 40 | final actual = await sut.save(user); 41 | 42 | // expect null 43 | expect(actual, equals(null)); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/i18n/LocaleProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.i18n 6 | 7 | import java.util.* 8 | 9 | /** 10 | * 현재 처리할 요청의 문맥에 맞는 Locale 정보를 반환합니다. 11 | * 만약 적절한 Locale 정보를 찾지 못한다면, [DEFAULT] 로케일을 반환합니다. 12 | * 13 | * @since 2022-02-14 14 | */ 15 | interface LocaleProvider { 16 | val locale: Locale 17 | 18 | companion object { 19 | val DEFAULT: Locale = Locale.ENGLISH 20 | 21 | /* 22 | * ISO 638-1 언어 코드 목록 23 | * https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 24 | * 25 | * 언어 스크립트 코드 목록 26 | * http://unicode.org/iso15924/iso15924-codes.html 27 | * 28 | * ISO 3166 ALPHA-2 국가 코드 목록 29 | * https://www.iso.org/obp/ui/#search/code/ 30 | */ 31 | val SUPPORTED_KOREAN: Locale = Locale.KOREAN 32 | 33 | val SUPPORTED_LIST: List = listOf( 34 | DEFAULT, 35 | SUPPORTED_KOREAN, 36 | ) 37 | 38 | /** 39 | * 지원 locale 에 가장 근접한 locale 을 찾습니다. 만약 없다면, DEFAULT 를 반환합니다. 40 | */ 41 | fun matchSupportedLocale(ranges: List): Locale = 42 | Locale.lookup(ranges, SUPPORTED_LIST) ?: DEFAULT 43 | 44 | fun defaultInstance(): LocaleProvider = DefaultLocaleProviderImpl 45 | } 46 | } 47 | 48 | private object DefaultLocaleProviderImpl : LocaleProvider { 49 | override val locale: Locale = LocaleProvider.DEFAULT 50 | } 51 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/local/user/localstorage_user_test.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 8 | 9 | // test 만 따로 package 형태로 import 할 방법이 없음 10 | // ignore: avoid_relative_lib_imports 11 | import '../../../../../lib/test_components.dart'; 12 | import '../../../../../mock/@localstorage/local_storage.mocks.dart'; 13 | import 'localstorage_user_test_find.dart'; 14 | import 'localstorage_user_test_findMyUuid.dart'; 15 | import 'localstorage_user_test_save.dart'; 16 | import 'localstorage_user_test_saveMyUuid.dart'; 17 | 18 | typedef SutSupplier = UserLocalStorage Function(); 19 | 20 | /// UserLocalStorage Test suite 21 | void main() { 22 | final mockLocalStorage = MockLocalStorage(); 23 | final logger = newTestLogger(); 24 | 25 | UserLocalStorage instantiateSut() => UserLocalStorage.newInstance(mockLocalStorage, logger); 26 | 27 | group("findMyUuid 는:", () { 28 | findMyUuidTestCases(instantiateSut, mockLocalStorage, logger); 29 | }); 30 | 31 | group("saveMyUuid 는:", () { 32 | saveMyUuidTestCases(instantiateSut, mockLocalStorage, logger); 33 | }); 34 | 35 | group("find 는:", () { 36 | findTestCases(instantiateSut, mockLocalStorage, logger); 37 | }); 38 | 39 | group("save 는:", () { 40 | saveTestCases(instantiateSut, mockLocalStorage, logger); 41 | }); 42 | 43 | tearDown(() { 44 | reset(mockLocalStorage); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /server/api-core-infra-impl/src/test/kotlin/testcase/large/SpringDataJdbcLargeTestBase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.large 6 | 7 | import com.sirloin.sandbox.server.core.CoreApplication 8 | import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest 9 | import org.springframework.context.annotation.ComponentScan 10 | import org.springframework.test.context.ContextConfiguration 11 | import org.springframework.test.context.TestContext 12 | import org.springframework.test.context.TestExecutionListeners 13 | import org.springframework.test.context.support.AbstractTestExecutionListener 14 | import test.com.sirloin.annotation.LargeTest 15 | 16 | /** 17 | * Repository 관련 로직을 테스트할 때, 번거로운 환경설정을 상속으로 해결할 수 있도록 하는 Template Class 18 | * 코드 공유를 위한 상속이므로 좋은 패턴은 아님 19 | * 20 | * @since 2022-02-14 21 | */ 22 | @LargeTest 23 | @DataJdbcTest 24 | @ComponentScan(CoreApplication.PACKAGE_NAME) 25 | @ContextConfiguration(classes = [SpringDataJdbcTestConfig::class]) 26 | @TestExecutionListeners( 27 | value = [SpringDataJdbcLargeTestBase::class], 28 | mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS 29 | ) 30 | class SpringDataJdbcLargeTestBase : AbstractTestExecutionListener() { 31 | override fun beforeTestClass(testContext: TestContext) { 32 | } 33 | 34 | override fun prepareTestInstance(testContext: TestContext) { 35 | } 36 | 37 | override fun beforeTestExecution(testContext: TestContext) { 38 | } 39 | 40 | override fun beforeTestMethod(testContext: TestContext) { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/lib/data/remote/http/user/dto/response_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/data/remote/http/http_exceptions.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 9 | 10 | @immutable 11 | class UserResponse extends Equatable { 12 | final String uuid; 13 | final String nickname; 14 | final String profileImageUrl; 15 | 16 | @visibleForTesting 17 | static const keyUuid = "uuid"; 18 | @visibleForTesting 19 | static const keyNickname = "nickname"; 20 | @visibleForTesting 21 | static const keyProfileImageUrl = "profileImageUrl"; 22 | 23 | const UserResponse({ 24 | required this.uuid, 25 | required this.nickname, 26 | required this.profileImageUrl, 27 | }); 28 | 29 | User toUser() => User.create(uuid: uuid, nickname: nickname, profileImageUrl: profileImageUrl); 30 | 31 | @override 32 | List get props => [uuid, nickname, profileImageUrl]; 33 | 34 | @override 35 | bool get stringify => true; 36 | 37 | static UserResponse fromJson(final Map jsonMap) { 38 | final uuid = jsonMap[keyUuid]; 39 | final nickname = jsonMap[keyNickname]; 40 | final profileImageUrl = jsonMap[keyProfileImageUrl]; 41 | 42 | if (uuid == null || nickname == null || profileImageUrl == null) { 43 | throw JsonParseException(); 44 | } 45 | 46 | return UserResponse(uuid: uuid, nickname: nickname, profileImageUrl: profileImageUrl); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/api-main/src/local/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/local/user/localstorage_user_test_findMyUuid.dart: -------------------------------------------------------------------------------- 1 | // test target 의 method name 표현을 위한 naming rule 위반 2 | // ignore_for_file: file_names 3 | // test 만 따로 package 형태로 import 할 방법이 없음 4 | // ignore_for_file: avoid_relative_lib_imports 5 | /* 6 | * sirloin-sandbox-client 7 | * Distributed under CC BY-NC-SA 8 | */ 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:logger/logger.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | import 'package:sirloin_sandbox_client/data/local/user/localstorage_user.dart'; 13 | import 'package:uuid/uuid.dart'; 14 | 15 | import '../../../../../mock/@localstorage/local_storage.mocks.dart'; 16 | import 'localstorage_user_test.dart'; 17 | 18 | void findMyUuidTestCases(final SutSupplier supplier, final MockLocalStorage localStorage, final Logger logger) { 19 | late UserLocalStorage sut; 20 | 21 | setUp(() { 22 | sut = supplier.call(); 23 | }); 24 | 25 | test("uuid 가 저장되어 있다면, 저장된 값을 반환한다", () async { 26 | // given: 27 | final uuid = const Uuid().v4().toString(); 28 | 29 | // when: 30 | when(localStorage.getItem(UserLocalStorageImpl.keyMyUuid)).thenReturn(uuid); 31 | 32 | // then: 33 | final savedUuid = await sut.findMyUuid(); 34 | 35 | // expect: 36 | expect(savedUuid, equals(uuid)); 37 | }); 38 | 39 | test("uuid 가 저장되어 있지 않다면, null 을 반환한다", () async { 40 | // when: 41 | when(localStorage.getItem(UserLocalStorageImpl.keyMyUuid)).thenReturn(null); 42 | 43 | // then: 44 | final savedUuid = await sut.findMyUuid(); 45 | 46 | // expect: 47 | expect(savedUuid, equals(null)); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/common/dto/mock_response_envelope.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'dart:convert'; 6 | 7 | import 'package:sirloin_sandbox_client/data/remote/http/common/dto/response_envelope.dart'; 8 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_exportable.dart'; 9 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_serializable.dart'; 10 | 11 | class MockResponseEnvelope implements JsonSerializable, JsonExportable { 12 | ResponseType? _type; 13 | String? _timestamp; 14 | Object? _body; 15 | 16 | MockResponseEnvelope(final ResponseEnvelope response) 17 | : _type = response.type, 18 | _timestamp = response.timestamp.toIso8601String(), 19 | _body = response.body; 20 | 21 | ResponseEnvelope get() { 22 | return ResponseEnvelope(_type!, DateTime.parse(_timestamp!), _body!); 23 | } 24 | 25 | MockResponseEnvelope type(final String? type) { 26 | _type = ResponseType.from(type); 27 | return this; 28 | } 29 | 30 | MockResponseEnvelope timestamp(final String? timestamp) { 31 | _timestamp = timestamp; 32 | return this; 33 | } 34 | 35 | MockResponseEnvelope body(final Object? body) { 36 | _body = body; 37 | return this; 38 | } 39 | 40 | @override 41 | Map toJson() { 42 | return { 43 | ResponseEnvelope.keyType: _type?.value, 44 | ResponseEnvelope.keyTimestamp: _timestamp, 45 | ResponseEnvelope.keyBody: _body 46 | }; 47 | } 48 | 49 | @override 50 | String toJsonString() { 51 | return jsonEncode(toJson()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | 31 | analyzer: 32 | exclude: 33 | - "**/*.g.dart" 34 | - "**/*.mocks.dart" 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = Sir.LOIN Sandbox Project 기여 가이드 2 | // refs: 3 | :link-prj: https://github.com/sirloin-dev/meatplatform-sandbox 4 | 5 | == 행동 강령 6 | 이 프로젝트는 link:CODE_OF_CONDUCT.adoc[Sir.LOIN Sandbox Project 행동 강령] 원칙을 적용받습니다. 참여자 여러분들은 행동 강령을 준수해 주시기 바랍니다. 7 | 8 | 행동 강령 위반하는 여러 행위를 발견하신다면, 즉시 신고해주세요. 9 | 10 | == Pull Request 제출 방법 11 | 아래 순서대로 진행해 주시기 바랍니다. 12 | 13 | . link:{link-prj}/issues[Github Issues] 에 발견하신 문제를 issue 로 등록해 주세요. 14 | . `meatplatform-sandbox` 을 참여자분의 저장소에 Fork 해 주세요. 15 | . `main` 브랜치를 최신 상태로 업데이트한 후, 작업을 잘 나타내는 새 브랜치를 생성해주세요. 브랜치 이름은 영문 기준으로 작성해 주시기 바랍니다. 16 | . 테스트를 작성해 주세요. link:{link-prj}/tree/main/server[Server] 프로젝트는 `JUnit` 를, link:{link-prj}/tree/main/client[Client] 프로젝트는 `flutter test` 를 기본 테스팅 도구로 활용합니다. 17 | . 작업을 마치시면 commit message 를 아래 형식으로 작성해 주세요. Commit message 는 한국어, 또는 영어로 작성해 주시기 바랍니다. 18 | + 19 | [source,shell] 20 | ---- 21 | # ?? 는 Github issue 번호입니다. 22 | # Commit message 의 길이는 제목 국/한 혼용 40자, 영문 72자를 넘기지 않도록 작성해 주세요. 23 | # 본문은 제한 없이 자유롭게 작성해 주시기 바랍니다. 24 | 25 | $ git commit -m "#1 커뮤니티 가이드 문서 추가" 26 | [master 6009820] #1 커뮤니티 가이드 문서 추가 27 | 2 files changed, 143 insertions(+) 28 | create mode 100644 CODE_OF_CONDUCT.adoc 29 | create mode 100644 CONTRIBUTING.adoc 30 | ---- 31 | . 참여자분의 브랜치에서 `meatplatform-sandbox/main` 브랜치로 새로운 PR 을 만드세요. 32 | . 리뷰를 기다려 주세요. 검토를 거친 후, 병합해 드리겠습니다. 33 | . 참여 감사합니다! 34 | 35 | == 병합 규칙 36 | 내 Commit 이 사라졌더라도 놀라지 마세요! 37 | 38 | 참여자 여러분들의 기여 내용은 Github 의 link:https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits[Squash and Merge] 규칙에 의거해 병합할 예정입니다. 내용이 궁금하시다면, 확인해 보시기 바랍니다. 39 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/ResponseEnvelopeV1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1 6 | 7 | import com.fasterxml.jackson.annotation.JsonCreator 8 | import com.fasterxml.jackson.annotation.JsonPropertyDescription 9 | import com.fasterxml.jackson.annotation.JsonValue 10 | import com.sirloin.sandbox.server.core.exception.MtException 11 | import java.time.Instant 12 | 13 | abstract class ResponseEnvelopeV1( 14 | @JsonPropertyDescription(DESC_TYPE) 15 | open val type: Type, 16 | 17 | @JsonPropertyDescription(DESC_TIMESTAMP) 18 | open val timestamp: Instant = Instant.now() 19 | ) { 20 | @get:JsonPropertyDescription(DESC_BODY) 21 | abstract val body: T? 22 | 23 | companion object { 24 | const val DESC_TYPE = "응답의 유형을 나타냅니다. 'OK' 또는 'ERROR' 로 표시합니다." 25 | const val DESC_TIMESTAMP = "ISO 8601 표준 포맷으로 나타낸 응답 발생 시간입니다. 시간대는 UTC 기준입니다." 26 | const val DESC_BODY = "응답의 실제 내용을 담고 있는 객체입니다." 27 | 28 | fun ok(payload: T?) = OkResponseV1(payload) 29 | 30 | fun error(exception: MtException) = ErrorResponseV1.create(exception) 31 | } 32 | 33 | enum class Type(@JsonValue val value: String) { 34 | OK("OK"), 35 | ERROR("ERROR"); 36 | 37 | companion object { 38 | @JsonCreator(mode = JsonCreator.Mode.DELEGATING) 39 | @JvmStatic 40 | fun from(value: String) = values().firstOrNull { it.value == value } 41 | ?: throw IllegalArgumentException("Cannot convert '${value}' as Response type") 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/deregister/state_deregister_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:meta/meta.dart'; 6 | import 'package:sirloin_sandbox_client/domain/base_bloc_state.dart'; 7 | 8 | abstract class DeregisterUserBlocState extends BaseBlocState {} 9 | 10 | @immutable 11 | class InitialState extends BaseEmptyState implements DeregisterUserBlocState {} 12 | 13 | @immutable 14 | class ApiErrorState extends BaseExceptionalState implements DeregisterUserBlocState { 15 | const ApiErrorState(Exception? exception) : super(exception); 16 | 17 | const ApiErrorState.empty() : super(null); 18 | } 19 | 20 | @immutable 21 | class UserDeregisteredState implements DeregisterUserBlocState {} 22 | 23 | typedef StateToObject = T Function(U); 24 | 25 | // Dart 에 Sealed class 가 없어서 pattern matching 을 직접 손으로 구현... 26 | extension DeregisterUserBlocStatePatternMatcher on DeregisterUserBlocState { 27 | T when( 28 | {required final StateToObject initialState, 29 | required final StateToObject userDeregisteredState, 30 | required final StateToObject apiErrorState}) { 31 | // Dart 에 Smart cast 가 없어서 불편... 32 | if (this is InitialState) { 33 | return initialState(this as InitialState); 34 | } else if (this is UserDeregisteredState) { 35 | return userDeregisteredState(this as UserDeregisteredState); 36 | } else if (this is ApiErrorState) { 37 | return apiErrorState(this as ApiErrorState); 38 | } 39 | 40 | throw UnimplementedError("Pattern matching rule for '$this extends ProfileScreenState' " 41 | "is not implemented"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/lib/screen/state_screen_splash.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 6 | 7 | abstract class SplashScreenState {} 8 | 9 | class InitiatedState implements SplashScreenState {} 10 | 11 | class LoadingState implements SplashScreenState {} 12 | 13 | class SavedUserNotFoundState implements SplashScreenState {} 14 | 15 | class SavedUserFoundState implements SplashScreenState { 16 | final User user; 17 | 18 | SavedUserFoundState(this.user); 19 | } 20 | 21 | typedef StateToObject = T Function(U); 22 | 23 | // Dart 에 Sealed class 가 없어서 pattern matching 을 직접 손으로 구현... 24 | extension SplashScreenStatePatternMatcher on SplashScreenState { 25 | T when({ 26 | required final StateToObject initiatedState, 27 | required final StateToObject loadingState, 28 | required final StateToObject savedUserNotFoundState, 29 | required final StateToObject savedUserFoundState, 30 | }) { 31 | // Dart 에 Smart cast 가 없어서 불편... 32 | if (this is InitiatedState) { 33 | return initiatedState(this as InitiatedState); 34 | } else if (this is LoadingState) { 35 | return loadingState(this as LoadingState); 36 | } else if (this is SavedUserNotFoundState) { 37 | return savedUserNotFoundState(this as SavedUserNotFoundState); 38 | } else if (this is SavedUserFoundState) { 39 | return savedUserFoundState(this as SavedUserFoundState); 40 | } 41 | 42 | throw UnimplementedError("Pattern matching rule for '$this extends SplashScreenState' " 43 | "is not implemented"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/service/UpdateUserService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.core.domain.user.service 6 | 7 | import com.sirloin.jvmlib.time.truncateToSeconds 8 | import com.sirloin.sandbox.server.core.domain.user.User 9 | import com.sirloin.sandbox.server.core.domain.user.common.UserServiceMixin 10 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 11 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 12 | import java.time.Instant 13 | import java.util.* 14 | 15 | /** 16 | * User 도메인 모델 수정 관련 로직을 담당하는 Service interface 17 | * 18 | * @since 2022-02-14 19 | */ 20 | interface UpdateUserService { 21 | fun updateUser( 22 | uuid: UUID, 23 | nickname: String? = null, 24 | profileImageUrl: String? = null 25 | ): User 26 | 27 | companion object { 28 | fun newInstance( 29 | userRepo: UserRepository, 30 | localeProvider: LocaleProvider 31 | ): UpdateUserService = UpdateUserServiceImpl(userRepo, localeProvider) 32 | } 33 | } 34 | 35 | internal class UpdateUserServiceImpl( 36 | override val userRepo: UserRepository, 37 | override val localeProvider: LocaleProvider 38 | ) : UpdateUserService, UserServiceMixin { 39 | override fun updateUser(uuid: UUID, nickname: String?, profileImageUrl: String?): User { 40 | val user = super.getUserByUuid(uuid) 41 | 42 | return userRepo.save(user.edit().apply { 43 | this.nickname = nickname ?: user.nickname 44 | this.profileImageUrl = profileImageUrl ?: user.profileImageUrl 45 | this.updatedAt = Instant.now().truncateToSeconds() 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/GetUserController.kt: -------------------------------------------------------------------------------- 1 | package com.sirloin.sandbox.server.api.endpoint.v1.user 2 | 3 | import com.sirloin.sandbox.server.api.endpoint.util.uuidStringToUuid 4 | import com.sirloin.sandbox.server.api.endpoint.v1.ApiPathsV1 5 | import com.sirloin.sandbox.server.api.endpoint.v1.user.response.UserResponse 6 | import com.sirloin.sandbox.server.core.domain.user.service.GetUserService 7 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 8 | import org.slf4j.Logger 9 | import org.springframework.http.MediaType 10 | import org.springframework.web.bind.annotation.PathVariable 11 | import org.springframework.web.bind.annotation.RequestMapping 12 | import org.springframework.web.bind.annotation.RequestMethod 13 | import org.springframework.web.bind.annotation.RestController 14 | 15 | /** 16 | * ``` 17 | * GET /user/{userId} 18 | * 19 | * Content-Type: application/json 20 | * ``` 21 | * 22 | * @since 2022-02-14 23 | */ 24 | @RequestMapping( 25 | produces = [MediaType.APPLICATION_JSON_VALUE], 26 | consumes = [MediaType.APPLICATION_JSON_VALUE] 27 | ) 28 | interface GetUserController { 29 | @RequestMapping( 30 | path = [ApiPathsV1.USER_UUID], 31 | method = [RequestMethod.GET] 32 | ) 33 | fun create(@PathVariable(ApiPathsV1.PATH_VAR_UUID) uuidStr: String): UserResponse 34 | } 35 | 36 | @RestController 37 | class GetUserControllerImpl( 38 | private val svc: GetUserService, 39 | private val localeProvider: LocaleProvider, 40 | private val log: Logger, 41 | ) : GetUserController { 42 | override fun create(uuidStr: String): UserResponse { 43 | val uuid = uuidStringToUuid(uuidStr, localeProvider, log) 44 | 45 | return UserResponse.from(svc.getUserByUuid(uuid)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/register/bloc_register_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_user.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 9 | import 'package:sirloin_sandbox_client/domain/user/bloc/register/message_register_user.dart'; 10 | import 'package:sirloin_sandbox_client/domain/user/bloc/register/state_register_user.dart'; 11 | 12 | class RegisterUserBloc extends Bloc { 13 | final UserRepository _userRepo; 14 | final Logger _logger; 15 | 16 | RegisterUserBloc(final this._userRepo, final this._logger) : super(InitialState()) { 17 | on(_onProceedRegistrationMessage); 18 | } 19 | 20 | void registerUser(final String nickname, final String profileImageUrl) { 21 | add(ProceedRegistrationMessage(nickname, profileImageUrl)); 22 | } 23 | 24 | Future _onProceedRegistrationMessage( 25 | final ProceedRegistrationMessage message, final Emitter emit) async { 26 | final nickname = message.nickname; 27 | final profileImageUrl = message.profileImageUrl; 28 | 29 | final User registeredUser; 30 | try { 31 | registeredUser = await _userRepo.register(nickname: nickname, profileImageUrl: profileImageUrl); 32 | } catch (e) { 33 | _logger.d("User registration via API has failed", e); 34 | if (e is Exception) { 35 | emit(ApiErrorState(e)); 36 | } else { 37 | emit(const ApiErrorState.empty()); 38 | } 39 | return; 40 | } 41 | 42 | emit(UserRegisteredState(registeredUser)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/advice/responseDecorator/V1ResponseDecorator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.advice.responseDecorator 6 | 7 | import com.sirloin.sandbox.server.api.endpoint.v1.ResponseEnvelopeV1 8 | import com.sirloin.sandbox.server.api.endpoint.v1.ResponseV1 9 | import org.springframework.core.MethodParameter 10 | import org.springframework.http.MediaType 11 | import org.springframework.http.converter.HttpMessageConverter 12 | import org.springframework.http.server.ServerHttpRequest 13 | import org.springframework.http.server.ServerHttpResponse 14 | import org.springframework.web.bind.annotation.RestControllerAdvice 15 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice 16 | 17 | /** 18 | * [ResponseV1] 어노테이션으로 감싼 컨트롤러의 반환값을 19 | * [ResponseEnvelopeV1] 로 감싸주는 역할을 합니다. Any type 에 동작하기 때문에, supports 조건 변경시 주의해야 합니다. 20 | * 21 | * @since 2022-02-14 22 | */ 23 | @RestControllerAdvice 24 | class V1ResponseDecorator : ResponseBodyAdvice { 25 | override fun supports(returnType: MethodParameter, converterType: Class>): Boolean { 26 | val methodReturnType = returnType.method?.returnType ?: return false 27 | return methodReturnType.declaredAnnotations.any { 28 | it.annotationClass == ResponseV1::class 29 | } 30 | } 31 | 32 | override fun beforeBodyWrite( 33 | body: Any?, 34 | returnType: MethodParameter, 35 | selectedContentType: MediaType, 36 | selectedConverterType: Class>, 37 | request: ServerHttpRequest, 38 | response: ServerHttpResponse 39 | ): Any? = 40 | ResponseEnvelopeV1.ok(body) 41 | } 42 | -------------------------------------------------------------------------------- /client/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Sirloin Sandbox Client 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | sirloin_sandbox_client 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /client/test/lib/data/remote/http/user/dto/randomiser_response_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:faker/faker.dart'; 6 | import 'package:sirloin_sandbox_client/data/remote/http/common/json_exportable.dart'; 7 | import 'package:sirloin_sandbox_client/data/remote/http/user/dto/response_user.dart'; 8 | import 'package:uuid/uuid.dart'; 9 | 10 | UserResponse randomUserResponse({ 11 | final String? uuid, 12 | final String? nickname, 13 | final String? profileImageUrl, 14 | }) { 15 | final faker = Faker(); 16 | 17 | return UserResponse( 18 | uuid: uuid ?? const Uuid().v4().toString(), 19 | nickname: nickname ?? faker.person.name(), 20 | profileImageUrl: profileImageUrl ?? faker.image.image()); 21 | } 22 | 23 | MockUserResponse mockUserResponse() { 24 | return MockUserResponse(randomUserResponse()); 25 | } 26 | 27 | class MockUserResponse implements JsonExportable { 28 | String? _uuid; 29 | String? _nickname; 30 | String? _profileImageUrl; 31 | 32 | MockUserResponse(final UserResponse src) 33 | : _uuid = src.uuid, 34 | _nickname = src.nickname, 35 | _profileImageUrl = src.profileImageUrl; 36 | 37 | MockUserResponse uuid(final String? uuid) { 38 | _uuid = uuid; 39 | return this; 40 | } 41 | 42 | MockUserResponse nickname(final String? nickname) { 43 | _nickname = nickname; 44 | return this; 45 | } 46 | 47 | MockUserResponse profileImageUrl(final String? profileImageUrl) { 48 | _profileImageUrl = profileImageUrl; 49 | return this; 50 | } 51 | 52 | @override 53 | Map toJson() { 54 | return { 55 | UserResponse.keyUuid: _uuid, 56 | UserResponse.keyNickname: _nickname, 57 | UserResponse.keyProfileImageUrl: _profileImageUrl 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 16 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/CreateUserController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.endpoint.v1.user 6 | 7 | import com.sirloin.sandbox.server.api.endpoint.v1.ApiPathsV1 8 | import com.sirloin.sandbox.server.api.endpoint.v1.user.request.CreateUserRequest 9 | import com.sirloin.sandbox.server.api.endpoint.v1.user.response.UserResponse 10 | import com.sirloin.sandbox.server.core.domain.user.service.CreateUserService 11 | import org.springframework.http.MediaType 12 | import org.springframework.web.bind.annotation.RequestBody 13 | import org.springframework.web.bind.annotation.RequestMapping 14 | import org.springframework.web.bind.annotation.RequestMethod 15 | import org.springframework.web.bind.annotation.RestController 16 | import javax.validation.Valid 17 | 18 | /** 19 | * ``` 20 | * POST /user 21 | * 22 | * Content-Type: application/json 23 | * ``` 24 | * 25 | * @since 2022-02-14 26 | */ 27 | @RequestMapping( 28 | produces = [MediaType.APPLICATION_JSON_VALUE], 29 | consumes = [MediaType.APPLICATION_JSON_VALUE] 30 | ) 31 | interface CreateUserController { 32 | @RequestMapping( 33 | path = [ApiPathsV1.USER], 34 | method = [RequestMethod.POST] 35 | ) 36 | fun create(@Valid @RequestBody req: CreateUserRequest): UserResponse 37 | } 38 | 39 | @RestController 40 | class CreateUserControllerImpl( 41 | private val svc: CreateUserService 42 | ) : CreateUserController { 43 | override fun create(req: CreateUserRequest): UserResponse { 44 | val createdUser = req.run { 45 | svc.createUser( 46 | nickname = req.nickname, 47 | profileImageUrl = req.profileImageUrl 48 | ) 49 | } 50 | 51 | return UserResponse.from(createdUser) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/api-main/src/test/kotlin/test/large/endpoint/v1/RestDocsFieldSnippets.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package test.large.endpoint.v1 6 | 7 | import com.sirloin.sandbox.server.api.endpoint.v1.ErrorResponseV1 8 | import com.sirloin.sandbox.server.api.endpoint.v1.ResponseEnvelopeV1 9 | import org.springframework.restdocs.payload.FieldDescriptor 10 | import org.springframework.restdocs.payload.JsonFieldType 11 | import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath 12 | import kotlin.reflect.KProperty 13 | 14 | fun basicResponseFieldsSnippet(): List = listOf( 15 | fieldWithPath(ResponseEnvelopeV1<*>::type.asRequestField()) 16 | .type(JsonFieldType.STRING) 17 | .description(ResponseEnvelopeV1.DESC_TYPE), 18 | fieldWithPath(ResponseEnvelopeV1<*>::timestamp.asRequestField()) 19 | .type(JsonFieldType.STRING) 20 | .description(ResponseEnvelopeV1.DESC_TIMESTAMP), 21 | fieldWithPath(ResponseEnvelopeV1<*>::body.asRequestField()) 22 | .type(JsonFieldType.OBJECT) 23 | .description(ResponseEnvelopeV1.DESC_BODY) 24 | ) 25 | 26 | fun errorResponseFieldsSnippet(): List = ArrayList(basicResponseFieldsSnippet()) + listOf( 27 | fieldWithPath(ErrorResponseV1.Body::message.asPrefixedRequestField("body")) 28 | .type(JsonFieldType.STRING) 29 | .description(ErrorResponseV1.DESC_BODY_MESSAGE), 30 | fieldWithPath(ErrorResponseV1.Body::code.asPrefixedRequestField("body")) 31 | .type(JsonFieldType.STRING) 32 | .description(ErrorResponseV1.DESC_BODY_CODE) 33 | ) 34 | 35 | fun KProperty.asRequestField(): String = 36 | this.asPrefixedRequestField("") 37 | 38 | fun KProperty.asPrefixedRequestField(prefix: String = ""): String = if (prefix.isEmpty()) { 39 | this.name 40 | } else { 41 | "${prefix}.${this.name}" 42 | } 43 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/repository/user/repo_user_impl_test.dart: -------------------------------------------------------------------------------- 1 | // test 만 따로 package 형태로 import 할 방법이 없음 2 | // ignore_for_file: avoid_relative_lib_imports 3 | /* 4 | * sirloin-sandbox-client 5 | * Distributed under CC BY-NC-SA 6 | */ 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | import 'package:sirloin_sandbox_client/data/repository/user/repo_user_impl.dart'; 10 | 11 | import '../../../../../lib/test_components.dart'; 12 | import '../../../../../mock/lib/data/local/user/localstorage_user.mocks.dart'; 13 | import '../../../../../mock/lib/data/remote/http/user/api_user.mocks.dart'; 14 | import 'repo_user_impl_test_deregister.dart'; 15 | import 'repo_user_impl_test_findSavedSelf.dart'; 16 | import 'repo_user_impl_test_getUser.dart'; 17 | import 'repo_user_impl_test_register.dart'; 18 | import 'repo_user_impl_test_updateProfile.dart'; 19 | 20 | typedef SutSupplier = UserRepositoryImpl Function(); 21 | 22 | void main() { 23 | final mockUserLocalStorage = MockUserLocalStorage(); 24 | final mockUserApi = MockUserApi(); 25 | final logger = newTestLogger(); 26 | 27 | supplier() => UserRepositoryImpl(mockUserLocalStorage, mockUserApi, logger); 28 | 29 | group("findSavedSelf 는:", () { 30 | findSavedSelfTestCases(supplier, mockUserLocalStorage, mockUserApi); 31 | }); 32 | 33 | group("getUser 는:", () { 34 | getUserTestCases(supplier, mockUserLocalStorage, mockUserApi); 35 | }); 36 | 37 | group("register 는:", () { 38 | registerTestCases(supplier, mockUserLocalStorage, mockUserApi); 39 | }); 40 | 41 | group("updateProfile 은:", () { 42 | updateProfileTestCases(supplier, mockUserLocalStorage, mockUserApi); 43 | }); 44 | 45 | group("deregister 는:", () { 46 | deregisterTestCases(supplier, mockUserLocalStorage, mockUserApi); 47 | }); 48 | 49 | tearDown(() { 50 | reset(mockUserLocalStorage); 51 | reset(mockUserApi); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /client/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/lib/widget/dialog/alert_dialog_exceptions.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_i18n/widgets/I18nText.dart'; 7 | import 'package:sirloin_sandbox_client/widget/extensions_build_context.dart'; 8 | 9 | /// Dialog 또한 Widget 의 sub tree 이므로 dialog 를 그리기 위해서는 호출 지점에서 10 | /// dialog 를 그리는 상태라는 것을 알려야 (setState()) 한다. 11 | /// 12 | /// 그리고 Flutter 의 규칙에 따라 setState 는 widget build 완료 이후에만 호출할 수 있다. 13 | /// 즉, Future 를 이용하지 않는 UI building 과정에서 이 함수 호출시 문제가 발생한다. 14 | /// 15 | /// 따라서 문제를 해결하기 위해 UI context 에서 직접 [isImmediate] 값을 결정할 수 있도록 한다. 16 | /// 17 | /// See also: https://api.flutter.dev/flutter/material/AlertDialog/build.html 18 | bool showExceptionAlertDialog(final BuildContext context, final Exception? exception, 19 | {final bool isImmediate = false}) { 20 | final Widget content; 21 | if (exception == null) { 22 | content = Text(context.i18n("error.common.message")); 23 | } else { 24 | content = I18nText("error.common.messageWithFormat", translationParams: {"message": exception.toString()}); 25 | } 26 | 27 | showAlertDialog() { 28 | showDialog( 29 | context: context, 30 | barrierDismissible: false, 31 | builder: (context) => 32 | AlertDialog(title: Text(context.i18n("registration.error")), content: content, actions: [ 33 | TextButton( 34 | child: Text(context.i18n("text.btn.confirm")), 35 | onPressed: () => Navigator.pop(context), 36 | ) 37 | ])); 38 | } 39 | 40 | if (isImmediate) { 41 | showAlertDialog(); 42 | return true; 43 | } 44 | 45 | // UI context 사라진 이후 호출될 수 있기 때문에 검사 수행 46 | final binding = WidgetsBinding.instance; 47 | if (binding == null) { 48 | return false; 49 | } 50 | 51 | binding.addPostFrameCallback((_) => showAlertDialog()); 52 | return true; 53 | } 54 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/get_profile/bloc_get_profile.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:sirloin_sandbox_client/domain/user/bloc/get_profile/message_get_profile.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/bloc/get_profile/state_get_profile.dart'; 9 | import 'package:sirloin_sandbox_client/domain/user/repository/repo_user.dart'; 10 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 11 | 12 | class GetProfileBloc extends Bloc { 13 | late User _user; 14 | final UserRepository _userRepo; 15 | final Logger _logger; 16 | 17 | GetProfileBloc(final this._userRepo, final this._logger) : super(InitialState()) { 18 | on(_onUserDataSetMessage); 19 | on(_onErrorMessage); 20 | } 21 | 22 | User get user => _user; 23 | 24 | set user(final User user) { 25 | _user = user; 26 | add(UserDataSetMessage(user)); 27 | } 28 | 29 | Future loadUser(final String uuid, [final bool forceRefresh = false]) async { 30 | late User updatedUser; 31 | try { 32 | updatedUser = await _userRepo.getUser(uuid: uuid, forceRefresh: forceRefresh); 33 | } catch (e) { 34 | _logger.d("User retrieval has failed", e); 35 | if (e is Exception) { 36 | add(ErrorMessage(e)); 37 | } else { 38 | add(const ErrorMessage(null)); 39 | } 40 | return; 41 | } 42 | 43 | user = updatedUser; 44 | } 45 | 46 | Future _onErrorMessage(final ErrorMessage message, final Emitter emit) async { 47 | emit(ApiErrorState(message.exception)); 48 | } 49 | 50 | Future _onUserDataSetMessage(final UserDataSetMessage message, final Emitter emit) async { 51 | emit(UserDataSetState(message.user)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/endpoint/v1/user/DeleteUserController.kt: -------------------------------------------------------------------------------- 1 | package com.sirloin.sandbox.server.api.endpoint.v1.user 2 | 3 | import com.sirloin.sandbox.server.api.endpoint.util.uuidStringToUuid 4 | import com.sirloin.sandbox.server.api.endpoint.v1.ApiPathsV1 5 | import com.sirloin.sandbox.server.api.endpoint.v1.user.response.DeletedUserResponse 6 | import com.sirloin.sandbox.server.core.domain.user.service.DeleteUserService 7 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 8 | import org.slf4j.Logger 9 | import org.springframework.http.MediaType 10 | import org.springframework.web.bind.annotation.PathVariable 11 | import org.springframework.web.bind.annotation.RequestMapping 12 | import org.springframework.web.bind.annotation.RequestMethod 13 | import org.springframework.web.bind.annotation.RestController 14 | 15 | /** 16 | * ``` 17 | * DELETE /user/{userId} 18 | * 19 | * Content-Type: application/json 20 | * ``` 21 | * 22 | * @since 2022-02-14 23 | */ 24 | // POINT: 이 API 에는 심각한 문제가 있습니다. 어떤 문제일까요? 어떻게 개선할 수 있을까요? 25 | @RequestMapping( 26 | produces = [MediaType.APPLICATION_JSON_VALUE], 27 | consumes = [MediaType.APPLICATION_JSON_VALUE] 28 | ) 29 | interface DeleteUserController { 30 | @RequestMapping( 31 | path = [ApiPathsV1.USER_UUID], 32 | method = [RequestMethod.DELETE] 33 | ) 34 | fun delete(@PathVariable(ApiPathsV1.PATH_VAR_UUID) uuidStr: String): DeletedUserResponse 35 | } 36 | 37 | @RestController 38 | class DeleteUserControllerImpl( 39 | private val svc: DeleteUserService, 40 | private val localeProvider: LocaleProvider, 41 | private val log: Logger, 42 | ) : DeleteUserController { 43 | override fun delete(uuidStr: String): DeletedUserResponse { 44 | val uuid = uuidStringToUuid(uuidStr, localeProvider, log) 45 | 46 | val deletedUser = svc.deleteUserByUuid(uuid) 47 | 48 | return DeletedUserResponse(deletedUser.uuid) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/api-main/largeTest.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | apply plugin: "org.asciidoctor.jvm.convert" 6 | 7 | dependencies { 8 | def MODULE_CORE = "api-core" 9 | 10 | // Project dependencies 11 | testImplementation project(path: ":${MODULE_CORE}", configuration: "testArtifacts") 12 | 13 | // Spring boot test 14 | testImplementation("org.springframework.boot:spring-boot-starter-test:$version_springBootTest") { 15 | exclude group: "com.vaadin.external.google", module: "android-json" 16 | } 17 | 18 | // REST Assured 19 | testImplementation "org.springframework.restdocs:spring-restdocs-core:$version_restDocs" 20 | testImplementation "org.springframework.restdocs:spring-restdocs-restassured:$version_restDocs" 21 | testImplementation "org.springframework.restdocs:spring-restdocs-asciidoctor:$version_restDocs" 22 | } 23 | 24 | 25 | asciidoctor { 26 | final String snippetsDir = "${project.buildDir}/generated-snippets" 27 | final String frameDocsDir = "${project.rootDir}/${project.name}/src/asciidoc" 28 | final String htmlOutputDir = "${project.buildDir}/docs" 29 | /* 30 | * asciidoctor task(버전 3.3.2) 에 한개의 input directory 만 지정할 수 있어 수동작성한 Document frame 을 31 | * 문서화 할 수 없는 문제가 있다. 따라서 asciidoctor 실행 전에 document 들을 largeTest 실행결과로 32 | * 생성한 build/generated-snippets 디렉토리에 복사한다. 33 | */ 34 | copy { 35 | from frameDocsDir 36 | into snippetsDir 37 | } 38 | 39 | // asciidoctor task 환경 설정 40 | baseDir snippetsDir 41 | sourceDir snippetsDir 42 | outputDir htmlOutputDir 43 | 44 | /* 45 | * 이 옵션을 적용하더라도 Windows 시스템에서 인코딩 문제로 문서를 제대로 생성하지 못하는 경우가 있습니다. 46 | * 그때는 제어판 > 고급 > 환경 변수... 메뉴에 진입 후 아래 환경변수를 추가해 주세요. 47 | * 48 | * set JAVA_OPTS=-Dfile.encoding=UTF-8 49 | * set GRADLE_OPTS=-Dfile.encoding=UTF-8 50 | * 51 | * 그리고 시스템을 재부팅 해주시기 바랍니다. 52 | */ 53 | options.encoding = "UTF-8" 54 | } 55 | -------------------------------------------------------------------------------- /server/api-core/src/test/kotlin/testcase/small/domain/user/service/DeleteUserServiceSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package testcase.small.domain.user.service 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.exception.UserNotFoundException 8 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 9 | import com.sirloin.sandbox.server.core.domain.user.service.DeleteUserService 10 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 11 | import org.hamcrest.CoreMatchers.`is` 12 | import org.hamcrest.MatcherAssert.assertThat 13 | import org.junit.jupiter.api.BeforeEach 14 | import org.junit.jupiter.api.DisplayName 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.assertThrows 17 | import test.com.sirloin.annotation.SmallTest 18 | import test.small.domain.randomUser 19 | import test.small.domain.user.MockUserRepository 20 | import java.util.* 21 | 22 | @SmallTest 23 | class DeleteUserServiceSpec { 24 | private lateinit var sut: DeleteUserService 25 | private lateinit var userRepo: UserRepository 26 | 27 | @BeforeEach 28 | fun setUp() { 29 | this.userRepo = MockUserRepository() 30 | this.sut = DeleteUserService.newInstance(userRepo, LocaleProvider.defaultInstance()) 31 | } 32 | 33 | @DisplayName("없는 이용자를 삭제하려 하면 UserNotFoundException 이 발생한다") 34 | @Test 35 | fun `Throws UserNotFoundException for nonexistent user`() { 36 | assertThrows { 37 | sut.deleteUserByUuid(UUID.randomUUID()) 38 | } 39 | } 40 | 41 | @DisplayName("생성한 이용자를 삭제하면, isDeleted 상태가 된다") 42 | @Test 43 | fun `User status is changed to 'isDeleted'`() { 44 | // given: 45 | val savedUser = userRepo.save(randomUser()) 46 | 47 | // then: 48 | val deletedUser = sut.deleteUserByUuid(savedUser.uuid) 49 | 50 | // expect: 51 | assertThat(deletedUser.isDeleted, `is`(true)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/get_profile/state_get_profile.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/domain/base_bloc_state.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 9 | 10 | abstract class GetProfileBlocState extends BaseBlocState {} 11 | 12 | @immutable 13 | class InitialState extends BaseEmptyState implements GetProfileBlocState {} 14 | 15 | @immutable 16 | class ApiErrorState extends BaseExceptionalState implements GetProfileBlocState { 17 | const ApiErrorState(Exception? exception) : super(exception); 18 | 19 | const ApiErrorState.empty() : super(null); 20 | } 21 | 22 | @immutable 23 | class UserDataSetState extends Equatable implements GetProfileBlocState { 24 | final User user; 25 | 26 | const UserDataSetState(this.user); 27 | 28 | @override 29 | List get props => [user]; 30 | 31 | @override 32 | bool get stringify => true; 33 | } 34 | 35 | typedef StateToObject = T Function(U); 36 | 37 | // Dart 에 Sealed class 가 없어서 pattern matching 을 직접 손으로 구현... 38 | extension GetProfileBlocStatePatternMatcher on GetProfileBlocState { 39 | T when( 40 | {required final StateToObject initialState, 41 | required final StateToObject userDataSetState, 42 | required final StateToObject apiErrorState}) { 43 | // Dart 에 Smart cast 가 없어서 불편... 44 | if (this is InitialState) { 45 | return initialState(this as InitialState); 46 | } else if (this is UserDataSetState) { 47 | return userDataSetState(this as UserDataSetState); 48 | } else if (this is ApiErrorState) { 49 | return apiErrorState(this as ApiErrorState); 50 | } 51 | 52 | throw UnimplementedError("Pattern matching rule for '$this extends ProfileScreenState' " 53 | "is not implemented"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/register/state_register_user.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/domain/base_bloc_state.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 9 | 10 | abstract class RegisterUserBlocState extends BaseBlocState {} 11 | 12 | @immutable 13 | class InitialState extends BaseEmptyState implements RegisterUserBlocState {} 14 | 15 | @immutable 16 | class ApiErrorState extends BaseExceptionalState implements RegisterUserBlocState { 17 | const ApiErrorState(Exception? exception) : super(exception); 18 | 19 | const ApiErrorState.empty() : super(null); 20 | } 21 | 22 | @immutable 23 | class UserRegisteredState extends Equatable implements RegisterUserBlocState { 24 | final User user; 25 | 26 | const UserRegisteredState(this.user); 27 | 28 | @override 29 | List get props => [user]; 30 | 31 | @override 32 | bool get stringify => true; 33 | } 34 | 35 | typedef StateToObject = T Function(U); 36 | 37 | // Dart 에 Sealed class 가 없어서 pattern matching 을 직접 손으로 구현... 38 | extension RegisterUserBlocStatePatternMatcher on RegisterUserBlocState { 39 | T when({ 40 | required final StateToObject initialState, 41 | required final StateToObject apiErrorState, 42 | required final StateToObject userRegisteredState, 43 | }) { 44 | // Dart 에 Smart cast 가 없어서 불편... 45 | if (this is InitialState) { 46 | return initialState(this as InitialState); 47 | } else if (this is ApiErrorState) { 48 | return apiErrorState(this as ApiErrorState); 49 | } else if (this is UserRegisteredState) { 50 | return userRegisteredState(this as UserRegisteredState); 51 | } 52 | 53 | throw UnimplementedError("Pattern matching rule for '$this extends RegistrationScreenState' " 54 | "is not implemented"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/lib/domain/user/bloc/edit_profile/state_edit_profile.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-client 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:meta/meta.dart'; 7 | import 'package:sirloin_sandbox_client/domain/base_bloc_state.dart'; 8 | import 'package:sirloin_sandbox_client/domain/user/user.dart'; 9 | 10 | abstract class EditProfileBlocState extends BaseBlocState {} 11 | 12 | @immutable 13 | class InitialState extends BaseEmptyState implements EditProfileBlocState {} 14 | 15 | @immutable 16 | class ApiErrorState extends BaseExceptionalState implements EditProfileBlocState { 17 | const ApiErrorState(Exception? exception) : super(exception); 18 | 19 | const ApiErrorState.empty() : super(null); 20 | } 21 | 22 | @immutable 23 | class UserDataSetState extends Equatable implements EditProfileBlocState { 24 | final User user; 25 | final bool isUpToDate; 26 | 27 | const UserDataSetState(this.user, this.isUpToDate); 28 | 29 | @override 30 | List get props => [user, isUpToDate]; 31 | 32 | @override 33 | bool get stringify => true; 34 | } 35 | 36 | typedef StateToObject = T Function(U); 37 | 38 | // Dart 에 Sealed class 가 없어서 pattern matching 을 직접 손으로 구현... 39 | extension EditProfileBlocStatePatternMatcher on EditProfileBlocState { 40 | T when({ 41 | required final StateToObject initialState, 42 | required final StateToObject apiErrorState, 43 | required final StateToObject userDataSetState, 44 | }) { 45 | // Dart 에 Smart cast 가 없어서 불편... 46 | if (this is InitialState) { 47 | return initialState(this as InitialState); 48 | } else if (this is ApiErrorState) { 49 | return apiErrorState(this as ApiErrorState); 50 | } else if (this is UserDataSetState) { 51 | return userDataSetState(this as UserDataSetState); 52 | } 53 | 54 | throw UnimplementedError("Pattern matching rule for '$this extends EditProfileScreenState' " 55 | "is not implemented"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/gradle/scripts/console.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | enum CC { 6 | BLACK("30m"), 7 | RED("31m"), 8 | GREEN("32m"), 9 | YELLOW("33m"), 10 | BLUE("34m"), 11 | MAGENTA("35m"), 12 | CYAN("36m"), 13 | WHITE("37m"), 14 | LIGHT_BLACK("30;1m"), 15 | LIGHT_RED("31;1m"), 16 | LIGHT_GREEN("32;1m"), 17 | LIGHT_YELLOW("33;1m"), 18 | LIGHT_BLUE("34;1m"), 19 | LIGHT_MAGENTA("35;1m"), 20 | LIGHT_CYAN("36;1m"), 21 | LIGHT_WHITE("37;1m"); 22 | 23 | public final String code; 24 | 25 | private CC(final String code) { 26 | this.code = code 27 | } 28 | } 29 | 30 | final String colour(final CC code, final String text) { 31 | final ESC = "${(char) 27}[" 32 | final RESET = "${ESC}0m" 33 | 34 | return "${ESC}${(code as CC).code}${text}${RESET}" 35 | } 36 | 37 | final String rPad(final int width, final String text) { 38 | int spacesCount = width - text.length() 39 | if (spacesCount > 1) { 40 | final sb = new StringBuffer(spacesCount) 41 | while (sb.length() < spacesCount) { 42 | sb.append(" ") 43 | } 44 | return text + sb.toString() 45 | } else { 46 | return text 47 | } 48 | } 49 | 50 | final OutputStream execWrapper(final List args) { 51 | def stdout = new ByteArrayOutputStream() 52 | exec { 53 | commandLine args 54 | standardOutput = stdout 55 | } 56 | return stdout 57 | } 58 | 59 | ext.CC = { code, text -> colour(code, text) } 60 | ext.RPAD = { width, text -> rPad(width, text) } 61 | ext.cmdLine = { args -> execWrapper(args) } 62 | ext.printlnErr = { text -> System.err.println(text) } 63 | 64 | ext.BLACK = CC.BLACK 65 | ext.RED = CC.RED 66 | ext.GREEN = CC.GREEN 67 | ext.YELLOW = CC.YELLOW 68 | ext.BLUE = CC.BLUE 69 | ext.MAGENTA = CC.MAGENTA 70 | ext.CYAN = CC.CYAN 71 | ext.WHITE = CC.WHITE 72 | ext.LIGHT_BLACK = CC.LIGHT_BLACK 73 | ext.LIGHT_RED = CC.LIGHT_RED 74 | ext.LIGHT_GREEN = CC.LIGHT_GREEN 75 | ext.LIGHT_YELLOW = CC.LIGHT_YELLOW 76 | ext.LIGHT_BLUE = CC.LIGHT_BLUE 77 | ext.LIGHT_MAGENTA = CC.LIGHT_MAGENTA 78 | ext.LIGHT_CYAN = CC.LIGHT_CYAN 79 | ext.LIGHT_WHITE = CC.LIGHT_WHITE 80 | -------------------------------------------------------------------------------- /client/test/testcase/small/data/repository/user/repo_user_impl_test_deregister.dart: -------------------------------------------------------------------------------- 1 | // test target 의 method name 표현을 위한 naming rule 위반 2 | // ignore_for_file: file_names 3 | // test 만 따로 package 형태로 import 할 방법이 없음 4 | // ignore_for_file: avoid_relative_lib_imports 5 | /* 6 | * sirloin-sandbox-client 7 | * Distributed under CC BY-NC-SA 8 | */ 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:mockito/mockito.dart'; 11 | import 'package:sirloin_sandbox_client/data/repository/user/repo_user_impl.dart'; 12 | import 'package:uuid/uuid.dart'; 13 | 14 | import '../../../../../mock/lib/data/local/user/localstorage_user.mocks.dart'; 15 | import '../../../../../mock/lib/data/remote/http/user/api_user.mocks.dart'; 16 | import 'repo_user_impl_test.dart'; 17 | 18 | void deregisterTestCases( 19 | final SutSupplier supplier, final MockUserLocalStorage userLocalStorage, final MockUserApi userApi) { 20 | late String uuid; 21 | late UserRepositoryImpl sut; 22 | 23 | setUp(() { 24 | sut = supplier.call(); 25 | 26 | uuid = const Uuid().v4().toString(); 27 | }); 28 | 29 | test("탈퇴 API 호출에 성공하면, local data 도 삭제한다", () async { 30 | // when: 31 | when(userApi.deleteUser(any)).thenAnswer((realInvocation) async { 32 | final realUuid = realInvocation.positionalArguments[0] as String; 33 | return realUuid; 34 | }); 35 | when(userLocalStorage.delete(any)).thenAnswer((realInvocation) async => true); 36 | when(userLocalStorage.saveMyUuid(any)).thenAnswer((realInvocation) async => null); 37 | 38 | // then: 39 | await sut.deregister(uuid); 40 | 41 | // expect: 42 | verify(userLocalStorage.delete(uuid)).called(1); 43 | verify(userLocalStorage.saveMyUuid(null)).called(1); 44 | }); 45 | 46 | test("탈퇴 API 호출에 실패하면, local data 를 삭제하면 안된다", () async { 47 | // when: 48 | when(userApi.deleteUser(any)) 49 | .thenAnswer((realInvocation) async => throw UnimplementedError("deleteUser API ERROR!!")); 50 | 51 | // expect: 52 | expect(() async => await sut.deregister(uuid), throwsA(const TypeMatcher())); 53 | 54 | // expect: 55 | verifyNever(userLocalStorage.delete(uuid)); 56 | verifyNever(userLocalStorage.saveMyUuid(null)); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /server/api-main/src/main/kotlin/com/sirloin/sandbox/server/api/appconfig/domain/user/UserBeanConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * sirloin-sandbox-server 3 | * Distributed under CC BY-NC-SA 4 | */ 5 | package com.sirloin.sandbox.server.api.appconfig.domain.user 6 | 7 | import com.sirloin.sandbox.server.core.domain.user.repository.UserReadonlyRepository 8 | import com.sirloin.sandbox.server.core.domain.user.repository.UserRepository 9 | import com.sirloin.sandbox.server.core.domain.user.service.CreateUserService 10 | import com.sirloin.sandbox.server.core.domain.user.service.DeleteUserService 11 | import com.sirloin.sandbox.server.core.domain.user.service.GetUserService 12 | import com.sirloin.sandbox.server.core.domain.user.service.UpdateUserService 13 | import com.sirloin.sandbox.server.core.i18n.LocaleProvider 14 | import org.springframework.beans.factory.annotation.Qualifier 15 | import org.springframework.context.annotation.Bean 16 | import org.springframework.context.annotation.Configuration 17 | 18 | /** 19 | * User 도메인 로직을 Spring Bean 으로 등록합니다. 20 | * 21 | * @since 2022-02-14 22 | */ 23 | @Configuration 24 | class UserBeanConfig { 25 | @Bean 26 | fun createUserService( 27 | @Qualifier(UserRepository.NAME) userRepository: UserRepository, 28 | localeProvider: LocaleProvider 29 | ): CreateUserService = 30 | CreateUserService.newInstance(userRepository, localeProvider) 31 | 32 | @Bean 33 | fun getUserService( 34 | @Qualifier(UserReadonlyRepository.NAME) userReadonlyRepository: UserReadonlyRepository, 35 | localeProvider: LocaleProvider 36 | ): GetUserService = 37 | GetUserService.newInstance(userReadonlyRepository, localeProvider) 38 | 39 | @Bean 40 | fun updateUserService( 41 | @Qualifier(UserRepository.NAME) userRepository: UserRepository, 42 | localeProvider: LocaleProvider 43 | ): UpdateUserService = 44 | UpdateUserService.newInstance(userRepository, localeProvider) 45 | 46 | @Bean 47 | fun deleteUserService( 48 | @Qualifier(UserRepository.NAME) userRepository: UserRepository, 49 | localeProvider: LocaleProvider 50 | ): DeleteUserService = 51 | DeleteUserService.newInstance(userRepository, localeProvider) 52 | } 53 | --------------------------------------------------------------------------------