프로젝트 최초 커밋

This commit is contained in:
JiWoong Sul
2025-07-02 17:45:44 +09:00
commit e346f83c97
235 changed files with 23139 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
---
description:
globs:
alwaysApply: true
---
---
description:
globs:
alwaysApply: true
---
## Core Directive
You are a senior software engineer AI assistant. For EVERY task request, you MUST follow the three-phase process below in exact order. Each phase must be completed with expert-level precision and detail.
## Guiding Principles
- **Minimalistic Approach**: Implement high-quality, clean solutions while avoiding unnecessary complexity
- **Expert-Level Standards**: Every output must meet professional software engineering standards
- **Concrete Results**: Provide specific, actionable details at each step
---
## Phase 1: Codebase Exploration & Analysis
**REQUIRED ACTIONS:**
1. **Systematic File Discovery**
- List ALL potentially relevant files, directories, and modules
- Search for related keywords, functions, classes, and patterns
- Examine each identified file thoroughly
2. **Convention & Style Analysis**
- Document coding conventions (naming, formatting, architecture patterns)
- Identify existing code style guidelines
- Note framework/library usage patterns
- Catalog error handling approaches
**OUTPUT FORMAT:**
```
### Codebase Analysis Results
**Relevant Files Found:**
- [file_path]: [brief description of relevance]
**Code Conventions Identified:**
- Naming: [convention details]
- Architecture: [pattern details]
- Styling: [format details]
**Key Dependencies & Patterns:**
- [library/framework]: [usage pattern]
```
---
## Phase 2: Implementation Planning
**REQUIRED ACTIONS:**
Based on Phase 1 findings, create a detailed implementation roadmap.
**OUTPUT FORMAT:**
```markdown
## Implementation Plan
### Module: [Module Name]
**Summary:** [1-2 sentence description of what needs to be implemented]
**Tasks:**
- [ ] [Specific implementation task]
- [ ] [Specific implementation task]
**Acceptance Criteria:**
- [ ] [Measurable success criterion]
- [ ] [Measurable success criterion]
- [ ] [Performance/quality requirement]
### Module: [Next Module Name]
[Repeat structure above]
```
---
## Phase 3: Implementation Execution
**REQUIRED ACTIONS:**
1. Implement each module following the plan from Phase 2
2. Verify ALL acceptance criteria are met before proceeding
3. Ensure code adheres to conventions identified in Phase 1
**QUALITY GATES:**
- [ ] All acceptance criteria validated
- [ ] Code follows established conventions
- [ ] Minimalistic approach maintained
- [ ] Expert-level implementation standards met
---
## Success Validation
Before completing any task, confirm:
- ✅ All three phases completed sequentially
- ✅ Each phase output meets specified format requirements
- ✅ Implementation satisfies all acceptance criteria
- ✅ Code quality meets professional standards
## Response Structure
Always structure your response as:
1. **Phase 1 Results**: [Codebase analysis findings]
2. **Phase 2 Plan**: [Implementation roadmap]
3. **Phase 3 Implementation**: [Actual code with validation]

View File

@@ -0,0 +1,53 @@
---
description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness.
globs: .cursor/rules/*.mdc
alwaysApply: true
---
- **Required Rule Structure:**
```markdown
---
description: Clear, one-line description of what the rule enforces
globs: path/to/files/*.ext, other/path/**/*
alwaysApply: boolean
---
- **Main Points in Bold**
- Sub-points with details
- Examples and explanations
```
- **File References:**
- Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files
- Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references
- Example: [schema.prisma](mdc:prisma/schema.prisma) for code references
- **Code Examples:**
- Use language-specific code blocks
```typescript
// ✅ DO: Show good examples
const goodExample = true;
// ❌ DON'T: Show anti-patterns
const badExample = false;
```
- **Rule Content Guidelines:**
- Start with high-level overview
- Include specific, actionable requirements
- Show examples of correct implementation
- Reference existing code when possible
- Keep rules DRY by referencing other rules
- **Rule Maintenance:**
- Update rules when new patterns emerge
- Add examples from actual codebase
- Remove outdated patterns
- Cross-reference related rules
- **Best Practices:**
- Use bullet points for clarity
- Keep descriptions concise
- Include both DO and DON'T examples
- Reference actual code over theoretical examples
- Use consistent formatting across rules

View File

@@ -0,0 +1,133 @@
---
description:
globs:
alwaysApply: true
---
You are a senior Dart programmer with experience in the Flutter framework and a preference for clean programming and design patterns.
Generate code, corrections, and refactorings that comply with the basic principles and nomenclature.
## Dart General Guidelines
### Basic Principles
- Use English for all code
- Use Korean for all comments in code,requests,answers and documentation.
- Always declare the type of each variable and function (parameters and return value).
- Avoid using any.
- Create necessary types.
- Don't leave blank lines within a function.
- One export per file.
### Nomenclature
- Use PascalCase for classes.
- Use camelCase for variables, functions, and methods.
- Use underscores_case for file and directory names.
- Use UPPERCASE for environment variables.
- Avoid magic numbers and define constants.
- Start each function with a verb.
- Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
- Use complete words instead of abbreviations and correct spelling.
- Except for standard abbreviations like API, URL, etc.
- Except for well-known abbreviations:
- i, j for loops
- err for errors
- ctx for contexts
- req, res, next for middleware function parameters
### Functions
- In this context, what is understood as a function will also apply to a method.
- Write short functions with a single purpose. Less than 20 instructions.
- Name functions with a verb and something else.
- If it returns a boolean, use isX or hasX, canX, etc.
- If it doesn't return anything, use executeX or saveX, etc.
- Avoid nesting blocks by:
- Early checks and returns.
- Extraction to utility functions.
- Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting.
- Use arrow functions for simple functions (less than 3 instructions).
- Use named functions for non-simple functions.
- Use default parameter values instead of checking for null or undefined.
- Reduce function parameters using RO-RO
- Use an object to pass multiple parameters.
- Use an object to return results.
- Declare necessary types for input arguments and output.
- Use a single level of abstraction.
### Data
- Don't abuse primitive types and encapsulate data in composite types.
- Avoid data validations in functions and use classes with internal validation.
- Prefer immutability for data.
- Use readonly for data that doesn't change.
- Use as const for literals that don't change.
### Classes
- Follow SOLID principles.
- Prefer composition over inheritance.
- Declare interfaces to define contracts.
- Write small classes with a single purpose.
- Less than 200 instructions.
- Less than 10 public methods.
- Less than 10 properties.
### Exceptions
- Use exceptions to handle errors you don't expect.
- If you catch an exception, it should be to:
- Fix an expected problem.
- Add context.
- Otherwise, use a global handler.
### Testing
- Follow the Arrange-Act-Assert convention for tests.
- Name test variables clearly.
- Follow the convention: inputX, mockX, actualX, expectedX, etc.
- Write unit tests for each public function.
- Use test doubles to simulate dependencies.
- Except for third-party dependencies that are not expensive to execute.
- Write acceptance tests for each module.
- Follow the Given-When-Then convention.
## Specific to Flutter
### Basic Principles
- Use clean architecture
- see modules if you need to organize code into modules
- see controllers if you need to organize code into controllers
- see services if you need to organize code into services
- see repositories if you need to organize code into repositories
- see entities if you need to organize code into entities
- Use repository pattern for data persistence
- see cache if you need to cache data
- Use controller pattern for business logic with Riverpod
- Use Riverpod to manage state
- see keepAlive if you need to keep the state alive
- Use freezed to manage UI states
- Controller always takes methods as input and updates the UI state that effects the UI
- Use getIt to manage dependencies
- Use singleton for services and repositories
- Use factory for use cases
- Use lazy singleton for controllers
- Use AutoRoute to manage routes
- Use extras to pass data between pages
- Use extensions to manage reusable code
- Use ThemeData to manage themes
- Use AppLocalizations to manage translations
- Use constants to manage constants values
- When a widget tree becomes too deep, it can lead to longer build times and increased memory usage. Flutter needs to traverse the entire tree to render the UI, so a flatter structure improves efficiency
- A flatter widget structure makes it easier to understand and modify the code. Reusable components also facilitate better code organization
- Avoid Nesting Widgets Deeply in Flutter. Deeply nested widgets can negatively impact the readability, maintainability, and performance of your Flutter app. Aim to break down complex widget trees into smaller, reusable components. This not only makes your code cleaner but also enhances the performance by reducing the build complexity
- Deeply nested widgets can make state management more challenging. By keeping the tree shallow, it becomes easier to manage state and pass data between widgets
- Break down large widgets into smaller, focused widgets
- Utilize const constructors wherever possible to reduce rebuilds
### Testing
- Use the standard widget testing for flutter
- Use integration tests for each api module.

View File

@@ -0,0 +1,73 @@
---
description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices.
globs: **/*
alwaysApply: true
---
- **Rule Improvement Triggers:**
- New code patterns not covered by existing rules
- Repeated similar implementations across files
- Common error patterns that could be prevented
- New libraries or tools being used consistently
- Emerging best practices in the codebase
- **Analysis Process:**
- Compare new code with existing rules
- Identify patterns that should be standardized
- Look for references to external documentation
- Check for consistent error handling patterns
- Monitor test patterns and coverage
- **Rule Updates:**
- **Add New Rules When:**
- A new technology/pattern is used in 3+ files
- Common bugs could be prevented by a rule
- Code reviews repeatedly mention the same feedback
- New security or performance patterns emerge
- **Modify Existing Rules When:**
- Better examples exist in the codebase
- Additional edge cases are discovered
- Related rules have been updated
- Implementation details have changed
- **Example Pattern Recognition:**
```typescript
// If you see repeated patterns like:
const data = await prisma.user.findMany({
select: { id: true, email: true },
where: { status: 'ACTIVE' }
});
// Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc):
// - Standard select fields
// - Common where conditions
// - Performance optimization patterns
```
- **Rule Quality Checks:**
- Rules should be actionable and specific
- Examples should come from actual code
- References should be up to date
- Patterns should be consistently enforced
- **Continuous Improvement:**
- Monitor code review comments
- Track common development questions
- Update rules after major refactors
- Add links to relevant documentation
- Cross-reference related rules
- **Rule Deprecation:**
- Mark outdated patterns as deprecated
- Remove rules that no longer apply
- Update references to deprecated rules
- Document migration paths for old patterns
- **Documentation Updates:**
- Keep examples synchronized with code
- Update references to external docs
- Maintain links between related rules
- Document breaking changes
Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure.

45
.gitignore vendored Normal file
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
.metadata Normal file
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: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: linux
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: macos
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: windows
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# 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'

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# superport
A new Flutter project.

1
analysis_options.yaml Normal file
View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

14
android/.gitignore vendored Normal file
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

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.superport"
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.superport"
// 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 = "../.."
}

View File

@@ -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>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="superport"
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>

View File

@@ -0,0 +1,5 @@
package com.example.superport
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
android/build.gradle.kts Normal file
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)
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -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.10.2-all.zip

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.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

327
doc/development_log.md Normal file
View File

@@ -0,0 +1,327 @@
# supERPort ERP 개발일지
## 대화 1: 프로젝트 분석 및 계획
- **날짜**: 2025년 04월 16일
- **내용**:
- supERPort ERP 시스템의 요구사항 문서(PRD) 검토
- Flutter 기반 프론트엔드 구현 계획 수립
- 주요 기능 리뷰: 장비 입고/출고, 회사/사용자/라이센스 등록 관리
- Metronic Admin Template 스타일 가이드라인 확인
- 추천 디렉토리 구조 검토
## 대화 2: 프로젝트 디렉토리 구조 생성
- **날짜**: 2025년 04월 16일
- **내용**:
- PRD에 명시된 디렉토리 구조 구현
- 모델, 화면, 서비스, 유틸리티 디렉토리 생성
- 각 기능별 파일 구조 설계
## 대화 3: 모델 클래스 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 장비 관련 모델 구현:
- `EquipmentModel`: 공통 장비 정보
- `EquipmentInModel`: 장비 입고 정보
- `EquipmentOutModel`: 장비 출고 정보
- 회사 관련 모델 구현:
- `CompanyModel`: 회사 및 지점 정보
- 사용자 관련 모델 구현:
- `UserModel`: 사용자 정보 및 권한
- 라이센스 관련 모델 구현:
- `LicenseModel`: 유지보수 라이센스 정보
## 대화 4: 테마 및 공통 위젯 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 앱 테마 구현 (`AppTheme` 클래스)
- Metronic Admin 스타일 적용
- 색상 팔레트, 텍스트 스타일, 버튼 스타일 정의
- 공통 위젯 구현:
- `PageTitle`: 화면 상단 제목 및 추가 버튼
- `DataTableCard`: 데이터 테이블을 감싸는 카드 위젯
- `FormFieldWrapper`: 폼 필드 레이블 및 래퍼
- `DatePickerField`: 날짜 선택 필드
- `CategorySelectionField`: 대분류/중분류/소분류 선택 필드
## 대화 5: 모의 데이터 서비스 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- `MockDataService` 클래스 구현
- 모의 데이터 생성 및 관리 기능:
- 장비 입고/출고 데이터 관리
- 회사 데이터 관리
- 사용자 데이터 관리
- 라이센스 데이터 관리
- CRUD 작업 지원 (생성, 조회, 업데이트, 삭제)
## 대화 6: 장비 입고 화면 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 장비 입고 목록 화면 구현:
- 데이터 테이블로 입고 장비 목록 표시
- 추가, 수정, 삭제 기능 구현
- 장비 입고 폼 화면 구현:
- 제조사명, 장비명, 분류 정보 입력
- 시리얼 넘버, 바코드, 물품 수량 입력
- 입고일 선택
- 폼 유효성 검사
## 대화 7: 장비 출고 화면 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 장비 출고 목록 화면 구현:
- 데이터 테이블로 출고 장비 목록 표시
- 추가, 수정, 삭제 기능 구현
- 장비 출고 폼 화면 구현:
- 장비명, 분류 정보 선택
- 시리얼 넘버, 바코드 입력
- 출고 수량, 출고일 입력
- 폼 유효성 검사
## 대화 8: 메인 화면 및 라우팅 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 메인 애플리케이션 파일(`main.dart`) 구현
- 홈 화면 구현:
- 주요 기능 바로가기 카드 메뉴 (그리드 레이아웃)
- 라우팅 설정:
- 각 화면에 대한 라우트 정의
- 인자 전달 처리 (수정 화면용 ID 등)
## 대화 9: 상수 및 유효성 검사 유틸리티 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 상수 관리 파일 구현:
- 라우트 경로 상수
- 장비 상태 상수
- 사용자 역할 상수
- 유효성 검사 함수 구현:
- 필수 입력값 검사
- 숫자 입력값 검사
- 전화번호 형식 검사
## 대화 10: 회사 관리 화면 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 회사 목록 화면 구현:
- 데이터 테이블로 회사 목록 표시
- 회사명, 주소, 지점 수 표시
- 추가, 수정, 삭제 기능 구현
- 회사 등록/수정 폼 구현:
- 회사명, 주소 입력
- 지점 정보 관리 (추가, 수정, 삭제)
- 지점 정보 입력 (지점명, 주소, 전화번호)
- 폼 유효성 검사
## 대화 11: 사용자 관리 화면 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 사용자 목록 화면 구현:
- 데이터 테이블로 사용자 목록 표시
- 이름, 소속 회사, 권한 정보 표시
- 추가, 수정, 삭제 기능 구현
- 사용자 등록/수정 폼 구현:
- 이름 입력
- 소속 회사 선택 (드롭다운)
- 권한 선택 (라디오 버튼: 관리자/일반 사용자)
- 폼 유효성 검사
## 대화 12: 라이센스 관리 화면 구현
- **날짜**: 2025년 04월 16일
- **내용**:
- 라이센스 목록 화면 구현:
- 데이터 테이블로 라이센스 목록 표시
- 라이센스명, 회사, 기간, 방문 주기 표시
- 추가, 수정, 삭제 기능 구현
- 라이센스 등록/수정 폼 구현:
- 라이센스명 입력
- 회사 선택 (드롭다운)
- 라이센스 기간, 방문 주기 입력
- 폼 유효성 검사
## 대화 13: 카테고리 선택 위젯 수정
- **날짜**: 2025년 04월 16일
- **내용**:
- `CategorySelectionField` 위젯의 오류 수정
- 타입 처리 개선 및 조건문 명확화
- 카테고리/서브카테고리 선택 로직 개선
## 대화 14: 프로젝트 완성 및 최종 점검
- **날짜**: 2025년 04월 16일
- **내용**:
- 모든 화면 구현 완료 확인:
- 장비 입고/출고 관리
- 회사 관리
- 사용자 관리
- 라이센스 관리
- 기능 점검:
- 각 화면 간 이동 및 데이터 전달
- 등록/수정/삭제 기능
- 유효성 검사
- UI/UX 최종 점검:
- 메트로닉 스타일 적용 확인
- 반응형 레이아웃 확인
- 사용성 검토
## 대화 15: Metronic 테일윈드 디자인 적용
- **날짜**: 2025년 04월 17일
- **내용**:
- Metronic Admin 테일윈드 버전 (데모6) 디자인 분석
- 테일윈드 CSS 기반으로 테마 시스템 재구성
- 새로운 테마 파일 생성: `theme_tailwind.dart`
- 기존 Material 테마에서 테일윈드 스타일로 변경
- Metronic의 색상 팔레트와 그림자 효과 적용
## 대화 16: 공통 레이아웃 컴포넌트 개발
- **날짜**: 2025년 04월 17일
- **내용**:
- Metronic 스타일의 레이아웃 컴포넌트 구현: `layout_components.dart`
- 공통 UI 컴포넌트 개발:
- `MetronicPageContainer`: 페이지 기본 레이아웃
- `MetronicCard`: 카드 컴포넌트
- `MetronicStatsCard`: 통계 카드 컴포넌트
- `MetronicPageTitle`: 페이지 제목 컴포넌트
- `MetronicDataTable`: 데이터 테이블 컴포넌트
- `MetronicFormField`: 폼 필드 래퍼 컴포넌트
- `MetronicTabContainer`: 탭 컨테이너 컴포넌트
- 전체 UI 일관성 향상을 위한 컴포넌트 표준화
## 대화 17: 오버뷰 대시보드 화면 구현
- **날짜**: 2025년 04월 17일
- **내용**:
- 오버뷰 화면 개발: `overview_screen.dart`
- 기능 구현:
- 환영 카드 섹션 개발
- 통계 카드 그리드 레이아웃 구현
- 시스템 활동 차트 영역 개발 (차트 플레이스홀더)
- 최근 활동, 알림, 예정된 작업 탭 구현
- 장비 입출고 통계 및 추이 표시
- Metronic 데모6 스타일 적용
- 홈 화면을 오버뷰 대시보드로 변경
## 대화 18: 메인 파일 업데이트 및 테마 적용
- **날짜**: 2025년 04월 17일
- **내용**:
- `main.dart` 파일 수정
- 테일윈드 테마 적용으로 변경: `AppThemeTailwind.lightTheme`
- 홈 화면을 기존 메뉴 그리드에서 오버뷰 대시보드로 변경
- 불필요한 HomeScreen 클래스 제거
- 라우트 설정 업데이트
- 모든 화면이 통일된 디자인 시스템 적용
## 대화 19: 좌측 사이드바 메뉴 구현 및 레이아웃 개선
- **날짜**: 2025년 04월 18일
- **내용**:
- Metronic 테일윈드 데모6 스타일의 사이드바 메뉴 구현
- 좌측에 메인 메뉴를 배치하는 레이아웃 구조 변경
- `SidebarMenu` 클래스 개발:
- 메뉴 계층 구조 지원 (접는/펼치는 기능)
- 활성 메뉴 시각적 표시
- 메뉴 항목별 아이콘 및 스타일 적용
- `MainLayout` 컴포넌트 구현:
- 좌측 사이드바와 우측 컨텐츠 영역 구성
- 커스텀 앱바 디자인 적용
- 모든 화면에 일관된 레이아웃 제공
- 오버뷰 화면을 새 레이아웃에 통합
- 메뉴 항목:
- 대시보드
- 장비 관리 (하위 메뉴: 장비 입고, 장비 출고)
- 회사 관리
- 사용자 관리
- 라이센스 관리
## 대화 20: 대시보드 인터페이스 개선
- **날짜**: 2025년 04월 19일
- **내용**:
- 대시보드 화면 UI 개선:
- 불필요한 환영 메시지 컴포넌트 제거
- 통계 카드 디자인 최적화
- MetronicStatsCard 컴포넌트 높이 축소 (여백 및 폰트 크기 조정)
- 통계 카드 레이아웃 비율 조정 (childAspectRatio 적용)
- 헤더 인터페이스 일관성 향상:
- 사이드바 헤더와 메인 헤더의 높이 일치 (72px로 통일)
- 브랜드 로고 및 앱 타이틀 정렬 개선
- 헤더 패딩 및 여백 최적화
- 전체적인 레이아웃 조화 개선
- 상단 네비게이션 영역 수직 정렬 통일
- 컴포넌트 간 일관된 간격 적용
## 대화 21: 대시보드 레이아웃 균형 개선
- **날짜**: 2025년 04월 19일
- **내용**:
- 대시보드 레이아웃의 일관성 향상:
- '시스템 활동'과 '최근 활동' 위젯 간의 간격을 표준화 (24px로 통일)
- 최근 활동 섹션의 불필요한 중첩 레이아웃 제거 및 단순화
- 위젯 간 상하 간격 일관성 확보로 시각적 조화 개선
- UI 요소 정리 및 최적화
- SizedBox 고정 높이 제한 제거로 컨텐츠에 따른 자연스러운 크기 조정
- 중복 컨테이너 래핑 최소화로 레이아웃 성능 향상
## 대화 22: 대시보드 UI 간격 표준화
- **날짜**: 2025년 04월 19일
- **내용**:
- 대시보드 화면의 모든 위젯 간 상하 간격 표준화:
- 주요 섹션 간 간격: 24px로 통일 (통계 그리드, 시스템 활동, 최근 활동)
- 섹션 내부 요소 간 간격: 16px로 통일
- 활동 차트 내부, 활동 레전드, 최근 활동 리스트 등의 간격 조정
- 컴포넌트 레이아웃 개선:
- 시스템 활동 카드에 하단 여백 추가
- 최근 활동 섹션에 상하 여백 추가
- 마지막 아이템 이후 불필요한 구분선 제거
- 전체적인 시각적 일관성 향상
- 모든 간격을 8px 단위의 배수로 설정 (8, 16, 24)
- 컴포넌트 내부 구성요소 간 관계성 강화
## 대화 23: 대시보드 UI 간격 미세 조정
- **날짜**: 2025년 04월 19일
- **내용**:
- 대시보드 화면의 동일한 위젯 간 간격 문제 해결:
- MetronicCard 컴포넌트에 내장된 하단 마진(bottom: 16) 제거
- 마진 속성을 컴포넌트 매개변수로 제공하여 외부에서 조정 가능하도록 개선
- 각 카드의 시각적 간격 정확히 통일 (24px)
- 컴포넌트 아키텍처 개선:
- 중첩 스타일 속성을 통일하여 일관된 시각적 경험 제공
- 레이아웃 컴포넌트의 유연성 향상
- 다양한 화면에서 재사용 가능한 컴포넌트 설계
## 대화 24: 일관된 인터페이스를 위한 사이드바 메뉴 통합
- **날짜**: 2025년 04월 20일
- **내용**:
- 모든 주요 화면에 사이드바 메뉴 적용:
- 장비 입고 관리 화면에 MainLayout 적용
- 장비 출고 관리 화면에 MainLayout 적용
- 회사 관리 화면에 MainLayout 적용
- 사용자 관리 화면에 MainLayout 적용
- 라이센스 관리 화면에 MainLayout 적용
- 레이아웃 구조 개선:
- 모든 화면에 일관된 MainLayout 컴포넌트 적용
- 페이지별 적절한 currentRoute 값 설정으로 사이드바 메뉴 활성화 상태 관리
- 기존 Scaffold와 AppBar를 MainLayout으로 대체
- 사용자 경험 향상:
- 모든 화면에서 일관된 내비게이션 경험 제공
- 새로고침 및 추가 기능 버튼 통일
- 플로팅 액션 버튼을 통한 추가 기능 접근성 개선

0
doc/refac.md Normal file
View File

340
doc/superportPRD.md Normal file
View File

@@ -0,0 +1,340 @@
**아래 내용 전체를 복사해서** `superportPRD.md` **파일로 저장하시면 됩니다.**
---
# supERPort ERP - 초기 프론트엔드 PRD (Flutter)
본 문서는 Flutter로 제작할 ERP 웹/앱 프론트엔드의 최소 기능 요구사항과 디렉터리 구조, 스타일 가이드, 그리고 AI 코드 생성에 활용 가능한 프롬프트를 포함하고 있습니다. 현재 계획은 확장 가능하며, 추후 대화와 요구사항 변경에 따라 자동으로 업데이트할 수 있습니다.
## 1. 개요
- **이름**: supERPort
- **기술 스택**: Flutter
- **주요 화면**:
1. 장비 입고
2. 장비 출고
3. 회사 등록
4. 사용자 등록
5. 라이센스 등록
## 2. 공통 스타일 가이드
- **디자인 레퍼런스**: [Metronic Admin Template](https://themeforest.net/item/metronic-responsive-admin-dashboard-template/4021469)
- **디자인 레퍼런스를 철저히 검토 및 적용**: Metronic의 Layout, 컬러 팔레트, 컴포넌트, 인터랙션 가이드라인을 꼼꼼하게 분석하여, Flutter 및 Material Icons와 조화롭게 적용해야 합니다.
- 하나의 화면에 중복되는 정보를 제공하지 않아야 합니다.
- **아이콘**: Material Icons
- **UI 기조**: 모던하고 직관적인 플랫 디자인, 적절한 그림자(Elevation)와 여백 사용
- **메인 컬러 예시**:
- Primary: #5867dd (Metronic 기본색)
- Secondary: #34bfa3
- Background: #f7f8fa
- **타이포그래피**: 가독성 높은 산세리프 폰트(예: Roboto, Noto Sans)
- **반응형 고려**: Web & Mobile 겸용, Responsive Layout 구성
## 3. 기능 요구사항
### 3.1 장비 입고 화면
- **등록 후 리스트로 관리**
- 새로운 장비 입고 기록을 추가 (Form)
- 등록된 목록을 리스트(테이블)로 보여주고, 항목별 Editing 기능 제공
- **필수 데이터**
1. **제조사명** (varchar 필수)
2. **장비명** (varchar 필수)
3. **대분류 / 중분류 / 소분류** (varchar, Email 선택 UI 유사 방식)
4. **시리얼 넘버** (varchar)
5. **바코드 입력** (varchar, 차후에 바코드 스캔 기능과 연동 예정)
6. **물품 수** (int 필수)
- 시리얼 번호가 존재할 경우 1 고정 및 수정 불가
- 시리얼 번호가 없으면 직접 입력
7. **입고일** (datetime)
- 당일 날짜를 기본값으로 설정
- 캘린더에서 과거 날짜만 선택 가능
8. (DB 연계 시) **장비(장비이력) 테이블**과 매핑
- **입출고**는 “I”(입고)로 기록
- **발생시간**: 입고 시점(= 입고일)
### 3.2 장비 출고 화면
- **등록 후 리스트로 관리**
- 새 출고 정보 등록 (Form)
- 출고 기록 목록 표시 및 편집 기능
- **필수 데이터**
1. **장비명** (varchar 필수)
2. **대분류 / 중분류 / 소분류** (출고 시점에 불러오고 수정 가능 여부는 정책 결정)
3. **시리얼 넘버** (varchar)
4. **바코드** (varchar, 바코드 스캔 기능 연동 예정)
5. **출고 수량** (int 필수)
6. **출고일** (datetime)
7. (DB 연계 시) **장비(장비이력) 테이블**과 매핑
- **입출고**는 “O”(출고)로 기록
- **발생시간**: 출고 시점(= 출고일)
### 3.3 회사 등록 화면
- **등록 후 리스트로 관리**
- 새 회사 정보 등록 (Form)
- 회사 목록 조회 및 편집
- **필수 데이터**
1. **회사명** (varchar 필수)
2. **주소** (varchar)
3. (확장) **지점 정보** (Optional / 별도 화면 구성 가능)
- 지점명 (varchar)
- 대표전화번호 (varchar)
- 주소 (varchar)
4. (DB 연계 시) **고객(회사) / 고객(지점)** 테이블 매핑
- 회사 ID, 지점 ID 등 Primary Key, FK 관계
### 3.4 사용자 등록 화면
- **등록 후 리스트로 관리**
- 사용자(직원) 가입/등록 Form
- 사용자 목록 조회 및 편집
- **필수 데이터**
1. **이름** (varchar 필수)
2. **소속 회사ID** (int)
3. **관리등급** (char) 예) S(관리자), M(멤버)
4. (DB 연계 시) **서비스(직원)** 테이블 매핑
### 3.5 라이센스 등록 화면
- **등록 후 리스트로 관리**
- 유지보수 라이센스 정보 등록 (Form)
- 등록된 라이센스 목록 표시 및 편집
- **필수 데이터**
1. **서비스(회사)ID** (int) 라이센스가 적용될 회사/서비스 정보
2. **라이센스명** (varchar, 예: “1년 유지보수”)
3. **라이센스기간** (int, 월 단위)
4. **방문주기** (int, 유지보수 방문 주기)
5. (DB 연계 시) **유지(라이센스)** 테이블과 매핑
## 4. 추천 디렉터리 구조
동일한 기능을 담은 모듈(파일)이 300라인을 초과하게 될 경우, **하위 파일로 분리**해 관리하는 것을 권장합니다.
장비(Equipment) 모델과 장비 입고(In) / 출고(Out) 모델을 **분리**하여, 공통 필드와 입·출고 전용 필드를 구분해 유지보수를 용이하게 합니다.
lib/
┣ models/
┃ ┣ equipment_model.dart (장비(Equipment) 공통 모델)
┃ ┣ equipment_in_model.dart (장비 입고 관련 모델, 장비 + 입고 필드)
┃ ┣ equipment_out_model.dart (장비 출고 관련 모델, 장비 + 출고 필드)
┃ ┣ company_model.dart (회사 정보 모델)
┃ ┣ user_model.dart (사용자 정보 모델)
┃ ┗ license_model.dart (유지보수 라이센스 정보 모델)
┣ screens/
┃ ┣ equipment_in/
┃ ┃ ┣ equipment_in_list.dart (장비 입고 리스트 화면)
┃ ┃ ┗ equipment_in_form.dart (장비 입고 등록/수정 폼)
┃ ┣ equipment_out/
┃ ┃ ┣ equipment_out_list.dart (장비 출고 리스트 화면)
┃ ┃ ┗ equipment_out_form.dart (장비 출고 등록/수정 폼)
┃ ┣ company/
┃ ┃ ┣ company_list.dart (회사 목록 화면)
┃ ┃ ┗ company_form.dart (회사 등록/수정 폼)
┃ ┣ user/
┃ ┃ ┣ user_list.dart (사용자 목록 화면)
┃ ┃ ┗ user_form.dart (사용자 등록/수정 폼)
┃ ┣ license/
┃ ┃ ┣ license_list.dart (라이센스 목록 화면)
┃ ┃ ┗ license_form.dart (라이센스 등록/수정 폼)
┃ ┗ common/
┃ ┣ custom_widgets.dart (공통 위젯)
┃ ┗ theme.dart (스타일, 테마 정보)
┣ services/
┃ ┗ mock_data_service.dart (서버 없는 샘플 데이터 관리)
┣ utils/
┃ ┣ validators.dart (입력값 검증 함수)
┃ ┗ constants.dart (상수 관리, 예: 라우트명, 컬러코드)
┗ main.dart
## 5. 데이터베이스 설계
아래는 장비, 회사(고객), 서비스(직원/결제), 유지보수 라이센스 등에 대한 **예시** 테이블입니다. 실제 구현 시에는 필요에 따라 테이블을 통합하거나 컬럼명을 조정할 수 있습니다.
### 5.1 고객 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|고객(회사)|ID|Integer|1|Primary Key|
||회사명|Varchar|LG전자|고객 회사명|
||주소|Varchar|서울시 종로구|고객 회사 주소|
|고객(지점)|ID|Integer|10|Primary Key|
||회사ID|Integer|1|FK - 고객(회사)|
||지점명|Varchar|본사|고객 지점명|
||주소|Varchar|서울시 종로구|지점 주소|
||대표전화번호|Varchar|02-3403-2222|지점 연락처|
### 5.2 서비스 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|서비스(직원)|ID|Integer|20|Primary Key|
||회사ID|Integer|1|FK - 서비스(회사) 또는 고객(회사)|
||이름|Varchar|홍길동|직원 이름|
||관리등급|Char|M|S(관리자)/M(멤버)|
|서비스(결제)|ID|Integer|30|Primary Key|
||서비스(직원)ID|Integer|20|FK - 서비스(직원)|
||결제여부|Char|A|승인(A)/반려(D)|
||결제일|Datetime|2025-03-04 11:00|결제 일시|
### 5.3 장비 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|장비(장비정보)|ID|Integer|50|Primary Key|
||장비(회사명)ID|Integer|1|FK - 장비(회사명) or 고객(회사)|
||장비명|Varchar|라우터 123|장비 모델명|
|장비(장비이력)|ID|Integer|60|Primary Key|
||장비(장비바코드)ID|Integer|50|FK - 장비(장비바코드)|
||입출고|Varchar|I|입고(I)/출고(O)/임대(R) 등 구분|
||발생시간|Datetime|2025-03-04 11:00|이력 발생 시간|
### 5.4 유지보수 라이센스 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|유지(라이센스)|ID|Integer|70|Primary Key|
||서비스(회사)ID|Integer|1|FK - 서비스(회사)|
||라이센스명|Varchar|1년 유지보수|유지보수 라이센스명|
||라이센스기간|Integer|12|월 단위|
||방문주기|Integer|1|유지보수 방문 주기|
> 위 테이블들은 예시이며, 실제 구현 시에는 **API 요청/응답 형식**과 **화면 요구사항**에 따라 컬럼을 추가/수정/제거할 수 있습니다.
## 6. 코드 생성용 AI 프롬프트 예시
(아래 텍스트는 코드 자동생성용 프롬프트 작성 예시일 뿐, 실제 환경에 맞춰 수정해서 사용하세요.)
[System]
You are Claude, a large language model trained by Anthropic.
[User]
read document "doc/doc name" at first.
- 앱 이름: supERPort
- 화면 개요: 장비 입고, 장비 출고, 회사 등록, 사용자 등록
- 참조 스타일: Metronic Admin + Material Icons
- 필수 폴더 구조와 기능
1. equipment_in (리스트 & 폼)
2. equipment_out (리스트 & 폼)
3. company (리스트 & 폼)
4. user (리스트 & 폼)
Generate Flutter code with the above requirements.
- Use a consistent coding style.
- Provide minimal working code example for each screen.
- Utilize Material Icons.
- Implement basic validation in the forms.
## 7. 추후 업데이트 사항
- 장비 출고 시 필수 데이터 상세
- 회사 등록/사용자 등록 시 필수 데이터 구조 정의 (지점 정보, 연락처 등 확장)
- Form 유효성 검사 규칙 세부화
- 권한(관리자/사용자)별 접근 제어
- 바코드 스캔 기능 연동
- API 연동 및 서버 연결
- **유지보수 라이센스** 관련 화면 및 기능 확대 (결제/계약 기간 연동 등)
---
본 문서는 ERP 플랫폼 “supERPort”의 Flutter 프론트엔드 개발에 필요한 **최소 요구사항**, **디렉터리 구조**, **스타일 가이드**, 그리고 **데이터베이스 설계** 정보를 담고 있습니다. 여기서 정의되지 않은 사항은 추후 대화에서 확정된 후 문서에 자동 반영될 예정입니다. 필요에 따라 Markdown 형식으로 다운로드해, 버전 관리 시스템에 추가하거나 직접 열람할 수 있습니다.

34
ios/.gitignore vendored Normal file
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

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>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
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

View File

@@ -0,0 +1,616 @@
// !$*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 */; };
/* 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>"; };
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; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; 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>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
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 */,
);
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>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
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 = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
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";
};
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";
};
/* 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.superport;
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;
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.superport.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;
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.superport.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;
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.superport.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 = 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;
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 = 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;
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.superport;
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.superport;
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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,99 @@
<?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"
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"
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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

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)
}
}

View File

@@ -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: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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

View File

@@ -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.

View File

@@ -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>

View File

@@ -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
ios/Runner/Info.plist Normal file
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>Superport</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>superport</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>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

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.
}
}

BIN
lib.zip Normal file

Binary file not shown.

Binary file not shown.

178
lib/main.dart Normal file
View File

@@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/screens/common/app_layout.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/company/company_form.dart';
import 'package:superport/screens/equipment/equipment_in_form.dart';
import 'package:superport/screens/equipment/equipment_out_form.dart';
import 'package:superport/screens/license/license_form.dart'; // MaintenanceFormScreen으로 사용
import 'package:superport/screens/user/user_form.dart';
import 'package:superport/screens/warehouse_location/warehouse_location_form.dart';
import 'package:superport/utils/constants.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:superport/screens/login/login_screen.dart';
void main() {
// MockDataService는 싱글톤으로 자동 초기화됨
runApp(const SuperportApp());
}
class SuperportApp extends StatelessWidget {
const SuperportApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'supERPort',
theme: AppThemeTailwind.lightTheme,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ko', 'KR'), Locale('en', 'US')],
locale: const Locale('ko', 'KR'),
initialRoute: '/login',
onGenerateRoute: (settings) {
// 로그인 라우트 처리
if (settings.name == '/login') {
return MaterialPageRoute(builder: (context) => const LoginScreen());
}
// 기본 AppLayout으로 라우팅할 경로 (홈, 목록 화면들)
if (settings.name == Routes.home ||
settings.name == Routes.equipment ||
settings.name == Routes.equipmentInList ||
settings.name == Routes.equipmentOutList ||
settings.name == Routes.equipmentRentList ||
settings.name == Routes.company ||
settings.name == Routes.user ||
settings.name == Routes.license) {
return MaterialPageRoute(
builder: (context) => AppLayout(initialRoute: settings.name!),
);
}
// 기존 라우팅 처리 (폼 화면들)
switch (settings.name) {
// 장비 입고 관련 라우트
case Routes.equipmentInAdd:
return MaterialPageRoute(
builder: (context) => const EquipmentInFormScreen(),
);
case Routes.equipmentInEdit:
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (context) => EquipmentInFormScreen(equipmentInId: id),
);
// 장비 출고 관련 라우트
case Routes.equipmentOutAdd:
// 선택된 장비 정보와 입고 ID가 전달되었는지 확인
final args = settings.arguments;
Equipment? equipment;
int? equipmentInId;
List<Map<String, dynamic>>? selectedEquipments;
// 인자 처리
if (args is Map<String, dynamic>) {
// 다중 선택 장비 처리
if (args.containsKey('selectedEquipments')) {
selectedEquipments =
args['selectedEquipments'] as List<Map<String, dynamic>>;
debugPrint('선택된 장비 목록: ${selectedEquipments.length}');
} else {
// 단일 장비 선택 (기존 방식)
equipment = args['equipment'] as Equipment?;
equipmentInId = args['equipmentInId'] as int?;
debugPrint('단일 장비 선택');
}
} else if (args is List<Map<String, dynamic>>) {
// 직접 리스트가 전달된 경우
selectedEquipments = args;
debugPrint('직접 리스트로 전달된 장비 목록: ${selectedEquipments.length}');
} else if (args is Equipment) {
equipment = args; // 기존 방식 대응 (하위 호환)
debugPrint('단일 Equipment 객체 전달');
} else {
debugPrint('알 수 없는 인자 타입: ${args.runtimeType}');
}
return MaterialPageRoute(
builder:
(context) => EquipmentOutFormScreen(
selectedEquipment: equipment,
selectedEquipmentInId: equipmentInId,
selectedEquipments: selectedEquipments,
),
);
case Routes.equipmentOutEdit:
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (context) => EquipmentOutFormScreen(equipmentOutId: id),
);
// 회사 관련 라우트
case Routes.companyAdd:
return MaterialPageRoute(
builder: (context) => const CompanyFormScreen(),
);
case Routes.companyEdit:
final args = settings.arguments;
if (args is Map) {
return MaterialPageRoute(
builder: (context) => CompanyFormScreen(args: args),
);
} else if (args is int) {
// 하위 호환: int만 넘어오는 경우
return MaterialPageRoute(
builder:
(context) => CompanyFormScreen(args: {'companyId': args}),
);
} else {
return MaterialPageRoute(
builder: (context) => CompanyFormScreen(),
);
}
// 사용자 관련 라우트
case Routes.userAdd:
return MaterialPageRoute(
builder: (context) => const UserFormScreen(),
);
case Routes.userEdit:
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (context) => UserFormScreen(userId: id),
);
// 라이센스 관련 라우트
case Routes.licenseAdd:
return MaterialPageRoute(
builder: (context) => const MaintenanceFormScreen(),
);
case Routes.licenseEdit:
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (context) => MaintenanceFormScreen(maintenanceId: id),
);
// 입고지 관련 라우트
case Routes.warehouseLocationAdd:
return MaterialPageRoute(
builder: (context) => const WarehouseLocationFormScreen(),
);
case Routes.warehouseLocationEdit:
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (context) => WarehouseLocationFormScreen(id: id),
);
default:
return MaterialPageRoute(
builder: (context) => AppLayout(initialRoute: Routes.home),
);
}
},
);
}
}

View File

@@ -0,0 +1,81 @@
/// 주소 모델
///
/// 우편번호, 시/도, 상세주소로 구성된 주소 정보를 관리합니다.
/// 회사 및 지점의 주소 정보를 일관되게 처리하기 위한 모델입니다.
class Address {
/// 우편번호
final String zipCode;
/// 시/도 (서울특별시, 경기도 등)
final String region;
/// 상세 주소
final String detailAddress;
/// 생성자
const Address({this.zipCode = '', this.region = '', this.detailAddress = ''});
/// 주소를 문자열로 반환합니다. (전체 주소)
///
/// 예시: "12345 서울특별시 강남구 테헤란로 123"
@override
String toString() {
final List<String> parts = [];
if (zipCode.isNotEmpty) {
parts.add(zipCode);
}
if (region.isNotEmpty) {
parts.add(region);
}
if (detailAddress.isNotEmpty) {
parts.add(detailAddress);
}
return parts.join(' ');
}
/// 전체 주소에서 Address 객체를 생성합니다.
///
/// 현재는 우편번호, 시/도, 상세주소를 분리하지 않고 전체를 detailAddress로 저장합니다.
/// 기존 데이터 마이그레이션을 위한 메서드입니다.
factory Address.fromFullAddress(String fullAddress) {
return Address(detailAddress: fullAddress);
}
/// JSON에서 Address 객체를 생성합니다.
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
zipCode: json['zipCode'] ?? '',
region: json['region'] ?? '',
detailAddress: json['detailAddress'] ?? '',
);
}
/// Address 객체를 JSON으로 변환합니다.
Map<String, dynamic> toJson() {
return {
'zipCode': zipCode,
'region': region,
'detailAddress': detailAddress,
};
}
/// 주소가 비어 있는지 확인합니다.
bool get isEmpty =>
zipCode.isEmpty && region.isEmpty && detailAddress.isEmpty;
/// 주소가 비어 있지 않은지 확인합니다.
bool get isNotEmpty => !isEmpty;
/// 복사본을 생성하고 일부 필드를 업데이트합니다.
Address copyWith({String? zipCode, String? region, String? detailAddress}) {
return Address(
zipCode: zipCode ?? this.zipCode,
region: region ?? this.region,
detailAddress: detailAddress ?? this.detailAddress,
);
}
}

View File

@@ -0,0 +1,259 @@
import 'package:superport/models/address_model.dart';
/// 회사 유형 열거형
/// - 고객사: 서비스를 이용하는 회사
/// - 파트너사: 서비스를 제공하는 회사
enum CompanyType {
customer, // 고객사
partner, // 파트너사
}
/// 회사 유형을 문자열로 변환 (복수 지원)
String companyTypeToString(CompanyType type) {
switch (type) {
case CompanyType.customer:
return '고객사';
case CompanyType.partner:
return '파트너사';
}
}
/// 문자열에서 회사 유형으로 변환 (단일)
CompanyType stringToCompanyType(String type) {
switch (type) {
case '고객사':
return CompanyType.customer;
case '파트너사':
return CompanyType.partner;
default:
return CompanyType.customer; // 기본값은 고객사
}
}
/// 문자열 리스트에서 회사 유형 리스트로 변환
List<CompanyType> stringListToCompanyTypeList(List<dynamic> types) {
// 문자열 또는 enum 문자열이 섞여 있을 수 있음
return types.map((e) {
if (e is CompanyType) return e;
if (e is String) {
if (e.contains('partner')) return CompanyType.partner;
return CompanyType.customer;
}
return CompanyType.customer;
}).toList();
}
/// 회사 유형 리스트를 문자열 리스트로 변환
List<String> companyTypeListToStringList(List<CompanyType> types) {
return types.map((e) => companyTypeToString(e)).toList();
}
class Branch {
final int? id;
final int companyId;
final String name;
final Address address; // 주소 모델 사용
final String? contactName; // 담당자 이름
final String? contactPosition; // 담당자 직책
final String? contactPhone; // 담당자 전화번호
final String? contactEmail; // 담당자 이메일
final String? remark; // 비고
Branch({
this.id,
required this.companyId,
required this.name,
Address? address, // 옵셔널 파라미터로 변경
this.contactName,
this.contactPosition,
this.contactPhone,
this.contactEmail,
this.remark,
}) : address = address ?? const Address(); // 기본값 제공
Map<String, dynamic> toJson() {
return {
'id': id,
'companyId': companyId,
'name': name,
'address': address.toString(), // 하위 호환성을 위해 문자열로 변환
'addressData': address.toJson(), // 새로운 형식으로 저장
'contactName': contactName,
'contactPosition': contactPosition,
'contactPhone': contactPhone,
'contactEmail': contactEmail,
'remark': remark,
};
}
factory Branch.fromJson(Map<String, dynamic> json) {
// 주소 데이터가 새 형식으로 저장되어 있는지 확인
Address addressData;
if (json.containsKey('addressData')) {
addressData = Address.fromJson(json['addressData']);
} else if (json.containsKey('address') && json['address'] != null) {
// 이전 버전 호환성 - 문자열 주소를 Address 객체로 변환
addressData = Address.fromFullAddress(json['address']);
} else {
addressData = const Address();
}
return Branch(
id: json['id'],
companyId: json['companyId'],
name: json['name'],
address: addressData,
contactName: json['contactName'],
contactPosition: json['contactPosition'],
contactPhone: json['contactPhone'],
contactEmail: json['contactEmail'],
remark: json['remark'],
);
}
/// 복사본을 생성하고 일부 필드를 업데이트합니다.
Branch copyWith({
int? id,
int? companyId,
String? name,
Address? address,
String? contactName,
String? contactPosition,
String? contactPhone,
String? contactEmail,
String? remark,
}) {
return Branch(
id: id ?? this.id,
companyId: companyId ?? this.companyId,
name: name ?? this.name,
address: address ?? this.address,
contactName: contactName ?? this.contactName,
contactPosition: contactPosition ?? this.contactPosition,
contactPhone: contactPhone ?? this.contactPhone,
contactEmail: contactEmail ?? this.contactEmail,
remark: remark ?? this.remark,
);
}
}
class Company {
final int? id;
final String name;
final Address address; // 주소 모델 사용
final String? contactName; // 담당자 이름
final String? contactPosition; // 담당자 직책
final String? contactPhone; // 담당자 전화번호
final String? contactEmail; // 담당자 이메일
final List<Branch>? branches;
final List<CompanyType> companyTypes; // 회사 유형 (복수 가능)
final String? remark; // 비고
Company({
this.id,
required this.name,
Address? address, // 옵셔널 파라미터로 변경
this.contactName,
this.contactPosition,
this.contactPhone,
this.contactEmail,
this.branches,
this.companyTypes = const [CompanyType.customer], // 기본값은 고객사
this.remark,
}) : address = address ?? const Address(); // 기본값 제공
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'address': address.toString(), // 하위 호환성을 위해 문자열로 변환
'addressData': address.toJson(), // 새로운 형식으로 저장
'contactName': contactName,
'contactPosition': contactPosition,
'contactPhone': contactPhone,
'contactEmail': contactEmail,
'branches': branches?.map((branch) => branch.toJson()).toList(),
// 회사 유형을 문자열 리스트로 저장
'companyTypes': companyTypes.map((e) => e.toString()).toList(),
'remark': remark,
};
}
factory Company.fromJson(Map<String, dynamic> json) {
List<Branch>? branchList;
if (json['branches'] != null) {
branchList =
(json['branches'] as List)
.map((branchJson) => Branch.fromJson(branchJson))
.toList();
}
// 주소 데이터가 새 형식으로 저장되어 있는지 확인
Address addressData;
if (json.containsKey('addressData')) {
addressData = Address.fromJson(json['addressData']);
} else if (json.containsKey('address') && json['address'] != null) {
// 이전 버전 호환성 - 문자열 주소를 Address 객체로 변환
addressData = Address.fromFullAddress(json['address']);
} else {
addressData = const Address();
}
// 회사 유형 파싱 (복수)
List<CompanyType> types = [CompanyType.customer]; // 기본값
if (json.containsKey('companyTypes')) {
final raw = json['companyTypes'];
if (raw is List) {
types = stringListToCompanyTypeList(raw);
}
} else if (json.containsKey('companyType')) {
// 이전 버전 호환성: 단일 값
final raw = json['companyType'];
if (raw is String) {
types = [stringToCompanyType(raw)];
} else if (raw is int) {
types = [CompanyType.values[raw]];
}
}
return Company(
id: json['id'],
name: json['name'],
address: addressData,
contactName: json['contactName'],
contactPosition: json['contactPosition'],
contactPhone: json['contactPhone'],
contactEmail: json['contactEmail'],
branches: branchList,
companyTypes: types,
remark: json['remark'],
);
}
/// 복사본을 생성하고 일부 필드를 업데이트합니다.
Company copyWith({
int? id,
String? name,
Address? address,
String? contactName,
String? contactPosition,
String? contactPhone,
String? contactEmail,
List<Branch>? branches,
List<CompanyType>? companyTypes,
String? remark,
}) {
return Company(
id: id ?? this.id,
name: name ?? this.name,
address: address ?? this.address,
contactName: contactName ?? this.contactName,
contactPosition: contactPosition ?? this.contactPosition,
contactPhone: contactPhone ?? this.contactPhone,
contactEmail: contactEmail ?? this.contactEmail,
branches: branches ?? this.branches,
companyTypes: companyTypes ?? this.companyTypes,
remark: remark ?? this.remark,
);
}
}

View File

@@ -0,0 +1,278 @@
import 'package:superport/utils/constants.dart';
// 장비 정보 모델
class Equipment {
final int? id;
final String manufacturer;
final String name;
final String category;
final String subCategory;
final String subSubCategory;
final String? serialNumber;
final String? barcode;
final int quantity;
final DateTime? inDate;
final String? remark; // 비고
final String? warrantyLicense; // 워런티 라이센스 명
DateTime? warrantyStartDate; // 워런티 시작일(수정 가능)
DateTime? warrantyEndDate; // 워런티 종료일(수정 가능)
Equipment({
this.id,
required this.manufacturer,
required this.name,
required this.category,
required this.subCategory,
required this.subSubCategory,
this.serialNumber,
this.barcode,
required this.quantity,
this.inDate,
this.remark,
this.warrantyLicense,
this.warrantyStartDate,
this.warrantyEndDate,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'manufacturer': manufacturer,
'name': name,
'category': category,
'subCategory': subCategory,
'subSubCategory': subSubCategory,
'serialNumber': serialNumber,
'barcode': barcode,
'quantity': quantity,
'inDate': inDate?.toIso8601String(),
'remark': remark,
'warrantyLicense': warrantyLicense,
'warrantyStartDate': warrantyStartDate?.toIso8601String(),
'warrantyEndDate': warrantyEndDate?.toIso8601String(),
};
}
factory Equipment.fromJson(Map<String, dynamic> json) {
return Equipment(
id: json['id'],
manufacturer: json['manufacturer'],
name: json['name'],
category: json['category'],
subCategory: json['subCategory'],
subSubCategory: json['subSubCategory'],
serialNumber: json['serialNumber'],
barcode: json['barcode'],
quantity: json['quantity'],
inDate: json['inDate'] != null ? DateTime.parse(json['inDate']) : null,
remark: json['remark'],
warrantyLicense: json['warrantyLicense'],
warrantyStartDate:
json['warrantyStartDate'] != null
? DateTime.parse(json['warrantyStartDate'])
: null,
warrantyEndDate:
json['warrantyEndDate'] != null
? DateTime.parse(json['warrantyEndDate'])
: null,
);
}
}
class EquipmentIn {
final int? id;
final Equipment equipment;
final DateTime inDate;
final String status; // I (입고)
final String type; // 장비 유형: '신제품', '중고', '계약'
final String? warehouseLocation; // 입고지
final String? partnerCompany; // 파트너사
final String? remark; // 비고
EquipmentIn({
this.id,
required this.equipment,
required this.inDate,
this.status = 'I',
this.type = EquipmentType.new_, // 기본값은 '신제품'으로 설정
this.warehouseLocation,
this.partnerCompany,
this.remark,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'equipment': equipment.toJson(),
'inDate': inDate.toIso8601String(),
'status': status,
'type': type,
'warehouseLocation': warehouseLocation,
'partnerCompany': partnerCompany,
'remark': remark,
};
}
factory EquipmentIn.fromJson(Map<String, dynamic> json) {
return EquipmentIn(
id: json['id'],
equipment: Equipment.fromJson(json['equipment']),
inDate: DateTime.parse(json['inDate']),
status: json['status'],
type: json['type'] ?? EquipmentType.new_,
warehouseLocation: json['warehouseLocation'],
partnerCompany: json['partnerCompany'],
remark: json['remark'],
);
}
}
class EquipmentOut {
final int? id;
final Equipment equipment;
final DateTime outDate;
final String status; // O (출고), I (재입고), R (수리)
final String? company; // 출고 회사
final String? manager; // 담당자
final String? license; // 라이센스
final DateTime? returnDate; // 재입고/수리 날짜
final String? returnType; // 재입고/수리 유형
final String? remark; // 비고
EquipmentOut({
this.id,
required this.equipment,
required this.outDate,
this.status = 'O',
this.company,
this.manager,
this.license,
this.returnDate,
this.returnType,
this.remark,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'equipment': equipment.toJson(),
'outDate': outDate.toIso8601String(),
'status': status,
'company': company,
'manager': manager,
'license': license,
'returnDate': returnDate?.toIso8601String(),
'returnType': returnType,
'remark': remark,
};
}
factory EquipmentOut.fromJson(Map<String, dynamic> json) {
return EquipmentOut(
id: json['id'],
equipment: Equipment.fromJson(json['equipment']),
outDate: DateTime.parse(json['outDate']),
status: json['status'],
company: json['company'],
manager: json['manager'],
license: json['license'],
returnDate:
json['returnDate'] != null
? DateTime.parse(json['returnDate'])
: null,
returnType: json['returnType'],
remark: json['remark'],
);
}
}
class UnifiedEquipment {
final int? id;
final Equipment equipment;
final DateTime date; // 입고일 또는 출고일
final String
status; // 상태 코드: 'I'(입고), 'O'(출고), 'R'(수리중), 'D'(손상), 'L'(분실), 'E'(기타)
final String? notes; // 추가 비고
final String? _type; // 내부용: 입고 장비 유형
UnifiedEquipment({
this.id,
required this.equipment,
required this.date,
required this.status,
this.notes,
String? type,
}) : _type = type;
// 장비 유형 반환 (입고 장비만)
String? get type => status == 'I' ? _type : null;
// 장비 상태 텍스트 변환
String get statusText {
switch (status) {
case EquipmentStatus.in_:
return '입고';
case EquipmentStatus.out:
return '출고';
case EquipmentStatus.rent:
return '대여';
case EquipmentStatus.repair:
return '수리중';
case EquipmentStatus.damaged:
return '손상';
case EquipmentStatus.lost:
return '분실';
case EquipmentStatus.etc:
return '기타';
default:
return '알 수 없음';
}
}
// EquipmentIn 모델에서 변환
factory UnifiedEquipment.fromEquipmentIn(
id,
equipment,
inDate,
status, {
String? type,
}) {
return UnifiedEquipment(
id: id,
equipment: equipment,
date: inDate,
status: status,
type: type,
);
}
// EquipmentOut 모델에서 변환
factory UnifiedEquipment.fromEquipmentOut(id, equipment, outDate, status) {
return UnifiedEquipment(
id: id,
equipment: equipment,
date: outDate,
status: status,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'equipment': equipment.toJson(),
'date': date.toIso8601String(),
'status': status,
'notes': notes,
};
}
factory UnifiedEquipment.fromJson(Map<String, dynamic> json) {
return UnifiedEquipment(
id: json['id'],
equipment: Equipment.fromJson(json['equipment']),
date: DateTime.parse(json['date']),
status: json['status'],
notes: json['notes'],
);
}
}

View File

@@ -0,0 +1,35 @@
class License {
final int? id;
final int companyId;
final String name;
final int durationMonths;
final String visitCycle; // 방문주기(월, 격월, 분기 등)
License({
this.id,
required this.companyId,
required this.name,
required this.durationMonths,
required this.visitCycle,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'companyId': companyId,
'name': name,
'durationMonths': durationMonths,
'visitCycle': visitCycle,
};
}
factory License.fromJson(Map<String, dynamic> json) {
return License(
id: json['id'],
companyId: json['companyId'],
name: json['name'],
durationMonths: json['durationMonths'],
visitCycle: json['visitCycle'] as String,
);
}
}

View File

@@ -0,0 +1,50 @@
class User {
final int? id;
final int companyId;
final int? branchId; // 지점 ID
final String name;
final String role; // 관리등급: S(관리자), M(멤버)
final String? position; // 직급
final String? email; // 이메일
final List<Map<String, String>> phoneNumbers; // 전화번호 목록 (유형과 번호)
User({
this.id,
required this.companyId,
this.branchId,
required this.name,
required this.role,
this.position,
this.email,
this.phoneNumbers = const [],
});
Map<String, dynamic> toJson() {
return {
'id': id,
'companyId': companyId,
'branchId': branchId,
'name': name,
'role': role,
'position': position,
'email': email,
'phoneNumbers': phoneNumbers,
};
}
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
companyId: json['companyId'],
branchId: json['branchId'],
name: json['name'],
role: json['role'],
position: json['position'],
email: json['email'],
phoneNumbers:
json['phoneNumbers'] != null
? List<Map<String, String>>.from(json['phoneNumbers'])
: [],
);
}
}

View File

@@ -0,0 +1,19 @@
// 전화번호 입력 필드 관리를 위한 클래스
// 타입 안정성 및 코드 간결성을 위해 사용
import 'package:flutter/material.dart';
class UserPhoneField {
// 전화번호 종류(휴대폰, 사무실 등)
String type;
// 전화번호 입력 컨트롤러
final TextEditingController controller;
UserPhoneField({required this.type, String? initialValue})
: controller = TextEditingController(text: initialValue);
// 현재 입력된 전화번호 반환
String get number => controller.text;
// 컨트롤러 해제
void dispose() => controller.dispose();
}

View File

@@ -0,0 +1,38 @@
import 'address_model.dart';
/// 입고지 정보를 나타내는 모델 클래스
class WarehouseLocation {
/// 입고지 고유 번호
final int id;
/// 입고지명
final String name;
/// 입고지 주소
final Address address;
/// 비고
final String? remark;
WarehouseLocation({
required this.id,
required this.name,
required this.address,
this.remark,
});
/// 복사본 생성 (불변성 유지)
WarehouseLocation copyWith({
int? id,
String? name,
Address? address,
String? remark,
}) {
return WarehouseLocation(
id: id ?? this.id,
name: name ?? this.name,
address: address ?? this.address,
remark: remark ?? this.remark,
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/sidebar/sidebar_screen.dart';
import 'package:superport/screens/overview/overview_screen.dart';
import 'package:superport/screens/equipment/equipment_list.dart';
import 'package:superport/screens/company/company_list.dart';
import 'package:superport/screens/user/user_list.dart';
import 'package:superport/screens/license/license_list.dart';
import 'package:superport/screens/warehouse_location/warehouse_location_list.dart';
import 'package:superport/screens/goods/goods_list.dart';
import 'package:superport/utils/constants.dart';
/// SPA 스타일의 앱 레이아웃 클래스
/// 사이드바는 고정되고 내용만 변경되는 구조를 제공
class AppLayout extends StatefulWidget {
final String initialRoute;
const AppLayout({Key? key, this.initialRoute = Routes.home})
: super(key: key);
@override
_AppLayoutState createState() => _AppLayoutState();
}
class _AppLayoutState extends State<AppLayout> {
late String _currentRoute;
@override
void initState() {
super.initState();
_currentRoute = widget.initialRoute;
}
/// 현재 경로에 따라 적절한 컨텐츠 섹션을 반환
Widget _getContentForRoute(String route) {
switch (route) {
case Routes.home:
return const OverviewScreen();
case Routes.equipment:
case Routes.equipmentInList:
case Routes.equipmentOutList:
case Routes.equipmentRentList:
// 장비 목록 화면에 현재 라우트 정보를 전달
return EquipmentListScreen(currentRoute: route);
case Routes.goods:
return const GoodsListScreen();
case Routes.company:
return const CompanyListScreen();
case Routes.license:
return const MaintenanceListScreen();
case Routes.warehouseLocation:
return const WarehouseLocationListScreen();
default:
return const OverviewScreen();
}
}
/// 경로 변경 메서드
void _navigateTo(String route) {
setState(() {
_currentRoute = route;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
// 왼쪽 사이드바
SizedBox(
width: 280,
child: SidebarMenu(
currentRoute: _currentRoute,
onRouteChanged: _navigateTo,
),
),
// 오른쪽 컨텐츠 영역
Expanded(child: _getContentForRoute(_currentRoute)),
],
),
);
}
}

View File

@@ -0,0 +1,8 @@
export 'custom_widgets/page_title.dart';
export 'custom_widgets/data_table_card.dart';
export 'custom_widgets/form_field_wrapper.dart';
export 'custom_widgets/date_picker_field.dart';
export 'custom_widgets/highlight_text.dart';
export 'custom_widgets/autocomplete_dropdown.dart';
export 'custom_widgets/category_selection_field.dart';
export 'custom_widgets/category_data.dart';

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'highlight_text.dart';
// 자동완성 드롭다운 공통 위젯
class AutocompleteDropdown extends StatelessWidget {
// 드롭다운에 표시할 항목 리스트
final List<String> items;
// 현재 입력된 텍스트(하이라이트 기준)
final String inputText;
// 항목 선택 시 콜백
final void Function(String) onSelect;
// 드롭다운 표시 여부
final bool showDropdown;
// 최대 높이(항목 개수에 따라 자동 조절)
final double maxHeight;
// 드롭다운이 비었을 때 표시할 위젯
final Widget emptyWidget;
const AutocompleteDropdown({
Key? key,
required this.items,
required this.inputText,
required this.onSelect,
required this.showDropdown,
this.maxHeight = 200,
this.emptyWidget = const Padding(
padding: EdgeInsets.all(12.0),
child: Text('검색 결과가 없습니다'),
),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height:
showDropdown
? (items.length > 4 ? maxHeight : items.length * 50.0)
: 0,
margin: EdgeInsets.only(top: showDropdown ? 4 : 0),
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: GestureDetector(
onTap: () {}, // 이벤트 버블링 방지
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(77),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child:
items.isEmpty
? emptyWidget
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: items.length,
separatorBuilder:
(context, index) =>
Divider(height: 1, color: Colors.grey.shade200),
itemBuilder: (context, index) {
final String item = items[index];
return ListTile(
dense: true,
title: HighlightText(
text: item,
highlight: inputText,
highlightColor: Theme.of(context).primaryColor,
),
onTap: () => onSelect(item),
);
},
),
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
// 카테고리 데이터 (예시)
final Map<String, Map<String, List<String>>> categoryData = {
'컴퓨터': {
'데스크탑': ['사무용', '게이밍', '워크스테이션'],
'노트북': ['사무용', '게이밍', '울트라북'],
'태블릿': ['안드로이드', 'iOS', '윈도우'],
},
'네트워크': {
'라우터': ['가정용', '기업용', '산업용'],
'스위치': ['관리형', '비관리형'],
'액세스 포인트': ['실내용', '실외용'],
},
'주변기기': {
'모니터': ['LCD', 'LED', 'OLED'],
'키보드': ['유선', '무선', '기계식'],
'마우스': ['유선', '무선', '트랙볼'],
},
};

View File

@@ -0,0 +1,562 @@
import 'package:flutter/material.dart';
import 'autocomplete_dropdown.dart';
import 'form_field_wrapper.dart';
import 'category_data.dart';
// 카테고리 선택 필드 (대분류/중분류/소분류)
class CategorySelectionField extends StatefulWidget {
final String category;
final String subCategory;
final String subSubCategory;
final Function(String, String, String) onCategoryChanged;
final bool isRequired;
const CategorySelectionField({
Key? key,
required this.category,
required this.subCategory,
required this.subSubCategory,
required this.onCategoryChanged,
this.isRequired = false,
}) : super(key: key);
@override
State<CategorySelectionField> createState() => _CategorySelectionFieldState();
}
class _CategorySelectionFieldState extends State<CategorySelectionField> {
// 검색 관련 컨트롤러 및 상태 변수
final TextEditingController _categoryController = TextEditingController();
final FocusNode _categoryFocusNode = FocusNode();
bool _showCategoryDropdown = false;
List<String> _filteredCategories = [];
// 중분류 관련 변수
final TextEditingController _subCategoryController = TextEditingController();
final FocusNode _subCategoryFocusNode = FocusNode();
bool _showSubCategoryDropdown = false;
List<String> _filteredSubCategories = [];
// 소분류 관련 변수
final TextEditingController _subSubCategoryController =
TextEditingController();
final FocusNode _subSubCategoryFocusNode = FocusNode();
bool _showSubSubCategoryDropdown = false;
List<String> _filteredSubSubCategories = [];
List<String> _allCategories = [];
String _selectedCategory = '';
String _selectedSubCategory = '';
String _selectedSubSubCategory = '';
@override
void initState() {
super.initState();
_selectedCategory = widget.category;
_selectedSubCategory = widget.subCategory;
_selectedSubSubCategory = widget.subSubCategory;
_categoryController.text = _selectedCategory;
_subCategoryController.text = _selectedSubCategory;
_subSubCategoryController.text = _selectedSubSubCategory;
// 모든 카테고리 목록 초기화
_allCategories = categoryData.keys.toList();
_filteredCategories = List.from(_allCategories);
// 중분류 목록 초기화
_updateSubCategories();
// 소분류 목록 초기화
_updateSubSubCategories();
// 대분류 컨트롤러 리스너 설정
_categoryController.addListener(_onCategoryTextChanged);
_categoryFocusNode.addListener(() {
setState(() {
if (_categoryFocusNode.hasFocus) {
_showCategoryDropdown = _filteredCategories.isNotEmpty;
} else {
_showCategoryDropdown = false;
}
});
});
// 중분류 컨트롤러 리스너 설정
_subCategoryController.addListener(_onSubCategoryTextChanged);
_subCategoryFocusNode.addListener(() {
setState(() {
if (_subCategoryFocusNode.hasFocus) {
_showSubCategoryDropdown = _filteredSubCategories.isNotEmpty;
} else {
_showSubCategoryDropdown = false;
}
});
});
// 소분류 컨트롤러 리스너 설정
_subSubCategoryController.addListener(_onSubSubCategoryTextChanged);
_subSubCategoryFocusNode.addListener(() {
setState(() {
if (_subSubCategoryFocusNode.hasFocus) {
_showSubSubCategoryDropdown = _filteredSubSubCategories.isNotEmpty;
} else {
_showSubSubCategoryDropdown = false;
}
});
});
}
@override
void dispose() {
_categoryController.dispose();
_categoryFocusNode.dispose();
_subCategoryController.dispose();
_subCategoryFocusNode.dispose();
_subSubCategoryController.dispose();
_subSubCategoryFocusNode.dispose();
super.dispose();
}
// 중분류 목록 업데이트
void _updateSubCategories() {
if (_selectedCategory.isNotEmpty) {
final subCategories =
categoryData[_selectedCategory]?.keys.toList() ?? [];
_filteredSubCategories = List.from(subCategories);
} else {
_filteredSubCategories = [];
}
}
// 소분류 목록 업데이트
void _updateSubSubCategories() {
if (_selectedCategory.isNotEmpty && _selectedSubCategory.isNotEmpty) {
final subSubCategories =
categoryData[_selectedCategory]?[_selectedSubCategory] ?? [];
_filteredSubSubCategories = List.from(subSubCategories);
} else {
_filteredSubSubCategories = [];
}
}
void _onCategoryTextChanged() {
final text = _categoryController.text;
setState(() {
_selectedCategory = text;
if (text.isEmpty) {
_filteredCategories = List.from(_allCategories);
} else {
_filteredCategories =
_allCategories
.where(
(item) => item.toLowerCase().contains(text.toLowerCase()),
)
.toList();
// 시작 부분이 일치하는 항목 우선 정렬
_filteredCategories.sort((a, b) {
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return a.compareTo(b);
});
}
_showCategoryDropdown =
_filteredCategories.isNotEmpty && _categoryFocusNode.hasFocus;
// 카테고리가 변경되면 하위 카테고리 초기화
if (_selectedCategory != widget.category) {
_selectedSubCategory = '';
_subCategoryController.text = '';
_selectedSubSubCategory = '';
_subSubCategoryController.text = '';
_updateSubCategories();
_updateSubSubCategories();
}
// 콜백 호출
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
_selectedSubSubCategory,
);
});
}
// 중분류 텍스트 변경 핸들러
void _onSubCategoryTextChanged() {
final text = _subCategoryController.text;
setState(() {
_selectedSubCategory = text;
if (_selectedCategory.isNotEmpty) {
final subCategories =
categoryData[_selectedCategory]?.keys.toList() ?? [];
if (text.isEmpty) {
_filteredSubCategories = List.from(subCategories);
} else {
_filteredSubCategories =
subCategories
.where(
(item) => item.toLowerCase().contains(text.toLowerCase()),
)
.toList();
// 시작 부분이 일치하는 항목 우선 정렬
_filteredSubCategories.sort((a, b) {
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return a.compareTo(b);
});
}
} else {
_filteredSubCategories = [];
}
_showSubCategoryDropdown =
_filteredSubCategories.isNotEmpty && _subCategoryFocusNode.hasFocus;
// 중분류가 변경되면 소분류 초기화
if (_selectedSubCategory != widget.subCategory) {
_selectedSubSubCategory = '';
_subSubCategoryController.text = '';
_updateSubSubCategories();
}
// 콜백 호출
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
_selectedSubSubCategory,
);
});
}
// 소분류 텍스트 변경 핸들러
void _onSubSubCategoryTextChanged() {
final text = _subSubCategoryController.text;
setState(() {
_selectedSubSubCategory = text;
if (_selectedCategory.isNotEmpty && _selectedSubCategory.isNotEmpty) {
final subSubCategories =
categoryData[_selectedCategory]?[_selectedSubCategory] ?? [];
if (text.isEmpty) {
_filteredSubSubCategories = List.from(subSubCategories);
} else {
_filteredSubSubCategories =
subSubCategories
.where(
(item) => item.toLowerCase().contains(text.toLowerCase()),
)
.toList();
// 시작 부분이 일치하는 항목 우선 정렬
_filteredSubSubCategories.sort((a, b) {
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return a.compareTo(b);
});
}
} else {
_filteredSubSubCategories = [];
}
_showSubSubCategoryDropdown =
_filteredSubSubCategories.isNotEmpty &&
_subSubCategoryFocusNode.hasFocus;
// 콜백 호출
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
_selectedSubSubCategory,
);
});
}
void _selectCategory(String category) {
setState(() {
_selectedCategory = category;
_categoryController.text = category;
_showCategoryDropdown = false;
_selectedSubCategory = '';
_subCategoryController.text = '';
_selectedSubSubCategory = '';
_subSubCategoryController.text = '';
_updateSubCategories();
_updateSubSubCategories();
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
_selectedSubSubCategory,
);
});
}
// 중분류 선택 핸들러
void _selectSubCategory(String subCategory) {
setState(() {
_selectedSubCategory = subCategory;
_subCategoryController.text = subCategory;
_showSubCategoryDropdown = false;
_selectedSubSubCategory = '';
_subSubCategoryController.text = '';
_updateSubSubCategories();
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
_selectedSubSubCategory,
);
});
}
// 소분류 선택 핸들러
void _selectSubSubCategory(String subSubCategory) {
setState(() {
_selectedSubSubCategory = subSubCategory;
_subSubCategoryController.text = subSubCategory;
_showSubSubCategoryDropdown = false;
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
_selectedSubSubCategory,
);
});
}
@override
Widget build(BuildContext context) {
return FormFieldWrapper(
label: '카테고리',
isRequired: widget.isRequired,
child: Column(
children: [
// 대분류 입력 필드 (자동완성)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _categoryController,
focusNode: _categoryFocusNode,
decoration: InputDecoration(
hintText: '대분류',
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
suffixIcon:
_categoryController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_categoryController.clear();
_selectedCategory = '';
_selectedSubCategory = '';
_selectedSubSubCategory = '';
_subCategoryController.clear();
_subSubCategoryController.clear();
_filteredCategories = List.from(_allCategories);
_filteredSubCategories = [];
_filteredSubSubCategories = [];
_showCategoryDropdown =
_categoryFocusNode.hasFocus;
widget.onCategoryChanged('', '', '');
});
},
)
: IconButton(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {
setState(() {
_showCategoryDropdown = !_showCategoryDropdown;
});
},
),
),
validator: (value) {
if (widget.isRequired && (value == null || value.isEmpty)) {
return '대분류를 선택해주세요';
}
return null;
},
onTap: () {
setState(() {
if (!_showCategoryDropdown) {
_showCategoryDropdown = true;
}
});
},
),
// 대분류 자동완성 드롭다운
AutocompleteDropdown(
items: _filteredCategories,
inputText: _categoryController.text,
onSelect: _selectCategory,
showDropdown: _showCategoryDropdown,
),
],
),
const SizedBox(height: 12),
// 중분류 및 소분류 선택 행
Row(
children: [
// 중분류 입력 필드 (자동완성)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _subCategoryController,
focusNode: _subCategoryFocusNode,
decoration: InputDecoration(
hintText: '중분류',
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
suffixIcon:
_subCategoryController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_subCategoryController.clear();
_selectedSubCategory = '';
_selectedSubSubCategory = '';
_subSubCategoryController.clear();
_updateSubCategories();
_updateSubSubCategories();
_showSubCategoryDropdown =
_subCategoryFocusNode.hasFocus;
widget.onCategoryChanged(
_selectedCategory,
'',
'',
);
});
},
)
: IconButton(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {
setState(() {
_showSubCategoryDropdown =
!_showSubCategoryDropdown;
});
},
),
),
enabled: _selectedCategory.isNotEmpty,
onTap: () {
setState(() {
if (!_showSubCategoryDropdown &&
_filteredSubCategories.isNotEmpty) {
_showSubCategoryDropdown = true;
}
});
},
),
// 중분류 자동완성 드롭다운
AutocompleteDropdown(
items: _filteredSubCategories,
inputText: _subCategoryController.text,
onSelect: _selectSubCategory,
showDropdown: _showSubCategoryDropdown,
),
],
),
),
const SizedBox(width: 12),
// 소분류 입력 필드 (자동완성)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _subSubCategoryController,
focusNode: _subSubCategoryFocusNode,
decoration: InputDecoration(
hintText: '소분류',
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
suffixIcon:
_subSubCategoryController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_subSubCategoryController.clear();
_selectedSubSubCategory = '';
_updateSubSubCategories();
_showSubSubCategoryDropdown =
_subSubCategoryFocusNode.hasFocus;
widget.onCategoryChanged(
_selectedCategory,
_selectedSubCategory,
'',
);
});
},
)
: IconButton(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {
setState(() {
_showSubSubCategoryDropdown =
!_showSubSubCategoryDropdown;
});
},
),
),
enabled:
_selectedCategory.isNotEmpty &&
_selectedSubCategory.isNotEmpty,
onTap: () {
setState(() {
if (!_showSubSubCategoryDropdown &&
_filteredSubSubCategories.isNotEmpty) {
_showSubSubCategoryDropdown = true;
}
});
},
),
// 소분류 자동완성 드롭다운
AutocompleteDropdown(
items: _filteredSubSubCategories,
inputText: _subSubCategoryController.text,
onSelect: _selectSubSubCategory,
showDropdown: _showSubSubCategoryDropdown,
),
],
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
// 데이터 테이블 카드
class DataTableCard extends StatelessWidget {
final Widget child;
final String? title;
final double? width;
const DataTableCard({Key? key, required this.child, this.title, this.width})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: width,
decoration: AppThemeTailwind.cardDecoration,
margin: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(title!, style: AppThemeTailwind.subheadingStyle),
),
Padding(padding: const EdgeInsets.all(16.0), child: child),
],
),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'form_field_wrapper.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
// 날짜 선택 필드
class DatePickerField extends StatelessWidget {
final DateTime selectedDate;
final Function(DateTime) onDateChanged;
final bool allowFutureDate;
final bool isRequired;
const DatePickerField({
Key? key,
required this.selectedDate,
required this.onDateChanged,
this.allowFutureDate = false,
this.isRequired = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2000),
lastDate: allowFutureDate ? DateTime(2100) : DateTime.now(),
);
if (picked != null && picked != selectedDate) {
onDateChanged(picked);
}
},
child: FormFieldWrapper(
label: '날짜',
isRequired: isRequired,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}',
style: AppThemeTailwind.bodyStyle,
),
const Icon(Icons.calendar_today, size: 20),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
// 폼 필드 래퍼
class FormFieldWrapper extends StatelessWidget {
final String label;
final Widget child;
final bool isRequired;
const FormFieldWrapper({
Key? key,
required this.label,
required this.child,
this.isRequired = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (isRequired)
const Text(
' *',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8.0),
child,
],
),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
// 자동완성 드롭다운에서 텍스트 하이라이트를 위한 위젯
class HighlightText extends StatelessWidget {
// 전체 텍스트
final String text;
// 하이라이트할 부분
final String highlight;
// 하이라이트 색상
final Color highlightColor;
// 텍스트 스타일
final TextStyle? style;
const HighlightText({
Key? key,
required this.text,
required this.highlight,
this.highlightColor = Colors.blue,
this.style,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (highlight.isEmpty) {
// 하이라이트가 없으면 전체 텍스트 반환
return Text(text, style: style);
}
final String lowerText = text.toLowerCase();
final String lowerHighlight = highlight.toLowerCase();
final int start = lowerText.indexOf(lowerHighlight);
if (start < 0) {
// 일치하는 부분이 없으면 전체 텍스트 반환
return Text(text, style: style);
}
final int end = start + highlight.length;
return RichText(
text: TextSpan(
style: style ?? DefaultTextStyle.of(context).style,
children: [
if (start > 0) TextSpan(text: text.substring(0, start)),
TextSpan(
text: text.substring(start, end),
style: TextStyle(
fontWeight: FontWeight.bold,
color: highlightColor,
),
),
if (end < text.length) TextSpan(text: text.substring(end)),
],
),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
// 페이지 타이틀 위젯
class PageTitle extends StatelessWidget {
final String title;
final Widget? rightWidget;
final double? width;
const PageTitle({Key? key, required this.title, this.rightWidget, this.width})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: width,
margin: const EdgeInsets.only(bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: AppThemeTailwind.headingStyle),
if (rightWidget != null) rightWidget!,
],
),
);
}
}

View File

@@ -0,0 +1,9 @@
/// 메트로닉 스타일 공통 레이아웃 컴포넌트 barrel 파일
/// 각 위젯은 SRP에 따라 별도 파일로 분리되어 있습니다.
export 'metronic_page_container.dart';
export 'metronic_card.dart';
export 'metronic_stats_card.dart';
export 'metronic_page_title.dart';
export 'metronic_data_table.dart';
export 'metronic_form_field.dart';
export 'metronic_tab_container.dart';

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
class MainLayout extends StatelessWidget {
final String title;
final Widget child;
final String currentRoute;
final List<Widget>? actions;
final bool showBackButton;
final Widget? floatingActionButton;
const MainLayout({
Key? key,
required this.title,
required this.child,
required this.currentRoute,
this.actions,
this.showBackButton = false,
this.floatingActionButton,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// MetronicCloud 스타일: 상단부 플랫, 여백 넓게, 타이틀/경로/버튼 스타일링
return Scaffold(
backgroundColor: AppThemeTailwind.surface,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 상단 앱바
_buildAppBar(context),
// 컨텐츠
Expanded(child: child),
],
),
floatingActionButton: floatingActionButton,
);
}
Widget _buildAppBar(BuildContext context) {
// 상단 앱바: 경로 텍스트가 수직 중앙에 오도록 조정, 배경색/글자색 변경
return Container(
height: 88,
padding: const EdgeInsets.symmetric(horizontal: 40),
decoration: BoxDecoration(
color: AppThemeTailwind.surface, // 회색 배경
border: const Border(
bottom: BorderSide(color: Color(0xFFF3F6F9), width: 1),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, // Row 내에서 수직 중앙 정렬
children: [
// 경로 및 타이틀 영역 (수직 중앙 정렬)
Column(
mainAxisAlignment: MainAxisAlignment.center, // Column 내에서 수직 중앙 정렬
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 경로 텍스트 (폰트 사이즈 2배, 검은색 글자)
Text(
_getBreadcrumb(currentRoute),
style: TextStyle(
fontSize: 26,
color: AppThemeTailwind.dark,
), // 검은색 글자
),
// 타이틀이 있을 때만 표시
if (title.isNotEmpty)
Text(
title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppThemeTailwind.dark,
),
),
],
),
const Spacer(),
if (actions != null)
Row(
children:
actions!
.map(
(w) => Padding(
padding: const EdgeInsets.only(left: 8),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: AppThemeTailwind.muted,
width: 1,
),
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(8),
),
child: w,
),
),
)
.toList(),
),
],
),
);
}
// 현재 라우트에 따라 경로 문자열을 반환하는 함수
String _getBreadcrumb(String route) {
// 실제 라우트에 따라 경로를 한글로 변환 (예시)
switch (route) {
case '/':
case '/home':
return '홈 / 대시보드';
case '/equipment':
return '홈 / 장비 관리';
case '/company':
return '홈 / 회사 관리';
case '/maintenance':
return '홈 / 유지보수 관리';
case '/item':
return '홈 / 물품 관리';
default:
return '';
}
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 메트로닉 스타일 카드 위젯 (SRP 분리)
class MetronicCard extends StatelessWidget {
final String? title;
final Widget child;
final List<Widget>? actions;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
const MetronicCard({
Key? key,
this.title,
required this.child,
this.actions,
this.padding,
this.margin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: AppThemeTailwind.cardDecoration,
margin: margin,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null || actions != null)
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (title != null)
Text(title!, style: AppThemeTailwind.subheadingStyle),
if (actions != null) Row(children: actions!),
],
),
),
Padding(padding: padding ?? const EdgeInsets.all(16), child: child),
],
),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/common/metronic_card.dart';
/// 메트로닉 스타일 데이터 테이블 카드 위젯 (SRP 분리)
class MetronicDataTable extends StatelessWidget {
final List<DataColumn> columns;
final List<DataRow> rows;
final String? title;
final bool isLoading;
final String? emptyMessage;
const MetronicDataTable({
Key? key,
required this.columns,
required this.rows,
this.title,
this.isLoading = false,
this.emptyMessage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return MetronicCard(
title: title,
child:
isLoading
? const Center(child: CircularProgressIndicator())
: rows.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
emptyMessage ?? '데이터가 없습니다.',
style: AppThemeTailwind.bodyStyle,
),
),
)
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: DataTable(
columns: columns,
rows: rows,
headingRowColor: MaterialStateProperty.all(
AppThemeTailwind.light,
),
dataRowMaxHeight: 60,
columnSpacing: 24,
horizontalMargin: 16,
),
),
),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 메트로닉 스타일 폼 필드 래퍼 위젯 (SRP 분리)
class MetronicFormField extends StatelessWidget {
final String label;
final Widget child;
final bool isRequired;
final String? helperText;
const MetronicFormField({
Key? key,
required this.label,
required this.child,
this.isRequired = false,
this.helperText,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: AppThemeTailwind.dark,
),
),
if (isRequired)
const Text(
' *',
style: TextStyle(
color: AppThemeTailwind.danger,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
child,
if (helperText != null)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(helperText!, style: AppThemeTailwind.smallText),
),
],
),
);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 메트로닉 스타일 페이지 컨테이너 위젯 (SRP 분리)
class MetronicPageContainer extends StatelessWidget {
final String title;
final Widget child;
final List<Widget>? actions;
final bool showBackButton;
const MetronicPageContainer({
Key? key,
required this.title,
required this.child,
this.actions,
this.showBackButton = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
automaticallyImplyLeading: showBackButton,
actions: actions,
elevation: 0,
),
body: Container(
color: AppThemeTailwind.surface,
padding: const EdgeInsets.all(16),
child: child,
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 메트로닉 스타일 페이지 타이틀 위젯 (SRP 분리)
class MetronicPageTitle extends StatelessWidget {
final String title;
final VoidCallback? onAddPressed;
final String? addButtonLabel;
const MetronicPageTitle({
Key? key,
required this.title,
this.onAddPressed,
this.addButtonLabel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: AppThemeTailwind.headingStyle),
if (onAddPressed != null)
ElevatedButton.icon(
onPressed: onAddPressed,
icon: const Icon(Icons.add),
label: Text(addButtonLabel ?? '추가'),
style: AppThemeTailwind.primaryButtonStyle,
),
],
),
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 메트로닉 스타일 통계 카드 위젯 (SRP 분리)
class MetronicStatsCard extends StatelessWidget {
final String title;
final String value;
final String? subtitle;
final IconData? icon;
final Color? iconBackgroundColor;
final bool showTrend;
final double? trendPercentage;
final bool isPositiveTrend;
const MetronicStatsCard({
Key? key,
required this.title,
required this.value,
this.subtitle,
this.icon,
this.iconBackgroundColor,
this.showTrend = false,
this.trendPercentage,
this.isPositiveTrend = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: AppThemeTailwind.cardDecoration,
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: AppThemeTailwind.bodyStyle.copyWith(
color: AppThemeTailwind.muted,
fontSize: 12,
),
),
if (icon != null)
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: iconBackgroundColor ?? AppThemeTailwind.light,
shape: BoxShape.circle,
),
child: Icon(
icon,
color:
iconBackgroundColor != null
? Colors.white
: AppThemeTailwind.primary,
size: 16,
),
),
],
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppThemeTailwind.dark,
),
),
if (subtitle != null || showTrend) const SizedBox(height: 4),
if (subtitle != null)
Text(subtitle!, style: AppThemeTailwind.smallText),
if (showTrend && trendPercentage != null)
Row(
children: [
Icon(
isPositiveTrend ? Icons.arrow_upward : Icons.arrow_downward,
color:
isPositiveTrend
? AppThemeTailwind.success
: AppThemeTailwind.danger,
size: 12,
),
const SizedBox(width: 4),
Text(
'${trendPercentage!.toStringAsFixed(1)}%',
style: TextStyle(
color:
isPositiveTrend
? AppThemeTailwind.success
: AppThemeTailwind.danger,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 메트로닉 스타일 탭 컨테이너 위젯 (SRP 분리)
class MetronicTabContainer extends StatelessWidget {
final List<String> tabs;
final List<Widget> tabViews;
final int initialIndex;
const MetronicTabContainer({
Key? key,
required this.tabs,
required this.tabViews,
this.initialIndex = 0,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabs.length,
initialIndex: initialIndex,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E7EB), width: 1),
),
),
child: TabBar(
tabs: tabs.map((tab) => Tab(text: tab)).toList(),
labelColor: AppThemeTailwind.primary,
unselectedLabelColor: AppThemeTailwind.muted,
labelStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
indicatorColor: AppThemeTailwind.primary,
indicatorWeight: 2,
),
),
Expanded(child: TabBarView(children: tabViews)),
],
),
);
}
}

View File

@@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
/// Metronic Admin 테일윈드 테마 (데모6 스타일)
class AppThemeTailwind {
// 메인 컬러 팔레트
static const Color primary = Color(0xFF5867DD);
static const Color secondary = Color(0xFF34BFA3);
static const Color success = Color(0xFF1BC5BD);
static const Color info = Color(0xFF8950FC);
static const Color warning = Color(0xFFFFA800);
static const Color danger = Color(0xFFF64E60);
static const Color light = Color(0xFFF3F6F9);
static const Color dark = Color(0xFF181C32);
static const Color muted = Color(0xFFB5B5C3);
// 배경 컬러
static const Color surface = Color(0xFFF7F8FA);
static const Color cardBackground = Colors.white;
// 테마 데이터
static ThemeData get lightTheme {
return ThemeData(
primaryColor: primary,
colorScheme: const ColorScheme.light(
primary: primary,
secondary: secondary,
background: surface,
surface: cardBackground,
error: danger,
),
scaffoldBackgroundColor: surface,
fontFamily: 'Poppins',
// AppBar 테마
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: dark,
elevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
color: dark,
fontSize: 18,
fontWeight: FontWeight.w600,
),
iconTheme: IconThemeData(color: dark),
),
// 버튼 테마
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),
// 카드 테마
cardTheme: CardTheme(
color: Colors.white,
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
margin: const EdgeInsets.symmetric(vertical: 8),
),
// 입력 폼 테마
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: primary),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: danger),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
// 데이터 테이블 테마
dataTableTheme: const DataTableThemeData(
headingRowColor: WidgetStatePropertyAll(light),
dividerThickness: 1,
columnSpacing: 24,
headingTextStyle: TextStyle(
color: dark,
fontWeight: FontWeight.w600,
fontSize: 14,
),
dataTextStyle: TextStyle(color: Color(0xFF6C7293), fontSize: 14),
),
);
}
// 스타일 - 헤딩 및 텍스트
static const TextStyle headingStyle = TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: dark,
);
static const TextStyle subheadingStyle = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: dark,
);
static const TextStyle bodyStyle = TextStyle(
fontSize: 14,
color: Color(0xFF6C7293),
);
// 굵은 본문 텍스트
static const TextStyle bodyBoldStyle = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: dark,
);
static const TextStyle smallText = TextStyle(fontSize: 12, color: muted);
// 버튼 스타일
static final ButtonStyle primaryButtonStyle = ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
);
// 라벨 스타일
static const TextStyle formLabelStyle = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: dark,
);
static final ButtonStyle secondaryButtonStyle = ElevatedButton.styleFrom(
backgroundColor: secondary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
);
static final ButtonStyle outlineButtonStyle = OutlinedButton.styleFrom(
foregroundColor: primary,
side: const BorderSide(color: primary),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
);
// 카드 장식
static final BoxDecoration cardDecoration = BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(13),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
);
// 기타 장식
static final BoxDecoration containerDecoration = BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE5E7EB)),
);
static const EdgeInsets cardPadding = EdgeInsets.all(20);
static const EdgeInsets listPadding = EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
);
}

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