회원목록

This commit is contained in:
2025-08-01 16:45:26 +09:00
parent a996def67a
commit ed8c7eacba
191 changed files with 17491 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "fcf2c11572af6f390246c056bc905eca609533a0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: android
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: ios
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: linux
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: macos
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: web
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: windows
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# mobile
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.mobile"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.mobile"
// 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
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Lanebow"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.mobile
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+21
View File
@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
+25
View File
@@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+160
View File
@@ -0,0 +1,160 @@
# 볼링 매니저 구독 및 인앱 결제 통합 문서
## 개요
볼링 매니저 앱은 구독 기반 비즈니스 모델을 채택하여 다양한 기능을 제공합니다. 이 문서는 Flutter 모바일 앱에서 구독 및 인앱 결제 기능의 구현과 통합에 대한 상세 정보를 제공합니다.
## 아키텍처 개요
구독 및 인앱 결제 시스템은 다음 주요 구성 요소로 이루어져 있습니다:
1. **모델 (Models)**
- `Subscription`: 사용자의 구독 정보를 나타냅니다.
- `SubscriptionPlan`: 사용 가능한 구독 플랜의 세부 정보를 포함합니다.
2. **서비스 (Services)**
- `SubscriptionService`: 구독 상태 관리 및 백엔드 API와의 통신을 담당합니다.
- `InAppPurchaseService`: 플랫폼별 인앱 결제 처리를 담당합니다.
3. **백엔드 API**
- 구독 검증, 상태 관리, 영수증 검증 등의 서버 측 로직을 제공합니다.
## 데이터 흐름
### 구독 구매 프로세스
1. 사용자가 구독 구매 버튼을 클릭합니다.
2. `SubscriptionService.createSubscription()`이 호출됩니다.
3. `InAppPurchaseService.purchaseSubscription()`이 호출되어 플랫폼별 결제 프로세스를 시작합니다.
4. 결제가 완료되면 `InAppPurchaseService``purchaseStream`이 업데이트됩니다.
5. 결제 상태가 `PurchaseStatus.purchased`로 변경되면 `_verifyAndSavePurchase()` 메서드가 호출됩니다.
6. `_verifyAndSavePurchase()`는 플랫폼별 영수증 데이터를 추출하고 서버에 전송하여 검증합니다.
7. 서버는 영수증을 검증하고 구독 정보를 데이터베이스에 저장합니다.
8. 검증이 성공하면 `SubscriptionService`는 서버에서 최신 구독 정보를 가져와 로컬 상태를 업데이트합니다.
### 구독 상태 확인 및 기능 접근 제어
1. 앱이 시작되면 `SubscriptionService.loadCurrentSubscription()`이 호출되어 현재 구독 상태를 로드합니다.
2. 사용자가 특정 기능에 접근하려고 할 때 `SubscriptionService.canAccessFeature()`가 호출됩니다.
3. 현재 구독 상태에 따라 기능 접근 권한이 결정됩니다.
4. 접근 권한이 없는 경우 업그레이드 안내 대화상자가 표시됩니다.
## 주요 클래스 및 메서드
### SubscriptionService
```dart
class SubscriptionService extends ChangeNotifier {
// 주요 속성
Subscription? _currentSubscription;
List<SubscriptionPlan> _availablePlans;
InAppPurchaseService _purchaseService;
// 주요 메서드
Future<void> loadCurrentSubscription();
Future<bool> createSubscription(SubscriptionPlanType planType, bool isYearly);
Future<bool> restoreSubscriptions();
Future<bool> changePlan(SubscriptionPlanType newPlanType, bool isYearly);
Future<bool> toggleAutoRenew();
bool canAccessFeature(SubscriptionFeature feature);
Future<bool> checkFeatureAccess(SubscriptionFeature feature, BuildContext context);
int getMaxMembersAllowed();
int getMaxEventsAllowed();
}
```
### InAppPurchaseService
```dart
class InAppPurchaseService extends ChangeNotifier {
// 주요 속성
List<ProductDetails> _products;
List<PurchaseDetails> _purchases;
bool _purchaseVerified;
Map<String, dynamic>? _verifiedPurchase;
// 주요 메서드
Future<void> initialize();
Future<bool> purchaseSubscription(SubscriptionPlanType planType);
Future<bool> restorePurchases();
Future<bool> _verifyAndSavePurchase(PurchaseDetails purchaseDetails);
}
```
## 구독 플랜 및 기능
볼링 매니저는 다음과 같은 구독 플랜을 제공합니다:
1. **무료 (Free)**
- 기본 볼링 점수 추적
- 최대 10명의 회원 관리
- 최대 5개의 이벤트 관리
2. **기본 (Basic)**
- 모든 무료 기능
- 최대 30명의 회원 관리
- 최대 10개의 이벤트 관리
- 기본 통계 분석
3. **프리미엄 (Premium)**
- 모든 기본 기능
- 최대 100명의 회원 관리
- 최대 50개의 이벤트 관리
- 고급 통계 분석
- 커스터마이징 옵션
4. **프로 (Pro)**
- 모든 프리미엄 기능
- 최대 500명의 회원 관리
- 최대 200개의 이벤트 관리
- 우선 지원
- 모든 고급 기능 접근
## 영수증 검증 프로세스
1. **iOS 영수증 검증**
- App Store 영수증 데이터 추출
- 서버에 영수증 데이터 전송
- 서버는 Apple 서버에 영수증 검증 요청
- 검증 결과에 따라 구독 상태 업데이트
2. **Android 영수증 검증**
- Google Play 영수증 데이터(JSON) 추출
- 서버에 영수증 데이터 전송
- 서버는 Google Play API를 통해 영수증 검증
- 검증 결과에 따라 구독 상태 업데이트
## 오류 처리
- 결제 프로세스 중 발생하는 오류는 `_error` 속성에 저장되고 UI에 표시됩니다.
- 네트워크 오류, 서버 오류, 검증 오류 등 다양한 오류 상황에 대한 처리가 구현되어 있습니다.
- 오류 발생 시 사용자에게 적절한 피드백을 제공하고 필요한 경우 재시도 옵션을 제공합니다.
## 테스트 및 QA
구독 및 인앱 결제 기능을 테스트하려면:
1. **샌드박스 환경 설정**
- iOS: App Store Connect에서 샌드박스 테스터 계정 설정
- Android: Google Play Console에서 테스트 트랙 설정
2. **테스트 시나리오**
- 구독 구매
- 구독 복원
- 플랜 변경
- 자동 갱신 설정 변경
- 구독 만료 처리
- 다양한 오류 상황 테스트
## 백엔드 API 엔드포인트
- `/subscriptions/current`: 현재 구독 정보 조회
- `/subscriptions/plans`: 사용 가능한 구독 플랜 목록 조회
- `/subscriptions/subscribe`: 새 구독 생성
- `/subscriptions/verify-receipt`: 인앱 결제 영수증 검증
- `/subscriptions/change-with-features`: 구독 플랜 변경
- `/subscriptions/autorenew`: 자동 갱신 설정 변경
## 결론
볼링 매니저의 구독 및 인앱 결제 시스템은 Flutter의 `in_app_purchase` 패키지와 커스텀 백엔드 API를 통합하여 안전하고 사용자 친화적인 결제 경험을 제공합니다. 이 문서는 시스템의 주요 구성 요소와 데이터 흐름을 설명하여 개발자들이 시스템을 이해하고 유지보수할 수 있도록 돕습니다.
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
+43
View File
@@ -0,0 +1,43 @@
PODS:
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
COCOAPODS: 1.16.2
+728
View File
@@ -0,0 +1,728 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A2B54DBFF2070F0147141B2E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AB1FA8761A510A1AB0E22A6 /* Pods_Runner.framework */; };
BED0EB1D643B5C8960491578 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D87907F099B026F68EDEAB39 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2AB1FA8761A510A1AB0E22A6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
355A374A8D2A72F499E2A232 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3F6039EC571A40CA5A3421CE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
53D946C2A42D4F1B1B4DBE9C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8154F9AE74B580244D51C084 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BCE312C5D51857E9B078DC0B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
D87907F099B026F68EDEAB39 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F9A79F7167432F25A9176E3F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A2B54DBFF2070F0147141B2E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A3C195AAAC5367AC911F04A6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
BED0EB1D643B5C8960491578 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1EEF9CC0167CE714191E3BC4 /* Frameworks */ = {
isa = PBXGroup;
children = (
2AB1FA8761A510A1AB0E22A6 /* Pods_Runner.framework */,
D87907F099B026F68EDEAB39 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
F8390BE3267FB56413CCA22F /* Pods */,
1EEF9CC0167CE714191E3BC4 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
F8390BE3267FB56413CCA22F /* Pods */ = {
isa = PBXGroup;
children = (
8154F9AE74B580244D51C084 /* Pods-Runner.debug.xcconfig */,
355A374A8D2A72F499E2A232 /* Pods-Runner.release.xcconfig */,
F9A79F7167432F25A9176E3F /* Pods-Runner.profile.xcconfig */,
53D946C2A42D4F1B1B4DBE9C /* Pods-RunnerTests.debug.xcconfig */,
3F6039EC571A40CA5A3421CE /* Pods-RunnerTests.release.xcconfig */,
BCE312C5D51857E9B078DC0B /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
8CE422EB9696F2D9AC24A2BF /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
A3C195AAAC5367AC911F04A6 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
F39579041B6959A7AE1A2D5D /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
8A77A54775D4EC0D63AC9681 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
8A77A54775D4EC0D63AC9681 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
8CE422EB9696F2D9AC24A2BF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
F39579041B6959A7AE1A2D5D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 53D946C2A42D4F1B1B4DBE9C /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3F6039EC571A40CA5A3421CE /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BCE312C5D51857E9B078DC0B /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Lanebow</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>lanebow</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+38
View File
@@ -0,0 +1,38 @@
class ApiConfig {
// 백엔드 API 기본 URL
static const String baseUrl = 'https://lanebow.com/api';
// API 엔드포인트
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String forgotPassword = '/auth/forgot-password';
static const String profile = '/users/profile';
// 클럽 관련 엔드포인트
static const String clubs = '/club';
static const String members = '/members';
static const String events = '/events';
static const String scores = '/scores';
// 구독 관련 엔드포인트
static const String subscriptions = '/subscriptions';
static const String subscriptionPlans = '/subscriptions/plans';
static const String currentSubscription = '/subscriptions/current';
static const String subscribeClub = '/subscriptions/subscribe';
static const String subscriptionPayments = '/subscriptions/payments';
static const String changePlan = '/subscriptions/change-with-features';
// 결제 관련 엔드포인트
static const String paymentRequest = '/subscriptions/payment/request';
static const String paymentSuccess = '/subscriptions/payment/success';
static const String paymentFail = '/subscriptions/payment/fail';
// 인앱 결제 검증 엔드포인트
static const String verifyReceipt = '/subscriptions/verify-receipt';
// 토큰 저장 키
static const String tokenKey = 'auth_token';
static const String userIdKey = 'user_id';
static const String userRoleKey = 'user_role';
static const String clubIdKey = 'club_id';
}
+344
View File
@@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'services/auth_service.dart';
import 'services/club_service.dart';
import 'services/member_service.dart';
import 'services/event_service.dart';
import 'services/score_service.dart';
import 'screens/auth/login_screen.dart';
import 'screens/auth/profile_screen.dart';
import 'screens/club/dashboard_screen.dart';
import 'screens/club/members_screen.dart';
import 'screens/club/events_screen.dart';
import 'screens/club/club_settings_screen.dart';
import 'screens/score/club_statistics_screen.dart';
// 전역 네비게이터 키 (인증 오류 처리용)
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthService()),
ChangeNotifierProvider(create: (_) => ClubService()),
ChangeNotifierProvider(create: (_) => MemberService()),
ChangeNotifierProvider(create: (_) => EventService()),
ChangeNotifierProvider(create: (_) => ScoreService()),
],
child: Consumer<AuthService>(
builder: (context, authService, _) {
// 앱 시작 시 인증 서비스 초기화
Future.delayed(Duration.zero, () {
if (!authService.isInitialized) {
authService.initialize();
}
});
return MaterialApp(
navigatorKey: navigatorKey, // 인증 오류 처리를 위한 전역 네비게이터 키
title: '볼링 매니저',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
// 로케일 설정 추가
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('ko', 'KR'),
Locale('en', 'US'),
],
locale: const Locale('ko', 'KR'),
home: authService.isAuthenticated ? const HomeScreen() : const LoginScreen(),
routes: {
'/login': (context) => const LoginScreen(),
'/profile': (context) => const ProfileScreen(),
ClubSettingsScreen.routeName: (context) => const ClubSettingsScreen(),
},
);
},
),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
bool _isInit = false;
static final List<Widget> _widgetOptions = <Widget>[
const DashboardScreen(),
const MembersScreen(),
const EventsScreen(),
const ClubStatisticsScreen(),
const ProfileScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
// 클럽 선택 다이얼로그 표시
Future<void> _showClubSelectionDialog(BuildContext context) async {
final clubService = Provider.of<ClubService>(context, listen: false);
final authService = Provider.of<AuthService>(context, listen: false);
// 사용자의 모든 클럽 가져오기
if (clubService.clubs.isEmpty) {
try {
await clubService.fetchUserClubs();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('클럽 목록을 불러오는데 실패했습니다: $e')),
);
return;
}
}
if (clubService.clubs.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('소속된 클럽이 없습니다')),
);
return;
}
if (!context.mounted) return;
// 다이얼로그 표시
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('클럽 선택'),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: clubService.clubs.length,
itemBuilder: (context, index) {
final club = clubService.clubs[index];
final isSelected = clubService.currentClub?.id == club.id;
return ListTile(
title: Text(club.name),
subtitle: Text(club.description ?? ''),
trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
onTap: () async {
Navigator.of(context).pop();
if (!isSelected) {
try {
// 백엔드 세션에 clubId 저장하고 클럽 정보 가져오기
await clubService.selectClub(club.id);
// 관련 서비스 초기화
final memberService = Provider.of<MemberService>(context, listen: false);
final eventService = Provider.of<EventService>(context, listen: false);
final scoreService = Provider.of<ScoreService>(context, listen: false);
memberService.initialize(authService.token!);
eventService.initialize(authService.token!);
scoreService.initialize(authService.token!);
// 데이터 다시 로드
await Future.wait([
memberService.fetchClubMembers(),
eventService.fetchClubEvents(),
]);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${club.name} 클럽으로 변경되었습니다')),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('클럽 변경 중 오류가 발생했습니다: $e')),
);
}
}
}
},
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
],
);
},
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInit) {
_initializeServices();
_isInit = true;
}
}
Future<void> _initializeServices() async {
try {
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
final scoreService = Provider.of<ScoreService>(context, listen: false);
if (authService.isAuthenticated && authService.token != null) {
// 클럽 서비스 초기화
await clubService.initialize(authService.token!);
// 클럽이 선택된 경우 점수 서비스 초기화
if (clubService.currentClub != null) {
scoreService.initialize(authService.token!);
} else {
// 현재 선택된 클럽이 없는 경우
// 사용자의 클럽 목록 가져오기
await clubService.fetchUserClubs();
// 클럽이 하나만 있으면 자동으로 선택
if (clubService.clubs.length == 1) {
try {
await clubService.selectClub(clubService.clubs[0].id);
scoreService.initialize(authService.token!);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${clubService.clubs[0].name} 클럽이 자동으로 선택되었습니다')),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('클럽 자동 선택 중 오류가 발생했습니다: $e')),
);
}
}
} else if (clubService.clubs.isNotEmpty) {
// 클럽이 여러 개 있으면 선택 다이얼로그 표시
Future.delayed(Duration.zero, () {
if (context.mounted) {
_showClubSelectionDialog(context);
}
});
} else {
// 클럽이 없는 경우 메시지 표시
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('소속된 클럽이 없습니다. 클럽을 생성하거나 초대를 요청하세요.')),
);
}
}
}
}
} catch (e) {
// 오류 처리
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('서비스 초기화 중 오류가 발생했습니다: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Consumer<ClubService>(
builder: (context, clubService, child) {
return InkWell(
onTap: () => _showClubSelectionDialog(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
clubService.currentClub?.name ?? '볼링 매니저',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_drop_down, size: 20),
],
),
);
},
),
actions: [
IconButton(
icon: const Icon(Icons.notifications),
onPressed: () {
// 알림 기능 (추후 구현)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('알림 기능은 아직 개발 중입니다')),
);
},
),
],
),
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.dashboard),
label: '대시보드',
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
label: '회원',
),
BottomNavigationBarItem(
icon: Icon(Icons.event),
label: '이벤트',
),
BottomNavigationBarItem(
icon: Icon(Icons.score),
label: '통계',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '프로필',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.blue,
onTap: _onItemTapped,
),
);
}
}
+265
View File
@@ -0,0 +1,265 @@
/// 사용자 역할 정의
enum UserRole {
member, // 일반 회원
manager, // 매니저 (일부 관리 권한)
admin, // 관리자 (대부분의 관리 권한)
owner // 소유자 (모든 권한)
}
/// 관리 권한 정의
class AdminPermission {
final bool canManageMembers; // 회원 관리 권한
final bool canManageEvents; // 이벤트 관리 권한
final bool canManageScores; // 점수 관리 권한
final bool canManageSettings; // 설정 관리 권한
final bool canManageSubscription; // 구독 관리 권한
final bool canManageRoles; // 역할 관리 권한
final bool canViewFinancials; // 재정 정보 조회 권한
final bool canExportData; // 데이터 내보내기 권한
const AdminPermission({
this.canManageMembers = false,
this.canManageEvents = false,
this.canManageScores = false,
this.canManageSettings = false,
this.canManageSubscription = false,
this.canManageRoles = false,
this.canViewFinancials = false,
this.canExportData = false,
});
/// 역할에 따른 기본 권한 생성
factory AdminPermission.fromRole(UserRole role) {
switch (role) {
case UserRole.member:
return const AdminPermission();
case UserRole.manager:
return const AdminPermission(
canManageMembers: true,
canManageEvents: true,
canManageScores: true,
canExportData: true,
);
case UserRole.admin:
return const AdminPermission(
canManageMembers: true,
canManageEvents: true,
canManageScores: true,
canManageSettings: true,
canViewFinancials: true,
canExportData: true,
);
case UserRole.owner:
return const AdminPermission(
canManageMembers: true,
canManageEvents: true,
canManageScores: true,
canManageSettings: true,
canManageSubscription: true,
canManageRoles: true,
canViewFinancials: true,
canExportData: true,
);
}
}
/// JSON에서 권한 객체 생성
factory AdminPermission.fromJson(Map<String, dynamic> json) {
return AdminPermission(
canManageMembers: json['canManageMembers'] ?? false,
canManageEvents: json['canManageEvents'] ?? false,
canManageScores: json['canManageScores'] ?? false,
canManageSettings: json['canManageSettings'] ?? false,
canManageSubscription: json['canManageSubscription'] ?? false,
canManageRoles: json['canManageRoles'] ?? false,
canViewFinancials: json['canViewFinancials'] ?? false,
canExportData: json['canExportData'] ?? false,
);
}
/// 권한 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'canManageMembers': canManageMembers,
'canManageEvents': canManageEvents,
'canManageScores': canManageScores,
'canManageSettings': canManageSettings,
'canManageSubscription': canManageSubscription,
'canManageRoles': canManageRoles,
'canViewFinancials': canViewFinancials,
'canExportData': canExportData,
};
}
}
/// 클럽 회원 역할 정보
class MemberRole {
final String userId;
final String memberId;
final String clubId;
final UserRole role;
final AdminPermission permissions;
final DateTime assignedAt;
final String? assignedBy;
MemberRole({
required this.userId,
required this.memberId,
required this.clubId,
required this.role,
required this.permissions,
required this.assignedAt,
this.assignedBy,
});
/// JSON에서 회원 역할 객체 생성
factory MemberRole.fromJson(Map<String, dynamic> json) {
return MemberRole(
userId: json['userId'],
memberId: json['memberId'],
clubId: json['clubId'],
role: UserRole.values.firstWhere(
(e) => e.toString().split('.').last == json['role'],
orElse: () => UserRole.member,
),
permissions: AdminPermission.fromJson(json['permissions'] ?? {}),
assignedAt: DateTime.parse(json['assignedAt']),
assignedBy: json['assignedBy'],
);
}
/// 회원 역할 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'userId': userId,
'memberId': memberId,
'clubId': clubId,
'role': role.toString().split('.').last,
'permissions': permissions.toJson(),
'assignedAt': assignedAt.toIso8601String(),
'assignedBy': assignedBy,
};
}
/// 역할 이름 (한글)
String get roleName {
switch (role) {
case UserRole.member:
return '일반 회원';
case UserRole.manager:
return '매니저';
case UserRole.admin:
return '관리자';
case UserRole.owner:
return '소유자';
}
}
}
/// 클럽 설정 모델
class ClubSettings {
final String clubId;
final String clubName;
final String? logoUrl;
final String? bannerUrl;
final String? description;
final String? contactEmail;
final String? contactPhone;
final String? address;
final String? website;
final Map<String, dynamic>? customFields;
final Map<String, dynamic>? notificationSettings;
final Map<String, dynamic>? privacySettings;
final DateTime createdAt;
final DateTime updatedAt;
ClubSettings({
required this.clubId,
required this.clubName,
this.logoUrl,
this.bannerUrl,
this.description,
this.contactEmail,
this.contactPhone,
this.address,
this.website,
this.customFields,
this.notificationSettings,
this.privacySettings,
required this.createdAt,
required this.updatedAt,
});
/// JSON에서 클럽 설정 객체 생성
factory ClubSettings.fromJson(Map<String, dynamic> json) {
return ClubSettings(
clubId: json['clubId'],
clubName: json['clubName'],
logoUrl: json['logoUrl'],
bannerUrl: json['bannerUrl'],
description: json['description'],
contactEmail: json['contactEmail'],
contactPhone: json['contactPhone'],
address: json['address'],
website: json['website'],
customFields: json['customFields'],
notificationSettings: json['notificationSettings'],
privacySettings: json['privacySettings'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
);
}
/// 클럽 설정 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'clubId': clubId,
'clubName': clubName,
'logoUrl': logoUrl,
'bannerUrl': bannerUrl,
'description': description,
'contactEmail': contactEmail,
'contactPhone': contactPhone,
'address': address,
'website': website,
'customFields': customFields,
'notificationSettings': notificationSettings,
'privacySettings': privacySettings,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
/// 복사본 생성 (일부 필드 업데이트)
ClubSettings copyWith({
String? clubName,
String? logoUrl,
String? bannerUrl,
String? description,
String? contactEmail,
String? contactPhone,
String? address,
String? website,
Map<String, dynamic>? customFields,
Map<String, dynamic>? notificationSettings,
Map<String, dynamic>? privacySettings,
}) {
return ClubSettings(
clubId: clubId,
clubName: clubName ?? this.clubName,
logoUrl: logoUrl ?? this.logoUrl,
bannerUrl: bannerUrl ?? this.bannerUrl,
description: description ?? this.description,
contactEmail: contactEmail ?? this.contactEmail,
contactPhone: contactPhone ?? this.contactPhone,
address: address ?? this.address,
website: website ?? this.website,
customFields: customFields ?? this.customFields,
notificationSettings: notificationSettings ?? this.notificationSettings,
privacySettings: privacySettings ?? this.privacySettings,
createdAt: createdAt,
updatedAt: DateTime.now(),
);
}
}
+77
View File
@@ -0,0 +1,77 @@
class Club {
final String id;
final String name;
final String? description;
final String? logo;
final String? address;
final String? location;
final String? phone;
final String? email;
final String? website;
final int? memberCount;
final String? ownerId;
final int? femaleHandicap;
final String? averageCalculationPeriod;
final DateTime? createdAt;
final DateTime? updatedAt;
Club({
required this.id,
required this.name,
this.description,
this.logo,
this.address,
this.location,
this.phone,
this.email,
this.website,
this.memberCount,
this.ownerId,
this.femaleHandicap,
this.averageCalculationPeriod,
this.createdAt,
this.updatedAt,
});
// JSON 데이터로부터 Club 객체 생성
factory Club.fromJson(Map<String, dynamic> json) {
return Club(
id: json['id'] != null ? json['id'].toString() : '',
name: json['name'] ?? '',
description: json['description'],
logo: json['logo'],
address: json['address'],
location: json['location'],
phone: json['phone'] ?? json['contact'], // contact 필드도 확인
email: json['email'],
website: json['website'],
memberCount: json['memberCount'],
ownerId: json['ownerId']?.toString(),
femaleHandicap: json['femaleHandicap'] != null ? int.tryParse(json['femaleHandicap'].toString()) : null,
averageCalculationPeriod: json['averageCalculationPeriod'],
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
);
}
// Club 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'logo': logo,
'address': address,
'location': location,
'phone': phone,
'email': email,
'website': website,
'memberCount': memberCount,
'ownerId': ownerId,
'femaleHandicap': femaleHandicap,
'averageCalculationPeriod': averageCalculationPeriod,
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}
}
+73
View File
@@ -0,0 +1,73 @@
class Event {
final String id;
final String clubId;
final String title;
final String? description;
final DateTime startDate;
final DateTime? endDate;
final String? location;
final String? type;
final int? maxParticipants;
final int? currentParticipants;
final bool isActive;
final String? createdBy;
final DateTime? createdAt;
final DateTime? updatedAt;
Event({
required this.id,
required this.clubId,
required this.title,
this.description,
required this.startDate,
this.endDate,
this.location,
this.type,
this.maxParticipants,
this.currentParticipants,
required this.isActive,
this.createdBy,
this.createdAt,
this.updatedAt,
});
// JSON 데이터로부터 Event 객체 생성
factory Event.fromJson(Map<String, dynamic> json) {
return Event(
id: json['id']?.toString() ?? '',
clubId: json['clubId']?.toString() ?? '',
title: json['title'] ?? '',
description: json['description']?.toString(),
startDate: json['startDate'] != null ? DateTime.parse(json['startDate'].toString()) : DateTime.now(),
endDate: json['endDate'] != null ? DateTime.parse(json['endDate'].toString()) : null,
location: json['location']?.toString(),
type: json['type']?.toString(),
maxParticipants: json['maxParticipants'],
currentParticipants: json['currentParticipants'],
isActive: json['isActive'] ?? true,
createdBy: json['createdBy']?.toString(),
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'].toString()) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'].toString()) : null,
);
}
// Event 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'clubId': clubId,
'title': title,
'description': description,
'startDate': startDate.toIso8601String(),
'endDate': endDate?.toIso8601String(),
'location': location,
'type': type,
'maxParticipants': maxParticipants,
'currentParticipants': currentParticipants,
'isActive': isActive,
'createdBy': createdBy,
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}
}
+126
View File
@@ -0,0 +1,126 @@
class Member {
final String id;
final String userId;
final String clubId;
final String name;
final String email;
final String? role;
final String? profileImage;
final String? phone;
final String? address;
final DateTime? joinDate;
final bool isActive;
final String? gender;
final String? memberType;
final int? handicap;
final String? status;
final DateTime? createdAt;
final DateTime? updatedAt;
Member({
required this.id,
required this.userId,
required this.clubId,
required this.name,
required this.email,
this.role,
this.profileImage,
this.phone,
this.address,
this.joinDate,
required this.isActive,
this.gender,
this.memberType,
this.handicap,
this.status,
this.createdAt,
this.updatedAt,
});
// JSON 데이터로부터 Member 객체 생성
factory Member.fromJson(Map<String, dynamic> json) {
return Member(
id: json['id']?.toString() ?? '',
userId: json['userId']?.toString() ?? '',
clubId: json['clubId']?.toString() ?? '',
name: json['name'] ?? '',
email: json['email'] ?? '',
role: json['role']?.toString(),
profileImage: json['profileImage']?.toString(),
phone: json['phone']?.toString(),
address: json['address']?.toString(),
joinDate: json['joinDate'] != null ? DateTime.parse(json['joinDate'].toString()) : null,
isActive: json['isActive'] ?? true,
gender: json['gender']?.toString(),
memberType: json['memberType']?.toString(),
handicap: json['handicap'] != null ? int.tryParse(json['handicap'].toString()) : null,
status: json['status']?.toString(),
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'].toString()) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'].toString()) : null,
);
}
// Member 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'clubId': clubId,
'name': name,
'email': email,
'role': role,
'profileImage': profileImage,
'phone': phone,
'address': address,
'joinDate': joinDate?.toIso8601String(),
'isActive': isActive,
'gender': gender,
'memberType': memberType,
'handicap': handicap,
'status': status,
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}
// 객체 복사 및 특정 필드 업데이트를 위한 copyWith 메서드
Member copyWith({
String? id,
String? userId,
String? clubId,
String? name,
String? email,
String? role,
String? profileImage,
String? phone,
String? address,
DateTime? joinDate,
bool? isActive,
String? gender,
String? memberType,
int? handicap,
String? status,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Member(
id: id ?? this.id,
userId: userId ?? this.userId,
clubId: clubId ?? this.clubId,
name: name ?? this.name,
email: email ?? this.email,
role: role ?? this.role,
profileImage: profileImage ?? this.profileImage,
phone: phone ?? this.phone,
address: address ?? this.address,
joinDate: joinDate ?? this.joinDate,
isActive: isActive ?? this.isActive,
gender: gender ?? this.gender,
memberType: memberType ?? this.memberType,
handicap: handicap ?? this.handicap,
status: status ?? this.status,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
+83
View File
@@ -0,0 +1,83 @@
class Score {
final String id;
final String memberId;
final String eventId;
final String clubId;
final List<int> frames;
final int totalScore;
final DateTime date;
final String? notes;
Score({
required this.id,
required this.memberId,
required this.eventId,
required this.clubId,
required this.frames,
required this.totalScore,
required this.date,
this.notes,
});
factory Score.fromJson(Map<String, dynamic> json) {
return Score(
id: json['id']?.toString() ?? '',
memberId: json['memberId']?.toString() ?? '',
eventId: json['eventId']?.toString() ?? '',
clubId: json['clubId']?.toString() ?? '',
frames: json['frames'] != null ? List<int>.from(json['frames']) : [],
totalScore: json['totalScore'] ?? 0,
date: json['date'] != null ? DateTime.parse(json['date'].toString()) : DateTime.now(),
notes: json['notes']?.toString(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'memberId': memberId,
'eventId': eventId,
'clubId': clubId,
'frames': frames,
'totalScore': totalScore,
'date': date.toIso8601String(),
'notes': notes,
};
}
}
class ScoreStatistics {
final double average;
final int highScore;
final int lowScore;
final int gamesPlayed;
final Map<String, dynamic>? additionalStats;
ScoreStatistics({
required this.average,
required this.highScore,
required this.lowScore,
required this.gamesPlayed,
this.additionalStats,
});
factory ScoreStatistics.fromJson(Map<String, dynamic> json) {
return ScoreStatistics(
average: json['average'] != null ? (json['average'] is int ? (json['average'] as int).toDouble() : json['average'].toDouble()) : 0.0,
highScore: json['highScore'] ?? 0,
lowScore: json['lowScore'] ?? 0,
gamesPlayed: json['gamesPlayed'] ?? 0,
additionalStats: json['additionalStats'],
);
}
Map<String, dynamic> toJson() {
return {
'average': average,
'highScore': highScore,
'lowScore': lowScore,
'gamesPlayed': gamesPlayed,
'additionalStats': additionalStats,
};
}
}
+243
View File
@@ -0,0 +1,243 @@
/// 구독 플랜 타입
enum SubscriptionPlanType {
free, // 무료 플랜
basic, // 기본 유료 플랜
premium, // 프리미엄 플랜
enterprise // 기업용 플랜
}
/// 구독 상태
enum SubscriptionStatus {
active, // 활성화 상태
canceled, // 취소됨 (아직 만료 안됨)
expired, // 만료됨
pending, // 결제 대기 중
failed // 결제 실패
}
/// 구독 모델
class Subscription {
final String id;
final String userId;
final String? clubId;
final SubscriptionPlanType planType;
final SubscriptionStatus status;
final DateTime startDate;
final DateTime endDate;
final double price;
final String currency;
final bool autoRenew;
final Map<String, dynamic>? features;
final String? paymentMethod;
final String? transactionId;
final DateTime createdAt;
final DateTime updatedAt;
Subscription({
required this.id,
required this.userId,
this.clubId,
required this.planType,
required this.status,
required this.startDate,
required this.endDate,
required this.price,
required this.currency,
required this.autoRenew,
this.features,
this.paymentMethod,
this.transactionId,
required this.createdAt,
required this.updatedAt,
});
/// JSON에서 구독 객체 생성
factory Subscription.fromJson(Map<String, dynamic> json) {
return Subscription(
id: json['id'],
userId: json['userId'],
clubId: json['clubId'],
planType: SubscriptionPlanType.values.firstWhere(
(e) => e.toString().split('.').last == json['planType'],
orElse: () => SubscriptionPlanType.free,
),
status: SubscriptionStatus.values.firstWhere(
(e) => e.toString().split('.').last == json['status'],
orElse: () => SubscriptionStatus.expired,
),
startDate: DateTime.parse(json['startDate']),
endDate: DateTime.parse(json['endDate']),
price: json['price'].toDouble(),
currency: json['currency'],
autoRenew: json['autoRenew'] ?? false,
features: json['features'],
paymentMethod: json['paymentMethod'],
transactionId: json['transactionId'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
);
}
/// 구독 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'clubId': clubId,
'planType': planType.toString().split('.').last,
'status': status.toString().split('.').last,
'startDate': startDate.toIso8601String(),
'endDate': endDate.toIso8601String(),
'price': price,
'currency': currency,
'autoRenew': autoRenew,
'features': features,
'paymentMethod': paymentMethod,
'transactionId': transactionId,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
/// 구독이 활성 상태인지 확인
bool get isActive => status == SubscriptionStatus.active;
/// 구독이 만료되었는지 확인
bool get isExpired => status == SubscriptionStatus.expired ||
DateTime.now().isAfter(endDate);
/// 구독 남은 일수 계산
int get daysLeft {
if (isExpired) return 0;
return endDate.difference(DateTime.now()).inDays;
}
/// 구독 플랜 이름 (한글)
String get planName {
switch (planType) {
case SubscriptionPlanType.free:
return '무료';
case SubscriptionPlanType.basic:
return '기본';
case SubscriptionPlanType.premium:
return '프리미엄';
case SubscriptionPlanType.enterprise:
return '기업용';
}
}
}
/// 구독 플랜 정보 모델
class SubscriptionPlan {
final SubscriptionPlanType type;
final String name;
final String description;
final double monthlyPrice;
final double yearlyPrice;
final String currency;
final List<String> features;
final int maxMembers;
final int maxEvents;
final bool hasAdvancedStats;
final bool hasCustomization;
final bool hasPriority;
SubscriptionPlan({
required this.type,
required this.name,
required this.description,
required this.monthlyPrice,
required this.yearlyPrice,
required this.currency,
required this.features,
required this.maxMembers,
required this.maxEvents,
required this.hasAdvancedStats,
required this.hasCustomization,
required this.hasPriority,
});
/// 기본 구독 플랜 목록 반환
static List<SubscriptionPlan> getDefaultPlans() {
return [
SubscriptionPlan(
type: SubscriptionPlanType.free,
name: '무료',
description: '소규모 클럽을 위한 기본 기능',
monthlyPrice: 0,
yearlyPrice: 0,
currency: 'KRW',
features: [
'최대 10명 회원 관리',
'기본 점수 기록',
'간단한 통계',
],
maxMembers: 10,
maxEvents: 5,
hasAdvancedStats: false,
hasCustomization: false,
hasPriority: false,
),
SubscriptionPlan(
type: SubscriptionPlanType.basic,
name: '기본',
description: '중소규모 클럽을 위한 확장 기능',
monthlyPrice: 9900,
yearlyPrice: 99000,
currency: 'KRW',
features: [
'최대 30명 회원 관리',
'상세 점수 분석',
'이벤트 관리',
'회원 통계',
],
maxMembers: 30,
maxEvents: 20,
hasAdvancedStats: true,
hasCustomization: false,
hasPriority: false,
),
SubscriptionPlan(
type: SubscriptionPlanType.premium,
name: '프리미엄',
description: '대규모 클럽을 위한 고급 기능',
monthlyPrice: 19900,
yearlyPrice: 199000,
currency: 'KRW',
features: [
'무제한 회원 관리',
'고급 통계 및 분석',
'무제한 이벤트',
'맞춤형 보고서',
'우선 지원',
],
maxMembers: 100,
maxEvents: 100,
hasAdvancedStats: true,
hasCustomization: true,
hasPriority: true,
),
SubscriptionPlan(
type: SubscriptionPlanType.enterprise,
name: '기업용',
description: '프로 클럽 및 단체를 위한 맞춤형 솔루션',
monthlyPrice: 49900,
yearlyPrice: 499000,
currency: 'KRW',
features: [
'무제한 회원 및 이벤트',
'전문가 수준 분석',
'맞춤형 기능',
'전담 지원',
'API 액세스',
],
maxMembers: 1000,
maxEvents: 1000,
hasAdvancedStats: true,
hasCustomization: true,
hasPriority: true,
),
];
}
}
+45
View File
@@ -0,0 +1,45 @@
class User {
final String id;
final String email;
final String name;
final String role;
final String? profileImage;
final String? clubId;
final String? clubRole;
User({
required this.id,
required this.email,
required this.name,
required this.role,
this.profileImage,
this.clubId,
this.clubRole,
});
// JSON 데이터로부터 User 객체 생성
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] != null ? json['id'].toString() : '',
email: json['email'] ?? '',
name: json['name'] ?? '',
role: json['role'] ?? 'user',
profileImage: json['profileImage'],
clubId: json['clubId']?.toString(),
clubRole: json['clubRole'],
);
}
// User 객체를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'name': name,
'role': role,
'profileImage': profileImage,
'clubId': clubId,
'clubRole': clubRole,
};
}
}
@@ -0,0 +1,456 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../services/admin_service.dart';
import '../../services/subscription_service.dart';
import '../../models/subscription_model.dart';
/// 관리자 대시보드 화면
class AdminDashboardScreen extends StatefulWidget {
const AdminDashboardScreen({super.key});
@override
State<AdminDashboardScreen> createState() => _AdminDashboardScreenState();
}
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
Map<String, dynamic>? _analyticsData;
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadData();
}
/// 데이터 로드
Future<void> _loadData() async {
setState(() {
_isLoading = true;
});
try {
final adminService = Provider.of<AdminService>(context, listen: false);
final analyticsData = await adminService.fetchClubAnalytics();
setState(() {
_analyticsData = analyticsData;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
final adminService = Provider.of<AdminService>(context);
final subscriptionService = Provider.of<SubscriptionService>(context);
return Scaffold(
appBar: AppBar(
title: const Text('관리자 대시보드'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadData,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadData,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPermissionsCard(adminService),
const SizedBox(height: 16),
_buildSubscriptionCard(subscriptionService),
const SizedBox(height: 16),
_buildAnalyticsCard(),
const SizedBox(height: 16),
_buildQuickActionsCard(context, adminService),
],
),
),
),
);
}
/// 권한 정보 카드
Widget _buildPermissionsCard(AdminService adminService) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.admin_panel_settings),
const SizedBox(width: 8),
Text(
'관리자 권한',
style: Theme.of(context).textTheme.titleLarge,
),
],
),
const Divider(),
const SizedBox(height: 8),
_buildPermissionItem(
'회원 관리',
adminService.canManageMembers,
Icons.people,
),
_buildPermissionItem(
'이벤트 관리',
adminService.canManageEvents,
Icons.event,
),
_buildPermissionItem(
'점수 관리',
adminService.canManageScores,
Icons.score,
),
_buildPermissionItem(
'설정 관리',
adminService.canManageSettings,
Icons.settings,
),
_buildPermissionItem(
'구독 관리',
adminService.canManageSubscription,
Icons.subscriptions,
),
_buildPermissionItem(
'역할 관리',
adminService.canManageRoles,
Icons.admin_panel_settings,
),
_buildPermissionItem(
'재무 정보 조회',
adminService.canViewFinancials,
Icons.attach_money,
),
_buildPermissionItem(
'데이터 내보내기',
adminService.canExportData,
Icons.file_download,
),
],
),
),
);
}
/// 권한 항목 위젯
Widget _buildPermissionItem(String title, bool hasPermission, IconData icon) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Icon(
icon,
size: 18,
color: hasPermission ? Colors.green : Colors.grey,
),
const SizedBox(width: 8),
Text(title),
const Spacer(),
Icon(
hasPermission ? Icons.check_circle : Icons.cancel,
color: hasPermission ? Colors.green : Colors.red,
size: 18,
),
],
),
);
}
/// 구독 정보 카드
Widget _buildSubscriptionCard(SubscriptionService subscriptionService) {
final subscription = subscriptionService.currentSubscription;
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.subscriptions),
const SizedBox(width: 8),
Text(
'구독 정보',
style: Theme.of(context).textTheme.titleLarge,
),
],
),
const Divider(),
const SizedBox(height: 8),
if (subscription != null) ...[
_buildInfoRow('플랜', subscription.planName),
_buildInfoRow('상태', _getStatusText(subscription.status)),
_buildInfoRow('시작일', _formatDate(subscription.startDate)),
_buildInfoRow('만료일', _formatDate(subscription.endDate)),
_buildInfoRow('자동 갱신', subscription.autoRenew ? '활성화' : '비활성화'),
_buildInfoRow('남은 일수', '${subscription.daysLeft}'),
] else
const Text('활성화된 구독이 없습니다.'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed('/subscription/manage');
},
child: const Text('구독 관리'),
),
],
),
),
);
}
/// 분석 데이터 카드
Widget _buildAnalyticsCard() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.analytics),
const SizedBox(width: 8),
Text(
'클럽 통계',
style: Theme.of(context).textTheme.titleLarge,
),
],
),
const Divider(),
const SizedBox(height: 8),
if (_analyticsData != null) ...[
_buildInfoRow('총 회원 수', '${_analyticsData!['totalMembers'] ?? 0}'),
_buildInfoRow('활성 회원 수', '${_analyticsData!['activeMembers'] ?? 0}'),
_buildInfoRow('이번 달 신규 회원', '${_analyticsData!['newMembersThisMonth'] ?? 0}'),
_buildInfoRow('총 이벤트 수', '${_analyticsData!['totalEvents'] ?? 0}'),
_buildInfoRow('이번 달 이벤트', '${_analyticsData!['eventsThisMonth'] ?? 0}'),
_buildInfoRow('총 게임 수', '${_analyticsData!['totalGames'] ?? 0}게임'),
_buildInfoRow('평균 점수', '${_analyticsData!['averageScore'] ?? 0}'),
] else
const Text('통계 데이터를 불러올 수 없습니다.'),
],
),
),
);
}
/// 빠른 작업 카드
Widget _buildQuickActionsCard(BuildContext context, AdminService adminService) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.flash_on),
const SizedBox(width: 8),
Text(
'빠른 작업',
style: Theme.of(context).textTheme.titleLarge,
),
],
),
const Divider(),
const SizedBox(height: 8),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
if (adminService.canManageMembers)
_buildActionButton(
context,
'회원 관리',
Icons.people,
() => Navigator.of(context).pushNamed('/admin/members'),
),
if (adminService.canManageEvents)
_buildActionButton(
context,
'이벤트 관리',
Icons.event,
() => Navigator.of(context).pushNamed('/admin/events'),
),
if (adminService.canManageSettings)
_buildActionButton(
context,
'클럽 설정',
Icons.settings,
() => Navigator.of(context).pushNamed('/admin/settings'),
),
if (adminService.canManageRoles)
_buildActionButton(
context,
'역할 관리',
Icons.admin_panel_settings,
() => Navigator.of(context).pushNamed('/admin/roles'),
),
if (adminService.canExportData)
_buildActionButton(
context,
'데이터 내보내기',
Icons.file_download,
() => _showExportDialog(context),
),
],
),
],
),
),
);
}
/// 작업 버튼 위젯
Widget _buildActionButton(
BuildContext context,
String label,
IconData icon,
VoidCallback onPressed,
) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 18),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
);
}
/// 데이터 내보내기 다이얼로그
void _showExportDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('데이터 내보내기'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.people),
title: const Text('회원 데이터'),
onTap: () => _exportData(context, 'members'),
),
ListTile(
leading: const Icon(Icons.event),
title: const Text('이벤트 데이터'),
onTap: () => _exportData(context, 'events'),
),
ListTile(
leading: const Icon(Icons.score),
title: const Text('점수 데이터'),
onTap: () => _exportData(context, 'scores'),
),
ListTile(
leading: const Icon(Icons.analytics),
title: const Text('통계 데이터'),
onTap: () => _exportData(context, 'analytics'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
],
),
);
}
/// 데이터 내보내기 처리
Future<void> _exportData(BuildContext context, String dataType) async {
Navigator.of(context).pop(); // 다이얼로그 닫기
setState(() {
_isLoading = true;
});
try {
final adminService = Provider.of<AdminService>(context, listen: false);
final downloadUrl = await adminService.exportClubData(dataType);
if (downloadUrl != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('데이터 내보내기 성공: $downloadUrl')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('데이터 내보내기에 실패했습니다.')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('데이터 내보내기 중 오류가 발생했습니다: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
/// 정보 행 위젯
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(value),
],
),
);
}
/// 날짜 포맷팅
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 구독 상태 텍스트
String _getStatusText(SubscriptionStatus status) {
switch (status) {
case SubscriptionStatus.active:
return '활성화';
case SubscriptionStatus.canceled:
return '취소됨';
case SubscriptionStatus.expired:
return '만료됨';
case SubscriptionStatus.pending:
return '대기 중';
case SubscriptionStatus.failed:
return '실패';
}
}
}
@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../services/auth_service.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _isSubmitting = false;
bool _emailSent = false;
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
// 비밀번호 재설정 이메일 전송 함수
Future<void> _resetPassword() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isSubmitting = true;
});
final authService = Provider.of<AuthService>(context, listen: false);
final success = await authService.sendPasswordResetEmail(
_emailController.text.trim(),
);
setState(() {
_isSubmitting = false;
_emailSent = success;
});
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('비밀번호 재설정 이메일이 전송되었습니다. 이메일을 확인해주세요.'),
backgroundColor: Colors.green,
),
);
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('비밀번호 재설정 이메일 전송에 실패했습니다. 이메일 주소를 확인해주세요.'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('비밀번호 찾기'),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(
Icons.lock_reset,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 24),
const Text(
'비밀번호를 잊으셨나요?',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
const Text(
'가입하신 이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드립니다.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
const SizedBox(height: 32),
// 이메일 입력 필드
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '이메일',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return '유효한 이메일 주소를 입력해주세요';
}
return null;
},
),
const SizedBox(height: 24),
// 비밀번호 재설정 이메일 전송 버튼
ElevatedButton(
onPressed: _isSubmitting ? null : _resetPassword,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: _isSubmitting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'비밀번호 재설정 이메일 전송',
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
// 로그인 화면으로 돌아가기
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('비밀번호가 기억나셨나요?'),
TextButton(
onPressed: () {
Navigator.pop(context); // 로그인 화면으로 돌아가기
},
child: const Text('로그인'),
),
],
),
],
),
),
),
),
),
);
}
}
+212
View File
@@ -0,0 +1,212 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../services/auth_service.dart';
import 'register_screen.dart';
import 'forgot_password_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _rememberMe = false;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
// 로그인 처리 함수
Future<void> _login() async {
if (_formKey.currentState!.validate()) {
final authService = Provider.of<AuthService>(context, listen: false);
final success = await authService.login(
_usernameController.text.trim(),
_passwordController.text,
);
if (success && mounted) {
// 로그인 성공 시 홈 화면으로 이동
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
} else if (!success && mounted) {
// 로그인 실패 시 스낵바 표시
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('로그인에 실패했습니다. 아이디와 비밀번호를 확인해주세요.'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final authService = Provider.of<AuthService>(context);
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 로고 이미지
Column(
children: [
Image.asset(
'assets/images/logo.png',
width: 120,
height: 120,
),
const SizedBox(height: 16),
const Text(
'레인보우 - 볼링 클럽 매니저',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const SizedBox(height: 48),
// 사용자명(아이디) 입력 필드
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: '아이디',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '아이디를 입력해주세요';
}
return null;
},
),
const SizedBox(height: 16),
// 비밀번호 입력 필드
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: '비밀번호',
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '비밀번호를 입력해주세요';
}
return null;
},
),
const SizedBox(height: 8),
// 로그인 상태 유지 체크박스
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
),
const Text('로그인 상태 유지'),
const Spacer(),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ForgotPasswordScreen(),
),
);
},
child: const Text('비밀번호 찾기'),
),
],
),
const SizedBox(height: 24),
// 로그인 버튼
ElevatedButton(
onPressed: authService.isLoading ? null : _login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: authService.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'로그인',
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
// 회원가입 링크
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('계정이 없으신가요?'),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegisterScreen(),
),
);
},
child: const Text('회원가입'),
),
],
),
],
),
),
),
),
),
);
}
}
+186
View File
@@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../services/auth_service.dart';
import '../../models/user_model.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
bool _isEditing = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final user = Provider.of<AuthService>(context, listen: false).currentUser;
if (user != null) {
_nameController.text = user.name;
}
});
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('내 프로필'),
actions: [
IconButton(
icon: Icon(_isEditing ? Icons.save : Icons.edit),
onPressed: () {
setState(() {
_isEditing = !_isEditing;
});
if (!_isEditing) {
// 저장 로직 구현 (추후 개발)
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('프로필이 업데이트되었습니다')),
);
}
}
},
),
],
),
body: Consumer<AuthService>(
builder: (context, authService, child) {
final User? user = authService.currentUser;
if (user == null) {
return const Center(child: Text('사용자 정보를 불러올 수 없습니다'));
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 프로필 이미지
CircleAvatar(
radius: 50,
backgroundImage: user.profileImage != null
? NetworkImage(user.profileImage!)
: null,
child: user.profileImage == null
? Text(
user.name.isNotEmpty ? user.name[0].toUpperCase() : '?',
style: const TextStyle(fontSize: 40),
)
: null,
),
const SizedBox(height: 16),
// 이름 필드
TextFormField(
controller: _nameController,
enabled: _isEditing,
decoration: const InputDecoration(
labelText: '이름',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이름을 입력해주세요';
}
return null;
},
),
const SizedBox(height: 16),
// 이메일 (수정 불가)
TextFormField(
initialValue: user.email,
enabled: false,
decoration: const InputDecoration(
labelText: '이메일',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 역할 표시
TextFormField(
initialValue: _getRoleDisplayName(user.role),
enabled: false,
decoration: const InputDecoration(
labelText: '역할',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 클럽 역할 표시 (클럽이 있는 경우)
if (user.clubRole != null)
TextFormField(
initialValue: _getRoleDisplayName(user.clubRole!),
enabled: false,
decoration: const InputDecoration(
labelText: '클럽 내 역할',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 32),
// 로그아웃 버튼
ElevatedButton(
onPressed: () async {
await authService.logout();
if (context.mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
),
child: const Text('로그아웃'),
),
],
),
),
);
},
),
);
}
// 역할 표시 이름 변환
String _getRoleDisplayName(String role) {
switch (role) {
case 'admin':
return '관리자';
case 'superadmin':
return '최고 관리자';
case 'user':
return '일반 사용자';
case 'owner':
return '클럽 소유자';
case 'manager':
return '클럽 매니저';
case 'member':
return '클럽 회원';
default:
return role;
}
}
}
@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../services/auth_service.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isConfirmPasswordVisible = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
// 회원가입 처리 함수
Future<void> _register() async {
if (_formKey.currentState!.validate()) {
final authService = Provider.of<AuthService>(context, listen: false);
final success = await authService.register(
_nameController.text.trim(),
_emailController.text.trim(),
_passwordController.text,
);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('회원가입이 완료되었습니다. 로그인해주세요.'),
backgroundColor: Colors.green,
),
);
Navigator.pop(context); // 로그인 화면으로 돌아가기
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('회원가입에 실패했습니다. 다시 시도해주세요.'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final authService = Provider.of<AuthService>(context);
return Scaffold(
appBar: AppBar(
title: const Text('회원가입'),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 이름 입력 필드
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '이름',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이름을 입력해주세요';
}
return null;
},
),
const SizedBox(height: 16),
// 이메일 입력 필드
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '이메일',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return '유효한 이메일 주소를 입력해주세요';
}
return null;
},
),
const SizedBox(height: 16),
// 비밀번호 입력 필드
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: '비밀번호',
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '비밀번호를 입력해주세요';
}
if (value.length < 6) {
return '비밀번호는 6자 이상이어야 합니다';
}
return null;
},
),
const SizedBox(height: 16),
// 비밀번호 확인 입력 필드
TextFormField(
controller: _confirmPasswordController,
obscureText: !_isConfirmPasswordVisible,
decoration: InputDecoration(
labelText: '비밀번호 확인',
prefixIcon: const Icon(Icons.lock_outline),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isConfirmPasswordVisible = !_isConfirmPasswordVisible;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '비밀번호를 다시 입력해주세요';
}
if (value != _passwordController.text) {
return '비밀번호가 일치하지 않습니다';
}
return null;
},
),
const SizedBox(height: 24),
// 회원가입 버튼
ElevatedButton(
onPressed: authService.isLoading ? null : _register,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: authService.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'회원가입',
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
// 로그인 화면으로 돌아가기
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('이미 계정이 있으신가요?'),
TextButton(
onPressed: () {
Navigator.pop(context); // 로그인 화면으로 돌아가기
},
child: const Text('로그인'),
),
],
),
],
),
),
),
),
),
);
}
}
@@ -0,0 +1,367 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/member_model.dart';
import '../../services/auth_service.dart';
import '../../services/club_service.dart';
import '../../services/member_service.dart';
import '../../utils/enum_mappings.dart';
class ClubSettingsScreen extends StatefulWidget {
static const routeName = '/club-settings';
const ClubSettingsScreen({super.key});
@override
_ClubSettingsScreenState createState() => _ClubSettingsScreenState();
}
class _ClubSettingsScreenState extends State<ClubSettingsScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
final _locationController = TextEditingController();
final _femaleHandicapController = TextEditingController();
String? _selectedAverageCalculationPeriod;
String? _selectedOwnerId;
List<Member> _members = [];
bool _isLoading = true;
bool _hasEditPermission = false;
@override
void initState() {
super.initState();
_loadData();
}
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_locationController.dispose();
_femaleHandicapController.dispose();
super.dispose();
}
Future<void> _loadData() async {
setState(() {
_isLoading = true;
});
try {
// 클럽 정보 로드
final clubService = Provider.of<ClubService>(context, listen: false);
final club = clubService.currentClub;
if (club != null) {
_nameController.text = club.name;
_descriptionController.text = club.description ?? '';
_locationController.text = club.location ?? '';
_femaleHandicapController.text = club.femaleHandicap?.toString() ?? '';
_selectedAverageCalculationPeriod =
club.averageCalculationPeriod ?? 'total';
_selectedOwnerId = club.ownerId;
}
// 회원 목록 로드
final memberService = Provider.of<MemberService>(context, listen: false);
await memberService.fetchClubMembers();
setState(() {
_members = memberService.members;
});
// 권한 확인
final authService = Provider.of<AuthService>(context, listen: false);
final currentUserId = authService.currentUser?.id;
// 현재 사용자가 클럽 소유자이거나 관리자인지 확인
if (currentUserId != null) {
final currentMember = _members.firstWhere(
(member) => member.userId == currentUserId,
orElse: () => Member(
id: '',
userId: '',
name: '',
memberType: '',
clubId: club?.id ?? '',
email: '',
isActive: true,
),
);
setState(() {
_hasEditPermission =
club?.ownerId == currentUserId || // 클럽 소유자
currentMember.memberType == 'manager' || // 클럽 매니저
authService.currentUser?.role == 'admin'; // 시스템 관리자
});
}
} catch (error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $error')));
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _saveClubInfo() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
final clubService = Provider.of<ClubService>(context, listen: false);
final club = clubService.currentClub;
if (club != null) {
// 기본 업데이트 데이터 준비
final Map<String, dynamic> updates = {
'clubId': club.id,
'name': _nameController.text.trim(),
};
// 빈 문자열이 아닌 경우에만 추가 (null 값 명시적 처리)
if (_descriptionController.text.trim().isNotEmpty) {
updates['description'] = _descriptionController.text.trim();
} else if (_descriptionController.text.trim().isEmpty &&
club.description != null) {
// 기존에 값이 있었는데 지금 비어있으면 명시적으로 null 설정
updates['description'] = null;
}
if (_locationController.text.trim().isNotEmpty) {
updates['location'] = _locationController.text.trim();
} else if (_locationController.text.trim().isEmpty &&
club.location != null) {
updates['location'] = null;
}
if (_femaleHandicapController.text.trim().isNotEmpty) {
updates['femaleHandicap'] = int.parse(
_femaleHandicapController.text.trim(),
);
}
if (_selectedAverageCalculationPeriod != null) {
updates['averageCalculationPeriod'] =
_selectedAverageCalculationPeriod;
}
if (_selectedOwnerId != null) {
updates['ownerId'] = _selectedOwnerId;
}
// 모임장 변경 시에만 이메일 정보 추가
if (_selectedOwnerId != club.ownerId) {
// 모임장이 변경된 경우 해당 회원의 이메일 정보 찾기
final selectedMember = _members.firstWhere(
(member) => member.userId == _selectedOwnerId,
orElse: () => Member(
id: '',
userId: '',
name: '',
memberType: '',
clubId: club.id,
email: '',
isActive: true,
),
);
// 이메일이 있는 경우에만 추가 (빈 문자열이나 null은 전송하지 않음)
if (selectedMember.email.isNotEmpty) {
updates['ownerEmail'] = selectedMember.email;
}
}
await clubService.updateClub(club.id, updates);
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('클럽 정보가 성공적으로 저장되었습니다')));
}
}
} catch (error) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('클럽 정보 저장 중 오류가 발생했습니다: $error')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('클럽 설정')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
if (!_hasEditPermission)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16.0),
color: Colors.amber.shade100,
child: const Text(
'권한 알림: 클럽 설정을 변경하려면 클럽 소유자 또는 관리자 권한이 필요합니다.',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 클럽 이름
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '클럽 이름',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '클럽 이름을 입력해주세요';
}
return null;
},
enabled: _hasEditPermission,
),
// 설명
const SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: '설명',
border: OutlineInputBorder(),
),
maxLines: 3,
enabled: _hasEditPermission,
),
// 위치
const SizedBox(height: 16),
TextFormField(
controller: _locationController,
decoration: const InputDecoration(
labelText: '위치',
border: OutlineInputBorder(),
),
enabled: _hasEditPermission,
),
// 여성 기본 핸디캡
const SizedBox(height: 16),
TextFormField(
controller: _femaleHandicapController,
decoration: const InputDecoration(
labelText: '여성 기본 핸디캡',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value != null && value.isNotEmpty) {
final handicap = int.tryParse(value);
if (handicap == null) {
return '숫자를 입력해주세요';
}
if (handicap < 0) {
return '0 이상의 값을 입력해주세요';
}
}
return null;
},
enabled: _hasEditPermission,
),
// 평균 산정 기준
const SizedBox(height: 16),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: '평균 산정 기준',
border: OutlineInputBorder(),
),
value: _selectedAverageCalculationPeriod,
items: averageCalculationPeriodOptions.entries
.map(
(entry) => DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
),
)
.toList(),
onChanged: _hasEditPermission
? (value) {
setState(() {
_selectedAverageCalculationPeriod = value;
});
}
: null,
),
// 모임장 설정
const SizedBox(height: 16),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: '모임장 설정',
border: OutlineInputBorder(),
),
value: _selectedOwnerId,
items: _members.map((member) {
return DropdownMenuItem<String>(
value: member.userId,
child: Text(
'${member.name} (${getMemberTypeLabel(member.memberType)})',
),
);
}).toList(),
onChanged: _hasEditPermission
? (value) {
setState(() {
_selectedOwnerId = value;
});
}
: null,
),
// 저장 버튼
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _hasEditPermission
? _saveClubInfo
: null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
),
),
child: const Text('저장'),
),
),
],
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,394 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../services/auth_service.dart';
import '../../services/club_service.dart';
import '../../services/member_service.dart';
import '../../services/event_service.dart';
import '../../models/club_model.dart';
import 'club_settings_screen.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
bool _isLoading = false;
bool _isInit = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInit) {
_loadDashboardData();
_isInit = true;
}
}
Future<void> _loadDashboardData() async {
setState(() {
_isLoading = true;
});
try {
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
// 클럽 정보 로드
if (clubService.currentClub == null) {
await clubService.initialize(authService.token!);
}
// 회원 및 이벤트 서비스 초기화
if (clubService.currentClub != null) {
final memberService = Provider.of<MemberService>(context, listen: false);
final eventService = Provider.of<EventService>(context, listen: false);
memberService.initialize(authService.token!);
eventService.initialize(authService.token!);
// 회원 및 이벤트 데이터 로드
await Future.wait([
memberService.fetchClubMembers(),
eventService.fetchClubEvents(),
]);
}
} catch (e) {
// 오류 처리
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadDashboardData,
child: Consumer3<ClubService, MemberService, EventService>(
builder: (context, clubService, memberService, eventService, _) {
final club = clubService.currentClub;
if (club == null) {
return const Center(
child: Text('클럽 정보를 불러올 수 없습니다'),
);
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 클럽 정보 카드
_buildClubInfoCard(club),
const SizedBox(height: 16),
// 통계 카드들
_buildStatisticsCards(
memberCount: memberService.members.length,
eventCount: eventService.events.length,
),
const SizedBox(height: 16),
// 최근 이벤트 목록
_buildRecentEventsList(eventService),
],
),
);
},
),
),
);
}
Widget _buildClubInfoCard(Club club) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
if (club.logo != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
club.logo!,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 60,
height: 60,
color: Colors.grey[300],
child: const Icon(Icons.sports_golf),
);
},
),
)
else
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.sports_golf,
size: 30,
color: Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
club.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
if (club.description != null)
Text(
club.description!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
IconButton(
icon: const Icon(Icons.settings),
tooltip: '클럽 설정',
onPressed: () {
Navigator.of(context).pushNamed(ClubSettingsScreen.routeName);
},
),
],
),
const SizedBox(height: 16),
if (club.address != null)
_buildInfoRow(Icons.location_on, club.address!),
if (club.phone != null)
_buildInfoRow(Icons.phone, club.phone!),
if (club.email != null)
_buildInfoRow(Icons.email, club.email!),
if (club.website != null)
_buildInfoRow(Icons.language, club.website!),
],
),
),
);
}
Widget _buildInfoRow(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey[600]),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: TextStyle(fontSize: 14, color: Colors.grey[800]),
),
),
],
),
);
}
Widget _buildStatisticsCards({required int memberCount, required int eventCount}) {
return Row(
children: [
Expanded(
child: _buildStatCard(
title: '회원',
value: memberCount.toString(),
icon: Icons.people,
color: Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
title: '이벤트',
value: eventCount.toString(),
icon: Icons.event,
color: Colors.orange,
),
),
],
);
}
Widget _buildStatCard({
required String title,
required String value,
required IconData icon,
required Color color,
}) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Icon(icon, size: 32, color: color),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
);
}
Widget _buildRecentEventsList(EventService eventService) {
final events = eventService.events;
if (events.isEmpty) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text('예정된 이벤트가 없습니다'),
),
),
);
}
// 날짜 기준으로 정렬 (가까운 날짜순)
final sortedEvents = [...events];
sortedEvents.sort((a, b) => a.startDate.compareTo(b.startDate));
// 앞으로 진행될 이벤트만 필터링 (최대 3개)
final upcomingEvents = sortedEvents
.where((event) => event.startDate.isAfter(DateTime.now()))
.take(3)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'다가오는 이벤트',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {
// 이벤트 목록 화면으로 이동 (추후 구현)
},
child: const Text('모두 보기'),
),
],
),
const SizedBox(height: 8),
...upcomingEvents.map((event) => _buildEventCard(event)),
],
);
}
Widget _buildEventCard(event) {
final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm');
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
title: Text(
event.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
dateFormat.format(event.startDate),
style: TextStyle(
color: Colors.grey[700],
),
),
if (event.location != null)
Text(
event.location!,
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {
// 이벤트 상세 화면으로 이동 (추후 구현)
},
),
);
}
}
+671
View File
@@ -0,0 +1,671 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../services/auth_service.dart';
import '../../services/club_service.dart';
import '../../services/event_service.dart';
import '../../models/event_model.dart';
class EventsScreen extends StatefulWidget {
const EventsScreen({super.key});
@override
State<EventsScreen> createState() => _EventsScreenState();
}
class _EventsScreenState extends State<EventsScreen> {
bool _isInit = false;
String _searchQuery = '';
final TextEditingController _searchController = TextEditingController();
String _filterType = '전체'; // '전체', '예정', '지난'
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInit) {
_loadEvents();
_isInit = true;
}
}
Future<void> _loadEvents() async {
try {
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
final eventService = Provider.of<EventService>(context, listen: false);
if (clubService.currentClub != null) {
eventService.initialize(authService.token!);
await eventService.fetchClubEvents();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('이벤트 목록을 불러오는데 실패했습니다: $e')),
);
}
}
}
List<Event> _getFilteredEvents(List<Event> events) {
// 검색어 필터링
List<Event> filteredEvents = events;
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
filteredEvents = filteredEvents.where((event) {
return event.title.toLowerCase().contains(query) ||
(event.description?.toLowerCase().contains(query) ?? false) ||
(event.location?.toLowerCase().contains(query) ?? false);
}).toList();
}
// 날짜 필터링
final now = DateTime.now();
if (_filterType == '예정') {
filteredEvents = filteredEvents.where((event) => event.startDate.isAfter(now)).toList();
} else if (_filterType == '지난') {
filteredEvents = filteredEvents.where((event) => event.startDate.isBefore(now)).toList();
}
// 날짜순 정렬
filteredEvents.sort((a, b) => a.startDate.compareTo(b.startDate));
return filteredEvents;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 검색 바
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '이벤트 검색',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(vertical: 0),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
const SizedBox(height: 8),
// 필터 버튼들
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildFilterChip('전체'),
const SizedBox(width: 8),
_buildFilterChip('예정'),
const SizedBox(width: 8),
_buildFilterChip('지난'),
],
),
),
],
),
),
// 이벤트 목록
Expanded(
child: Consumer<EventService>(
builder: (context, eventService, _) {
if (eventService.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final filteredEvents = _getFilteredEvents(eventService.events);
if (filteredEvents.isEmpty) {
return Center(
child: Text(
_searchQuery.isEmpty
? '등록된 이벤트가 없습니다'
: '검색 결과가 없습니다',
),
);
}
return RefreshIndicator(
onRefresh: _loadEvents,
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 16),
itemCount: filteredEvents.length,
itemBuilder: (context, index) {
final event = filteredEvents[index];
return _buildEventCard(event);
},
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 이벤트 추가 화면으로 이동 (추후 구현)
_showAddEventDialog();
},
backgroundColor: Colors.blue,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildFilterChip(String label) {
final isSelected = _filterType == label;
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) {
setState(() {
_filterType = selected ? label : '전체';
});
},
backgroundColor: Colors.grey[200],
selectedColor: Colors.blue[100],
checkmarkColor: Colors.blue,
labelStyle: TextStyle(
color: isSelected ? Colors.blue[800] : Colors.black87,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
);
}
Widget _buildEventCard(Event event) {
final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm');
final now = DateTime.now();
final isPast = event.startDate.isBefore(now);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isPast ? Colors.grey[200] : Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.event,
color: isPast ? Colors.grey[600] : Colors.blue,
),
),
title: Text(
event.title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isPast ? Colors.grey[600] : Colors.black,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
dateFormat.format(event.startDate),
style: TextStyle(
color: isPast ? Colors.grey[500] : Colors.grey[700],
),
),
if (event.location != null)
Text(
event.location!,
style: TextStyle(
color: isPast ? Colors.grey[500] : Colors.grey[600],
),
),
],
),
trailing: PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'edit') {
_showEditEventDialog(event);
} else if (value == 'delete') {
_showDeleteConfirmationDialog(event);
}
},
itemBuilder: (context) => [
const PopupMenuItem<String>(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 18),
SizedBox(width: 8),
Text('수정'),
],
),
),
const PopupMenuItem<String>(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 18, color: Colors.red),
SizedBox(width: 8),
Text('삭제', style: TextStyle(color: Colors.red)),
],
),
),
],
),
onTap: () {
// 이벤트 상세 화면으로 이동 (추후 구현)
_showEventDetailsDialog(event);
},
),
);
}
// 이벤트 추가 다이얼로그
void _showAddEventDialog() {
final titleController = TextEditingController();
final descriptionController = TextEditingController();
final locationController = TextEditingController();
DateTime selectedDate = DateTime.now().add(const Duration(days: 1));
TimeOfDay selectedTime = TimeOfDay.now();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('이벤트 추가'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: '제목',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '설명',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: locationController,
decoration: const InputDecoration(
labelText: '장소',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ListTile(
title: const Text('날짜'),
subtitle: Text(
DateFormat('yyyy년 MM월 dd일').format(selectedDate),
),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (pickedDate != null && context.mounted) {
setState(() {
selectedDate = pickedDate;
});
}
},
),
ListTile(
title: const Text('시간'),
subtitle: Text(selectedTime.format(context)),
trailing: const Icon(Icons.access_time),
onTap: () async {
final pickedTime = await showTimePicker(
context: context,
initialTime: selectedTime,
);
if (pickedTime != null && context.mounted) {
setState(() {
selectedTime = pickedTime;
});
}
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('제목은 필수 입력 항목입니다')),
);
return;
}
try {
final eventService = Provider.of<EventService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
if (clubService.currentClub != null) {
// 날짜와 시간 결합
final eventDateTime = DateTime(
selectedDate.year,
selectedDate.month,
selectedDate.day,
selectedTime.hour,
selectedTime.minute,
);
await eventService.createEvent({
'title': titleController.text.trim(),
'description': descriptionController.text.trim(),
'location': locationController.text.trim(),
'startDate': eventDateTime.toIso8601String(),
'clubId': clubService.currentClub!.id,
'isActive': true,
});
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('이벤트가 추가되었습니다')),
);
}
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('이벤트 추가에 실패했습니다: $e')),
);
}
},
child: const Text('추가'),
),
],
),
);
}
// 이벤트 수정 다이얼로그
void _showEditEventDialog(Event event) {
final titleController = TextEditingController(text: event.title);
final descriptionController = TextEditingController(text: event.description ?? '');
final locationController = TextEditingController(text: event.location ?? '');
DateTime selectedDate = event.startDate;
TimeOfDay selectedTime = TimeOfDay(
hour: event.startDate.hour,
minute: event.startDate.minute,
);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('이벤트 수정'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: '제목',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '설명',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: locationController,
decoration: const InputDecoration(
labelText: '장소',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ListTile(
title: const Text('날짜'),
subtitle: Text(
DateFormat('yyyy년 MM월 dd일').format(selectedDate),
),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (pickedDate != null && context.mounted) {
setState(() {
selectedDate = pickedDate;
});
}
},
),
ListTile(
title: const Text('시간'),
subtitle: Text(selectedTime.format(context)),
trailing: const Icon(Icons.access_time),
onTap: () async {
final pickedTime = await showTimePicker(
context: context,
initialTime: selectedTime,
);
if (pickedTime != null && context.mounted) {
setState(() {
selectedTime = pickedTime;
});
}
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('제목은 필수 입력 항목입니다')),
);
return;
}
try {
final eventService = Provider.of<EventService>(context, listen: false);
// 날짜와 시간 결합
final eventDateTime = DateTime(
selectedDate.year,
selectedDate.month,
selectedDate.day,
selectedTime.hour,
selectedTime.minute,
);
await eventService.updateEvent(event.id, {
'title': titleController.text.trim(),
'description': descriptionController.text.trim(),
'location': locationController.text.trim(),
'startDate': eventDateTime.toIso8601String(),
});
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('이벤트가 업데이트되었습니다')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('이벤트 업데이트에 실패했습니다: $e')),
);
}
},
child: const Text('저장'),
),
],
),
);
}
// 이벤트 상세 정보 다이얼로그
void _showEventDetailsDialog(Event event) {
final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(event.title),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.calendar_today, size: 16, color: Colors.blue),
const SizedBox(width: 8),
Text(dateFormat.format(event.startDate)),
],
),
const SizedBox(height: 8),
if (event.location != null) ...[
Row(
children: [
const Icon(Icons.location_on, size: 16, color: Colors.blue),
const SizedBox(width: 8),
Text(event.location!),
],
),
const SizedBox(height: 8),
],
if (event.description != null) ...[
const Divider(),
const SizedBox(height: 8),
Text(
event.description!,
style: const TextStyle(fontSize: 16),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('닫기'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_showEditEventDialog(event);
},
child: const Text('수정'),
),
],
),
);
}
// 이벤트 삭제 확인 다이얼로그
void _showDeleteConfirmationDialog(Event event) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('이벤트 삭제'),
content: Text('${event.title} 이벤트를 정말 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
TextButton(
onPressed: () async {
try {
final eventService = Provider.of<EventService>(context, listen: false);
await eventService.deleteEvent(event.id);
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('이벤트가 삭제되었습니다')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('이벤트 삭제에 실패했습니다: $e')),
);
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('삭제'),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More