diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fb9ce31..e4bb8f3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,37 @@ jobs: printf '%s' "$IOS_GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > macos/Runner/GoogleService-Info.plist fi + - name: Verify Firebase config files + run: | + set -euo pipefail + + echo "πŸ” Verifying Firebase configuration files..." + + # Check if google-services.json exists and is valid + if [ ! -f "android/app/google-services.json" ]; then + echo "❌ android/app/google-services.json does not exist" >&2 + exit 1 + fi + + # Check if file is not empty + if [ ! -s "android/app/google-services.json" ]; then + echo "❌ android/app/google-services.json is empty" >&2 + exit 1 + fi + + # Check if JSON is valid and contains the expected package name + if ! python3 -c "import json, sys; data = json.load(open('android/app/google-services.json')); clients = [c for c in data.get('client', []) if c.get('client_info', {}).get('android_client_info', {}).get('package_name') == 'com.clustudy.clustudy']; sys.exit(0 if clients else 1)" 2>/dev/null; then + echo "⚠️ Warning: google-services.json does not contain package name 'com.clustudy.clustudy'" >&2 + echo "πŸ“„ File contents (first 500 chars):" >&2 + head -c 500 android/app/google-services.json | cat >&2 + echo "" >&2 + echo "πŸ” Searching for package names in file:" >&2 + python3 -c "import json; data = json.load(open('android/app/google-services.json')); [print(f\" - {c.get('client_info', {}).get('android_client_info', {}).get('package_name', 'N/A')}\") for c in data.get('client', [])]" 2>/dev/null || echo " Could not parse JSON" >&2 + exit 1 + fi + + echo "βœ… android/app/google-services.json is valid and contains expected package name" + - name: Setup Flutter uses: subosito/flutter-action@v2 with: @@ -62,6 +93,13 @@ jobs: channel: "stable" cache: true + - name: Install FlutterFire CLI + run: | + dart pub global activate flutterfire_cli + echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH" + export PATH="$PATH:$HOME/.pub-cache/bin" + flutterfire --version + - name: Get dependencies run: flutter pub get @@ -119,6 +157,37 @@ jobs: printf '%s' "$IOS_GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > macos/Runner/GoogleService-Info.plist fi + - name: Verify Firebase config files + run: | + set -euo pipefail + + echo "πŸ” Verifying Firebase configuration files..." + + # Check if google-services.json exists and is valid + if [ ! -f "android/app/google-services.json" ]; then + echo "❌ android/app/google-services.json does not exist" >&2 + exit 1 + fi + + # Check if file is not empty + if [ ! -s "android/app/google-services.json" ]; then + echo "❌ android/app/google-services.json is empty" >&2 + exit 1 + fi + + # Check if JSON is valid and contains the expected package name + if ! python3 -c "import json, sys; data = json.load(open('android/app/google-services.json')); clients = [c for c in data.get('client', []) if c.get('client_info', {}).get('android_client_info', {}).get('package_name') == 'com.clustudy.clustudy']; sys.exit(0 if clients else 1)" 2>/dev/null; then + echo "⚠️ Warning: google-services.json does not contain package name 'com.clustudy.clustudy'" >&2 + echo "πŸ“„ File contents (first 500 chars):" >&2 + head -c 500 android/app/google-services.json | cat >&2 + echo "" >&2 + echo "πŸ” Searching for package names in file:" >&2 + python3 -c "import json; data = json.load(open('android/app/google-services.json')); [print(f\" - {c.get('client_info', {}).get('android_client_info', {}).get('package_name', 'N/A')}\") for c in data.get('client', [])]" 2>/dev/null || echo " Could not parse JSON" >&2 + exit 1 + fi + + echo "βœ… android/app/google-services.json is valid and contains expected package name" + - name: Setup Java uses: actions/setup-java@v4 with: @@ -219,6 +288,13 @@ jobs: channel: "stable" cache: true + - name: Install FlutterFire CLI + run: | + dart pub global activate flutterfire_cli + echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH" + export PATH="$PATH:$HOME/.pub-cache/bin" + flutterfire --version + - name: Cache Flutter dependencies uses: actions/cache@v4 with: @@ -242,14 +318,14 @@ jobs: pod install cd .. - - name: Build iOS (no codesign) - run: flutter build ios --release --no-codesign + - name: Build iOS (simulator only) + run: flutter build ios --debug --simulator - name: Verify iOS build run: | - if [ -d "build/ios/iphoneos/Runner.app" ]; then - echo "βœ… iOS build successful" - ls -la build/ios/iphoneos/ + if [ -d "build/ios/iphonesimulator/Runner.app" ]; then + echo "βœ… iOS simulator build successful" + ls -la build/ios/iphonesimulator/ else echo "❌ iOS build failed" exit 1 diff --git a/.gitignore b/.gitignore index 7fb9ab71..d0e0ddeb 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ app.*.map.json **/*.key **/*.pem **/key.properties +.github-secrets/ # iOS related **/ios/Flutter/App.framework diff --git a/LEGAL_TEMPLATES.md b/LEGAL.md similarity index 96% rename from LEGAL_TEMPLATES.md rename to LEGAL.md index acb952bd..058819c1 100644 --- a/LEGAL_TEMPLATES.md +++ b/LEGAL.md @@ -1,13 +1,6 @@ -# 법적 λ¬Έμ„œ ν…œν”Œλ¦Ώ +# 법적 λ¬Έμ„œ -이 λ¬Έμ„œλŠ” Clustudy μ•±μŠ€ν† μ–΄ μΆœμ‹œλ₯Ό μœ„ν•œ κ°œμΈμ •λ³΄ 보호 μ •μ±… 및 μ΄μš©μ•½κ΄€ ν…œν”Œλ¦Ώμž…λ‹ˆλ‹€. - -## μ‚¬μš© 방법 - -1. μ•„λž˜ ν…œν”Œλ¦Ώμ„ λ³΅μ‚¬ν•˜μ—¬ μ‚¬μš©ν•˜μ„Έμš” -2. `[PLACEHOLDER]`둜 ν‘œμ‹œλœ 뢀뢄을 μ‹€μ œ μ •λ³΄λ‘œ κ΅μ²΄ν•˜μ„Έμš” -3. 법적 κ²€ν† κ°€ ν•„μš”ν•œ 경우 전문가와 μƒλ‹΄ν•˜μ„Έμš” -4. μ΅œμ’… 버전을 `lib/design_system/screens/settings/widgets/legal_text_dialog.dart`의 `dummyPrivacyPolicyText`, `dummyTermsOfServiceText` μƒμˆ˜μ— λ³΅μ‚¬ν•˜μ„Έμš” +이 λ¬Έμ„œλŠ” Clustudy μ•±μŠ€ν† μ–΄ μΆœμ‹œλ₯Ό μœ„ν•œ κ°œμΈμ •λ³΄ 보호 μ •μ±… 및 μ΄μš©μ•½κ΄€ μž…λ‹ˆλ‹€. --- diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 935fc4e2..e0b90122 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.android.application") // START: FlutterFire Configuration id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. @@ -21,7 +22,7 @@ val hasReleaseKeystore = if (keystorePropertiesFile.exists()) { } android { - namespace = "com.trycatchping.clustudy" + namespace = "com.clustudy.clustudy" compileSdk = flutter.compileSdkVersion ndkVersion = "29.0.13599879" @@ -35,7 +36,7 @@ android { } defaultConfig { - applicationId = "com.trycatchping.clustudy" + applicationId = "com.clustudy.clustudy" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/kotlin/com/trycatchping/clustudy/MainActivity.kt b/android/app/src/main/kotlin/com/clustudy/clustudy/MainActivity.kt similarity index 98% rename from android/app/src/main/kotlin/com/trycatchping/clustudy/MainActivity.kt rename to android/app/src/main/kotlin/com/clustudy/clustudy/MainActivity.kt index 4c5fddc4..4c0f74aa 100644 --- a/android/app/src/main/kotlin/com/trycatchping/clustudy/MainActivity.kt +++ b/android/app/src/main/kotlin/com/clustudy/clustudy/MainActivity.kt @@ -1,4 +1,4 @@ -package com.trycatchping.clustudy +package com.clustudy.clustudy import android.os.Handler import android.os.Looper @@ -10,7 +10,7 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel private const val INSTALL_REFERRER_CHANNEL = - "com.trycatchping.clustudy/install_referrer" + "com.clustudy.clustudy/install_referrer" class MainActivity : FlutterActivity() { private val mainHandler = Handler(Looper.getMainLooper()) diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index bd7522f7..b15106c9 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -21,6 +21,7 @@ plugins { id("com.android.application") version "8.7.3" apply false // START: FlutterFire Configuration id("com.google.gms.google-services") version("4.3.15") apply false + id("com.google.firebase.crashlytics") version("2.8.1") apply false // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/README_prev.md b/docs/README_prev.md similarity index 100% rename from README_prev.md rename to docs/README_prev.md diff --git a/docs/screenshots/10inch tablet/Screenshot_1760788958.png b/docs/screenshots/10inch tablet/Screenshot_1760788958.png new file mode 100644 index 00000000..5f359962 Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760788958.png differ diff --git a/docs/screenshots/10inch tablet/Screenshot_1760789004.png b/docs/screenshots/10inch tablet/Screenshot_1760789004.png new file mode 100644 index 00000000..cdab07eb Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760789004.png differ diff --git a/docs/screenshots/10inch tablet/Screenshot_1760789081.png b/docs/screenshots/10inch tablet/Screenshot_1760789081.png new file mode 100644 index 00000000..9fcaa6dc Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760789081.png differ diff --git a/docs/screenshots/10inch tablet/Screenshot_1760789098.png b/docs/screenshots/10inch tablet/Screenshot_1760789098.png new file mode 100644 index 00000000..cf1534e4 Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760789098.png differ diff --git a/docs/screenshots/10inch tablet/Screenshot_1760789105.png b/docs/screenshots/10inch tablet/Screenshot_1760789105.png new file mode 100644 index 00000000..4ad734cd Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760789105.png differ diff --git a/docs/screenshots/10inch tablet/Screenshot_1760789177.png b/docs/screenshots/10inch tablet/Screenshot_1760789177.png new file mode 100644 index 00000000..768c9cad Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760789177.png differ diff --git a/docs/screenshots/10inch tablet/Screenshot_1760789247.png b/docs/screenshots/10inch tablet/Screenshot_1760789247.png new file mode 100644 index 00000000..8f27296f Binary files /dev/null and b/docs/screenshots/10inch tablet/Screenshot_1760789247.png differ diff --git a/docs/screenshots/6.5inch phone/Screenshot_1760787770.png b/docs/screenshots/6.5inch phone/Screenshot_1760787770.png new file mode 100644 index 00000000..4dd18490 Binary files /dev/null and b/docs/screenshots/6.5inch phone/Screenshot_1760787770.png differ diff --git a/docs/screenshots/6.5inch phone/Screenshot_1760787825.png b/docs/screenshots/6.5inch phone/Screenshot_1760787825.png new file mode 100644 index 00000000..abeb672f Binary files /dev/null and b/docs/screenshots/6.5inch phone/Screenshot_1760787825.png differ diff --git a/docs/screenshots/6.5inch phone/Screenshot_1760787980.png b/docs/screenshots/6.5inch phone/Screenshot_1760787980.png new file mode 100644 index 00000000..7e4766a1 Binary files /dev/null and b/docs/screenshots/6.5inch phone/Screenshot_1760787980.png differ diff --git a/docs/screenshots/6.5inch phone/Screenshot_1760788008.png b/docs/screenshots/6.5inch phone/Screenshot_1760788008.png new file mode 100644 index 00000000..e0998b14 Binary files /dev/null and b/docs/screenshots/6.5inch phone/Screenshot_1760788008.png differ diff --git a/docs/screenshots/6.5inch phone/Screenshot_1760788210.png b/docs/screenshots/6.5inch phone/Screenshot_1760788210.png new file mode 100644 index 00000000..406d4849 Binary files /dev/null and b/docs/screenshots/6.5inch phone/Screenshot_1760788210.png differ diff --git a/docs/screenshots/6.5inch phone/Screenshot_1760788293.png b/docs/screenshots/6.5inch phone/Screenshot_1760788293.png new file mode 100644 index 00000000..1990cca9 Binary files /dev/null and b/docs/screenshots/6.5inch phone/Screenshot_1760788293.png differ diff --git a/docs/screenshots/7inch tablet/Screenshot_1760788528.png b/docs/screenshots/7inch tablet/Screenshot_1760788528.png new file mode 100644 index 00000000..aba31c51 Binary files /dev/null and b/docs/screenshots/7inch tablet/Screenshot_1760788528.png differ diff --git a/docs/screenshots/7inch tablet/Screenshot_1760788536.png b/docs/screenshots/7inch tablet/Screenshot_1760788536.png new file mode 100644 index 00000000..1dc3be31 Binary files /dev/null and b/docs/screenshots/7inch tablet/Screenshot_1760788536.png differ diff --git a/docs/screenshots/7inch tablet/Screenshot_1760788647.png b/docs/screenshots/7inch tablet/Screenshot_1760788647.png new file mode 100644 index 00000000..671afabd Binary files /dev/null and b/docs/screenshots/7inch tablet/Screenshot_1760788647.png differ diff --git a/docs/screenshots/7inch tablet/Screenshot_1760788714.png b/docs/screenshots/7inch tablet/Screenshot_1760788714.png new file mode 100644 index 00000000..84132ab9 Binary files /dev/null and b/docs/screenshots/7inch tablet/Screenshot_1760788714.png differ diff --git a/docs/screenshots/7inch tablet/Screenshot_1760788764.png b/docs/screenshots/7inch tablet/Screenshot_1760788764.png new file mode 100644 index 00000000..82dddd12 Binary files /dev/null and b/docs/screenshots/7inch tablet/Screenshot_1760788764.png differ diff --git a/docs/screenshots/7inch tablet/Screenshot_1760788807.png b/docs/screenshots/7inch tablet/Screenshot_1760788807.png new file mode 100644 index 00000000..c2e6513b Binary files /dev/null and b/docs/screenshots/7inch tablet/Screenshot_1760788807.png differ diff --git "a/docs/screenshots/\352\267\270\353\236\230\355\224\275\354\235\264\353\257\270\354\247\200.png" "b/docs/screenshots/\352\267\270\353\236\230\355\224\275\354\235\264\353\257\270\354\247\200.png" new file mode 100644 index 00000000..4c4a2fa8 Binary files /dev/null and "b/docs/screenshots/\352\267\270\353\236\230\355\224\275\354\235\264\353\257\270\354\247\200.png" differ diff --git a/firebase.json b/firebase.json index 93d03abb..178abe2a 100644 --- a/firebase.json +++ b/firebase.json @@ -1 +1 @@ -{"flutter":{"platforms":{"android":{"default":{"projectId":"clustudy-taeung","appId":"1:1818279587:android:c0167b49740c31c5c7c916","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"clustudy-taeung","appId":"1:1818279587:ios:221dd0c885ebd6bac7c916","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"clustudy-taeung","appId":"1:1818279587:ios:221dd0c885ebd6bac7c916","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"clustudy-taeung","configurations":{"android":"1:1818279587:android:c0167b49740c31c5c7c916","ios":"1:1818279587:ios:221dd0c885ebd6bac7c916","macos":"1:1818279587:ios:221dd0c885ebd6bac7c916","web":"1:1818279587:web:ad8e4508c47c22e0c7c916","windows":"1:1818279587:web:41abeccf7931ea47c7c916"}}}}}} \ No newline at end of file +{"flutter":{"platforms":{"android":{"default":{"projectId":"clustudy-2baa4","appId":"1:467430016303:android:e7972d16cf6c22bfe98dda","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"clustudy-2baa4","appId":"1:467430016303:ios:a916328bd25c3dcce98dda","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"clustudy-2baa4","appId":"1:467430016303:ios:a916328bd25c3dcce98dda","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"clustudy-2baa4","configurations":{"android":"1:467430016303:android:e7972d16cf6c22bfe98dda","ios":"1:467430016303:ios:a916328bd25c3dcce98dda","macos":"1:467430016303:ios:a916328bd25c3dcce98dda","web":"1:467430016303:web:b8f05911336ae18de98dda","windows":"1:467430016303:web:ed8ec57c962c693ee98dda"}}}}}} \ No newline at end of file diff --git a/fonts/Pretendard-Bold.otf b/fonts/Pretendard-Bold.otf new file mode 100644 index 00000000..8e5e30a2 Binary files /dev/null and b/fonts/Pretendard-Bold.otf differ diff --git a/fonts/Pretendard-Regular.otf b/fonts/Pretendard-Regular.otf new file mode 100644 index 00000000..08bf4cfc Binary files /dev/null and b/fonts/Pretendard-Regular.otf differ diff --git a/fonts/Pretendard-SemiBold.otf b/fonts/Pretendard-SemiBold.otf new file mode 100644 index 00000000..e7e36abc Binary files /dev/null and b/fonts/Pretendard-SemiBold.otf differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 090eee65..d5606e2d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -202,6 +202,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 595C6E4E49691832B8FBF459 /* [CP] Copy Pods Resources */, + 29EC9DA13CF69125BB54B02B /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, ); buildRules = ( ); @@ -274,6 +275,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 29EC9DA13CF69125BB54B02B /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/lib/design_system/components/organisms/note_top_toolbar.dart b/lib/design_system/components/organisms/note_top_toolbar.dart index 1be5da57..efc8d953 100644 --- a/lib/design_system/components/organisms/note_top_toolbar.dart +++ b/lib/design_system/components/organisms/note_top_toolbar.dart @@ -1,26 +1,34 @@ // lib/design_system/components/organisms/note_top_toolbar.dart import 'package:flutter/material.dart'; +import '../../../shared/constants/breakpoints.dart'; import '../../tokens/app_colors.dart'; import '../../tokens/app_spacing.dart'; import '../../tokens/app_typography.dart'; import '../atoms/app_icon_button.dart'; +/// νˆ΄λ°” μ•‘μ…˜μ„ λ‚˜νƒ€λ‚΄λŠ” 데이터 클래슀 class ToolbarAction { + /// νˆ΄λ°” μ•‘μ…˜ μƒμ„±μž const ToolbarAction({ required this.svgPath, required this.onTap, this.tooltip, }); + /// SVG μ•„μ΄μ½˜ 경둜 final String svgPath; + /// νƒ­ μ‹œ 싀행될 콜백 final VoidCallback onTap; + /// 툴팁 ν…μŠ€νŠΈ (μ˜΅μ…˜) final String? tooltip; } // D: NoteScreen μ—μ„œ μ‚¬μš© // F: NoteEditorScreen μ—μ„œ μ‚¬μš© // 전체화면 μ‹œ λΉ„ν™œμ„±ν™” +/// λ…ΈνŠΈ ν™”λ©΄ 상단 νˆ΄λ°” μœ„μ ― class NoteTopToolbar extends StatelessWidget implements PreferredSizeWidget { + /// λ…ΈνŠΈ 상단 νˆ΄λ°” μƒμ„±μž const NoteTopToolbar({ super.key, required this.title, @@ -33,40 +41,60 @@ class NoteTopToolbar extends StatelessWidget implements PreferredSizeWidget { this.showBottomDivider = true, }); + /// νˆ΄λ°” 제λͺ© final String title; + /// μ™Όμͺ½ μ•‘μ…˜ λ²„νŠΌ λͺ©λ‘ final List leftActions; + /// 였λ₯Έμͺ½ μ•‘μ…˜ λ²„νŠΌ λͺ©λ‘ final List rightActions; + /// μ•„μ΄μ½˜ 색상 final Color iconColor; + /// μ•„μ΄μ½˜ 크기 final double iconSize; + /// νˆ΄λ°” 높이 final double height; - final TextStyle? titleStyle; // κΈ°λ³Έ μŠ€νƒ€μΌμ€ μ•„λž˜μ—μ„œ 정함 + /// 제λͺ© ν…μŠ€νŠΈ μŠ€νƒ€μΌ (κΈ°λ³Έ μŠ€νƒ€μΌμ€ μ•„λž˜μ—μ„œ 정함) + final TextStyle? titleStyle; + /// ν•˜λ‹¨ ꡬ뢄선 ν‘œμ‹œ μ—¬λΆ€ final bool showBottomDivider; @override Size get preferredSize { - // Note: MediaQuery is not available here, so we use a conservative estimate. + // Note: MediaQuery is not available here, + // so we use a conservative estimate. // The actual height is adjusted in build(). return Size.fromHeight(height + 50); // Estimate for status bar } @override Widget build(BuildContext context) { - final ts = - titleStyle ?? - AppTypography.subtitle1.copyWith( - color: AppColors.gray50, - ); // μŠ€μƒ·μ²˜λŸΌ μž‘κ³  쀑립 톀 - final mediaQuery = MediaQuery.of(context); + final screenWidth = mediaQuery.size.width; + final isMobile = Breakpoints.isMobile(screenWidth); + + // λͺ¨λ°”일일 λ•Œ ν…μŠ€νŠΈ 크기 μΆ•μ†Œ: subtitle1 (17px) β†’ body2 (16px) + final ts = titleStyle ?? + (isMobile + ? AppTypography.body2.copyWith( + color: AppColors.gray50, + ) + : AppTypography.subtitle1.copyWith( + color: AppColors.gray50, + )); // μŠ€μƒ·μ²˜λŸΌ μž‘κ³  쀑립 톀 + final topPadding = mediaQuery.padding.top + 15; final totalHeight = height + mediaQuery.padding.top; + // λͺ¨λ°”일일 λ•Œ νŒ¨λ”© μΆ•μ†Œ: 30px β†’ 16px + final horizontalPadding = + isMobile ? AppSpacing.medium : AppSpacing.screenPadding; + return Container( height: totalHeight, padding: EdgeInsets.only( - left: AppSpacing.screenPadding, // 30 - right: AppSpacing.screenPadding, // 30 + left: horizontalPadding, + right: horizontalPadding, top: topPadding, // SafeArea top + 15 bottom: 15, ), @@ -92,11 +120,15 @@ class NoteTopToolbar extends StatelessWidget implements PreferredSizeWidget { svgPath: leftActions[i].svgPath, onPressed: leftActions[i].onTap, tooltip: leftActions[i].tooltip, - size: AppIconButtonSize.md, // md = 32px 프리셋 + size: AppIconButtonSize.md, // md = 40px ν„°μΉ˜ νƒ€κ²Ÿ color: iconColor, ), if (i != leftActions.length - 1) - const SizedBox(width: AppSpacing.medium), // 16 + SizedBox( + width: isMobile + ? AppSpacing.small + : AppSpacing.medium, // λͺ¨λ°”일: 8px, νƒœλΈ”λ¦Ώ: 16px + ), ], ], ), @@ -104,11 +136,19 @@ class NoteTopToolbar extends StatelessWidget implements PreferredSizeWidget { // κ°€μš΄λ° 제λͺ© IgnorePointer( - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: ts, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isMobile + ? screenWidth * 0.5 + : double.infinity, // λͺ¨λ°”μΌμ—μ„œ μ΅œλŒ€ λ„ˆλΉ„ μ œν•œ + ), + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ts, + textAlign: TextAlign.center, + ), ), ), @@ -123,12 +163,15 @@ class NoteTopToolbar extends StatelessWidget implements PreferredSizeWidget { svgPath: rightActions[i].svgPath, onPressed: rightActions[i].onTap, tooltip: rightActions[i].tooltip, - // 톡일이 μ•ˆλ˜μ–΄μžˆλ„€.. - size: AppIconButtonSize.md, + size: AppIconButtonSize.md, // md = 40px ν„°μΉ˜ νƒ€κ²Ÿ color: iconColor, ), if (i != rightActions.length - 1) - const SizedBox(width: AppSpacing.medium), // 16 + SizedBox( + width: isMobile + ? AppSpacing.small + : AppSpacing.medium, // λͺ¨λ°”일: 8px, νƒœλΈ”λ¦Ώ: 16px + ), ], ], ), diff --git a/lib/design_system/utils/extensions.dart b/lib/design_system/utils/extensions.dart index 34736dec..634c3d6e 100644 --- a/lib/design_system/utils/extensions.dart +++ b/lib/design_system/utils/extensions.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; // import '../tokens/app_colors.dart'; // import '../tokens/app_spacing.dart'; +import '../../shared/constants/breakpoints.dart'; /// πŸ”§ λ””μžμΈ μ‹œμŠ€ν…œ μœ ν‹Έλ¦¬ν‹° ν™•μž₯ /// @@ -42,7 +43,11 @@ extension BuildContextExtensions on BuildContext { bool get isKeyboardOpen => MediaQuery.of(this).viewInsets.bottom > 0; /// λ°˜μ‘ν˜• λ””μžμΈμ„ μœ„ν•œ breakpoint 확인 - bool get isMobile => screenWidth < 768; - bool get isTablet => screenWidth >= 768 && screenWidth < 1024; - bool get isDesktop => screenWidth >= 1024; + /// Breakpoints 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬ 600px κΈ°μ€€μœΌλ‘œ 톡일 + /// λͺ¨λ°”일 화면인지 확인 (600px 미만) + bool get isMobile => Breakpoints.isMobile(screenWidth); + /// νƒœλΈ”λ¦Ώ 화면인지 확인 (600px 이상) + bool get isTablet => Breakpoints.isTablet(screenWidth); + /// λ°μŠ€ν¬ν†± 화면인지 확인 + bool get isDesktop => Breakpoints.isDesktop(screenWidth); } diff --git a/lib/shared/services/install_attribution_service.dart b/lib/shared/services/install_attribution_service.dart index bbd0460d..b5cd03b4 100644 --- a/lib/shared/services/install_attribution_service.dart +++ b/lib/shared/services/install_attribution_service.dart @@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'firebase_service_providers.dart'; const MethodChannel _installReferrerChannel = MethodChannel( - 'com.trycatchping.clustudy/install_referrer', + 'com.clustudy.clustudy/install_referrer', ); /// Provides access to the install attribution payload captured at bootstrap. diff --git a/linker_milestone_1.md b/linker_milestone_1.md deleted file mode 100644 index fbffa569..00000000 --- a/linker_milestone_1.md +++ /dev/null @@ -1,251 +0,0 @@ -## 링컀 κΈ°λŠ₯ κ΅¬ν˜„ λ§ˆμΌμŠ€ν†€ 및 ν˜„μž¬ μž‘μ—… λͺ…μ„Έ - -### 전체 λ§ˆμΌμŠ€ν†€ κ°œμš” - -#### Milestone 1: 링크 생성 기반 ꡬ좕 βœ… (ν˜„μž¬ λͺ©ν‘œ) - -- 링컀 λ„κ΅¬λ‘œ μ§μ‚¬κ°ν˜• 선택 -- 링크 데이터 λͺ¨λΈ μ •μ˜ -- 링크 생성 및 μ €μž₯ - -#### Milestone 2: 링크 μ‹œκ°ν™” 및 μƒν˜Έμž‘μš© - -- 링크 μ‹œκ°μ  ν‘œν˜„ -- 링크 클릭 감지 및 λ„€λΉ„κ²Œμ΄μ…˜ -- 링크 보호 λ©”μ»€λ‹ˆμ¦˜ - -#### Milestone 3: 링크 관리 κΈ°λŠ₯ - -- 링크 νŽΈμ§‘/μ‚­μ œ -- 백링크 ν‘œμ‹œ -- 링크 λͺ©λ‘ 보기 - -#### Milestone 4: κ³ κΈ‰ κΈ°λŠ₯ - -- 펜 μŠ€νƒ€μΌ 링컀 μΆ”κ°€ -- μ™ΈλΆ€ URL 링크 -- 링크 κ·Έλž˜ν”„ μ‹œκ°ν™” - ---- - -## ν˜„μž¬ μž‘μ—… λͺ…μ„Έ (Milestone 1) - -### 1. 데이터 λͺ¨λΈ κ΅¬ν˜„ - -#### 1.1 LinkModel 클래슀 생성 - -**파일 μœ„μΉ˜:** `lib/features/canvas/models/link_model.dart` - -```dart -class LinkModel { - final String id; - final String sourceNoteId; - final String sourcePageId; - final String targetNoteId; - final String? targetPageId; - final Rect boundingBox; - final DateTime createdAt; - final DateTime updatedAt; - - // μƒμ„±μž - // fromJson, toJson λ©”μ„œλ“œ - // copyWith λ©”μ„œλ“œ -} -``` - -#### 1.2 NotePageModel ν™•μž₯ - -**파일:** `lib/features/notes/models/note_page_model.dart` - -**μΆ”κ°€ν•  λ‚΄μš©:** - -- `List links = []` ν•„λ“œ μΆ”κ°€ -- `void addLink(LinkModel link)` λ©”μ„œλ“œ -- `void removeLink(String linkId)` λ©”μ„œλ“œ -- `List getLinksInArea(Rect area)` λ©”μ„œλ“œ -- JSON 직렬화에 links 포함 - -### 2. 링컀 도ꡬ UI κ΅¬ν˜„ - -#### 2.1 μ§μ‚¬κ°ν˜• 선택 μ˜€λ²„λ ˆμ΄ - -**파일 생성:** `lib/features/canvas/widgets/link_selection_overlay.dart` - -**κ΅¬ν˜„ λ‚΄μš©:** - -- CustomPainterλ₯Ό μ‚¬μš©ν•œ μ§μ‚¬κ°ν˜• 그리기 -- μ‹œμž‘μ κ³Ό 끝점을 λ°›μ•„ μ‹€μ‹œκ°„ λ Œλ”λ§ -- 핑크색 반투λͺ… μ±„μš°κΈ° + ν…Œλ‘λ¦¬ - -**λ™μž‘ λͺ…μ„Έ:** - -1. λ“œλž˜κ·Έ μ‹œμž‘: 반투λͺ… 핑크 (opacity: 0.3) -2. λ“œλž˜κ·Έ 쀑: ν…Œλ‘λ¦¬ μ• λ‹ˆλ©”μ΄μ…˜ (점선 효과) -3. μ΅œμ†Œ 크기: 20x20 ν”½μ…€ -4. μ΅œμ†Œ 크기 미달 μ‹œ: 빨간색 ν…Œλ‘λ¦¬λ‘œ κ²½κ³  - -#### 2.2 CustomScribbleNotifier ν™•μž₯ - -**파일:** `lib/features/canvas/notifiers/custom_scribble_notifier.dart` - -**μΆ”κ°€ν•  μƒνƒœ:** - -```dart -// 링크 선택 κ΄€λ ¨ μƒνƒœ -Offset? linkSelectionStart; -Offset? linkSelectionEnd; -bool isLinkSelecting = false; -``` - -**μΆ”κ°€ν•  λ©”μ„œλ“œ:** - -```dart -void startLinkSelection(Offset point) -void updateLinkSelection(Offset point) -void completeLinkSelection() -void cancelLinkSelection() -``` - -### 3. 링크 생성 λ‹€μ΄μ–Όλ‘œκ·Έ - -#### 3.1 λ‹€μ΄μ–Όλ‘œκ·Έ μœ„μ ― - -**파일 생성:** `lib/features/canvas/widgets/dialogs/link_creation_dialog.dart` - -**UI ꡬ성:** - -``` -제λͺ©: "링크 생성" - -λ³Έλ¬Έ: -- 선택 μ˜μ—­ 미리보기 (μž‘μ€ 썸넀일) -- λŒ€μƒ λ…ΈνŠΈ 선택 - - λ“œλ‘­λ‹€μš΄ 메뉴 - - 졜근 λ…ΈνŠΈ 5개 μš°μ„  ν‘œμ‹œ - - "λͺ¨λ“  λ…ΈνŠΈ 보기" μ˜΅μ…˜ -- νŠΉμ • νŽ˜μ΄μ§€ μ§€μ • (선택사항) - - μ²΄ν¬λ°•μŠ€: "νŠΉμ • νŽ˜μ΄μ§€λ‘œ 링크" - - νŽ˜μ΄μ§€ 번호 μž…λ ₯ - -λ²„νŠΌ: -- μ·¨μ†Œ (νšŒμƒ‰) -- 생성 (프라이머리 색상) -``` - -**μœ νš¨μ„± 검사:** - -- λŒ€μƒ λ…ΈνŠΈ ν•„μˆ˜ 선택 -- 자기 μžμ‹ μœΌλ‘œμ˜ 링크 λ°©μ§€ -- 쀑볡 링크 κ²½κ³  (같은 μ˜μ—­μ— 이미 링크 쑴재) - -### 4. 이벀트 처리 κ΅¬ν˜„ - -#### 4.1 GestureDetector μ„€μ • - -**파일:** `lib/features/canvas/widgets/note_page_view_item.dart` - -**μˆ˜μ • λ‚΄μš©:** - -```dart -// Scribble μœ„μ ―μ„ GestureDetector둜 감싸기 -GestureDetector( - onPanStart: (details) { - if (notifier.toolMode == ToolMode.linker) { - notifier.startLinkSelection(details.localPosition); - } - }, - onPanUpdate: (details) { - if (notifier.toolMode == ToolMode.linker) { - notifier.updateLinkSelection(details.localPosition); - } - }, - onPanEnd: (details) { - if (notifier.toolMode == ToolMode.linker) { - notifier.completeLinkSelection(); - _showLinkCreationDialog(); - } - }, - child: Stack([ - Scribble(...), - if (notifier.isLinkSelecting) - LinkSelectionOverlay( - start: notifier.linkSelectionStart, - end: notifier.linkSelectionEnd, - ), - ]), -) -``` - -### 5. 링크 μ €μž₯ 둜직 - -#### 5.1 링크 생성 ν”Œλ‘œμš° - -**μˆœμ„œ:** - -1. λ‹€μ΄μ–Όλ‘œκ·Έμ—μ„œ "생성" 클릭 -2. LinkModel μΈμŠ€ν„΄μŠ€ 생성 (UUID 생성) -3. ν˜„μž¬ νŽ˜μ΄μ§€μ˜ links λ¦¬μŠ€νŠΈμ— μΆ”κ°€ -4. NotePageModel의 변경사항 μ €μž₯ -5. UI μ—…λ°μ΄νŠΈ (notifyListeners) - -#### 5.2 μ €μž₯ ν˜•μ‹ - -**JSON ꡬ쑰:** - -```json -{ - "noteId": "note1", - "pageId": "page1", - "pageNumber": 1, - "jsonData": "...", // κΈ°μ‘΄ Scribble 데이터 - "links": [ - { - "id": "link-uuid-1", - "sourceNoteId": "note1", - "sourcePageId": "page1", - "targetNoteId": "note2", - "targetPageId": null, - "boundingBox": { - "left": 100, - "top": 200, - "width": 150, - "height": 50 - }, - "createdAt": "2024-01-01T00:00:00Z", - "updatedAt": "2024-01-01T00:00:00Z" - } - ] -} -``` - -### 6. μ‹œκ°μ  ν”Όλ“œλ°± - -#### 6.1 링컀 도ꡬ 선택 μ‹œ - -- μ»€μ„œ λ³€κ²½: crosshair -- νˆ΄λ°”μ— 링컀 ν™œμ„± ν‘œμ‹œ -- μƒνƒœλ°”μ— "μ˜μ—­μ„ λ“œλž˜κ·Έν•˜μ—¬ 링크 생성" 힌트 - -#### 6.2 선택 쀑 - -- μ‹€μ‹œκ°„ μ§μ‚¬κ°ν˜• ν‘œμ‹œ -- 크기 정보 ν‘œμ‹œ (선택사항) -- ESC ν‚€λ‘œ μ·¨μ†Œ κ°€λŠ₯ - ---- - -## λ‹€μŒ 단계 (Milestone 2 예고) - -### 링크 μ‹œκ°ν™” - -- LinkVisualizationLayer μœ„μ ― κ΅¬ν˜„ -- μ €μž₯된 링크λ₯Ό μΊ”λ²„μŠ€μ— ν‘œμ‹œ -- ν˜Έλ²„/ν„°μΉ˜ 효과 - -### 링크 λ„€λΉ„κ²Œμ΄μ…˜ - -- 링크 클릭 감지 -- λŒ€μƒ λ…ΈνŠΈ/νŽ˜μ΄μ§€λ‘œ 이동 -- 이동 νžˆμŠ€ν† λ¦¬ 관리 - -이 λͺ…μ„ΈλŒ€λ‘œ κ΅¬ν˜„ν•˜λ©΄ Milestone 1이 μ™„μ„±λ©λ‹ˆλ‹€. diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f7be6378..0a9043c0 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -244,6 +244,7 @@ 3399D490228B24CF009A79C7 /* ShellScript */, 1A8F3E0CD0A62ACF585609F6 /* [CP] Embed Pods Frameworks */, 64B61378FD70EE14FAF25A96 /* [CP] Copy Pods Resources */, + DC2A2DA5EE1527DE36C37F10 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, ); buildRules = ( ); @@ -421,6 +422,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DC2A2DA5EE1527DE36C37F10 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=macos --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; + }; FE2706754515A732F4C9ECAB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/pubspec.yaml b/pubspec.yaml index 37f53205..98cac932 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # - λ§ˆμ΄λ„ˆ(x.1.x): ν˜Έν™˜μ„± μœ μ§€ν•˜λ©΄μ„œ 의미 μžˆλŠ” κΈ°λŠ₯ μΆ”κ°€λ‚˜ UX κ°œμ„ μ΄ μžˆμ„ λ•Œ. κΈ°μ‘΄ μ‚¬μš© 흐름은 μœ μ§€λ˜μ§€λ§Œ β€œμƒˆ κΈ°λŠ₯”이라 말할 수 μžˆλŠ” μˆ˜μ€€ # - 패치(x.x.1): 버그 μˆ˜μ •, μ•ˆμ •ν™”, μ„±λŠ₯ μ‘°μ •μ²˜λŸΌ κΈ°μ‘΄ κΈ°λŠ₯을 κΉ¨μ§€ μ•Šκ³  닀듬을 λ•Œ. ν•«ν”½μŠ€ # - λΉŒλ“œ 번호(+n): 같은 μ•± 버전을 μ—¬λŸ¬ 번 μŠ€ν† μ–΄μ— μ œμΆœν•  λ•Œ -version: 1.0.0+1 +version: 1.0.2+3 environment: sdk: ^3.8.1 @@ -109,7 +109,12 @@ flutter: fonts: - family: Pretendard fonts: - - asset: assets/fonts/PretendardVariable.ttf + - asset: fonts/Pretendard-Regular.otf + weight: 400 + - asset: fonts/Pretendard-SemiBold.otf + weight: 600 + - asset: fonts/Pretendard-Bold.otf + weight: 700 - family: Play fonts: - asset: fonts/Play-Regular.ttf diff --git a/test_page_controller.dart b/test_page_controller.dart deleted file mode 100644 index 944c27a7..00000000 --- a/test_page_controller.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'lib/features/canvas/providers/note_editor_provider.dart'; -import 'lib/features/notes/models/note_page_model.dart'; -import 'lib/features/notes/widgets/draggable_page_thumbnail.dart'; - -void main() { - runApp( - const ProviderScope( - child: MaterialApp( - home: PageControllerTestScreen(), - ), - ), - ); -} - -class PageControllerTestScreen extends ConsumerWidget { - const PageControllerTestScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - const testNoteId = 'test-note-id'; - - // μƒˆλ‘œ μΆ”κ°€ν•œ νŽ˜μ΄μ§€ 컨트둀러 μƒνƒœ 확인 - final pageControllerState = ref.watch( - pageControllerNotifierProvider(testNoteId), - ); - - return Scaffold( - appBar: AppBar(title: const Text('νŽ˜μ΄μ§€ 컨트둀러 ν…ŒμŠ€νŠΈ')), - body: Column( - children: [ - Text('λ‘œλ”© μƒνƒœ: ${pageControllerState.isLoading}'), - Text('였λ₯˜ λ©”μ‹œμ§€: ${pageControllerState.errorMessage ?? "μ—†μŒ"}'), - Text('ν˜„μž¬ μž‘μ—…: ${pageControllerState.currentOperation ?? "μ—†μŒ"}'), - Text('λ“œλž˜κ·Έ μƒνƒœ: ${pageControllerState.dragDropState.isDragging}'), - Text('썸넀일 μΊμ‹œ 수: ${pageControllerState.thumbnailCache.length}'), - - ElevatedButton( - onPressed: () { - ref - .read(pageControllerNotifierProvider(testNoteId).notifier) - .setLoading(true, operation: 'ν…ŒμŠ€νŠΈ λ‘œλ”©'); - }, - child: const Text('λ‘œλ”© μƒνƒœ ν…ŒμŠ€νŠΈ'), - ), - - ElevatedButton( - onPressed: () { - ref - .read(pageControllerNotifierProvider(testNoteId).notifier) - .setError('ν…ŒμŠ€νŠΈ 였λ₯˜ λ©”μ‹œμ§€'); - }, - child: const Text('였λ₯˜ μƒνƒœ ν…ŒμŠ€νŠΈ'), - ), - - ElevatedButton( - onPressed: () { - ref - .read(pageControllerNotifierProvider(testNoteId).notifier) - .startDrag('test-page-id', 0, [0, 1, 2]); - }, - child: const Text('λ“œλž˜κ·Έ μ‹œμž‘ ν…ŒμŠ€νŠΈ'), - ), - - const SizedBox(height: 20), - const Text('DraggablePageThumbnail 데λͺ¨:'), - const SizedBox(height: 10), - - // λ“œλž˜κ·Έ μ•€ λ“œλ‘­ 데λͺ¨ μ˜μ—­ - Container( - height: 200, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - const Text('λ“œλž˜κ·Έν•΄μ„œ μˆœμ„œλ₯Ό λ°”κΏ”λ³΄μ„Έμš”:'), - const SizedBox(height: 10), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // λ“œλ‘­ νƒ€κ²Ÿ 1 - DragTarget( - onAcceptWithDetails: (details) { - print('νŽ˜μ΄μ§€ ${details.data.pageNumber}이 μœ„μΉ˜ 1에 λ“œλ‘­λ¨'); - }, - builder: (context, candidateData, rejectedData) { - return Container( - width: 120, - height: 120, - decoration: BoxDecoration( - border: Border.all( - color: candidateData.isNotEmpty - ? Colors.blue - : Colors.grey[300]!, - width: 2, - ), - borderRadius: BorderRadius.circular(8), - ), - child: candidateData.isNotEmpty - ? const Center(child: Text('여기에 λ“œλ‘­')) - : DraggablePageThumbnail( - page: NotePageModel( - noteId: testNoteId, - pageId: 'demo-page-1', - pageNumber: 1, - jsonData: '{"strokes":[]}', - backgroundType: PageBackgroundType.blank, - ), - onTap: () => print('νŽ˜μ΄μ§€ 1 탭됨'), - onDelete: () => print('νŽ˜μ΄μ§€ 1 μ‚­μ œλ¨'), - onDragStart: () => print('νŽ˜μ΄μ§€ 1 λ“œλž˜κ·Έ μ‹œμž‘'), - onDragEnd: () => print('νŽ˜μ΄μ§€ 1 λ“œλž˜κ·Έ μ’…λ£Œ'), - ), - ); - }, - ), - - // λ“œλ‘­ νƒ€κ²Ÿ 2 - DragTarget( - onAcceptWithDetails: (details) { - print('νŽ˜μ΄μ§€ ${details.data.pageNumber}이 μœ„μΉ˜ 2에 λ“œλ‘­λ¨'); - }, - builder: (context, candidateData, rejectedData) { - return Container( - width: 120, - height: 120, - decoration: BoxDecoration( - border: Border.all( - color: candidateData.isNotEmpty - ? Colors.blue - : Colors.grey[300]!, - width: 2, - ), - borderRadius: BorderRadius.circular(8), - ), - child: candidateData.isNotEmpty - ? const Center(child: Text('여기에 λ“œλ‘­')) - : DraggablePageThumbnail( - page: NotePageModel( - noteId: testNoteId, - pageId: 'demo-page-2', - pageNumber: 2, - jsonData: '{"strokes":[]}', - backgroundType: PageBackgroundType.pdf, - ), - onTap: () => print('νŽ˜μ΄μ§€ 2 탭됨'), - onDelete: () => print('νŽ˜μ΄μ§€ 2 μ‚­μ œλ¨'), - onDragStart: () => print('νŽ˜μ΄μ§€ 2 λ“œλž˜κ·Έ μ‹œμž‘'), - onDragEnd: () => print('νŽ˜μ΄μ§€ 2 λ“œλž˜κ·Έ μ’…λ£Œ'), - ), - ); - }, - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } -}