From b08a382a23364babbd067d54ffb87ac87f2de803 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 2 Sep 2025 00:22:35 +0900 Subject: [PATCH 01/17] Squashed 'docs/' content from commit bfe4c5c git-subtree-dir: docs git-subtree-split: bfe4c5cb74bd77125285649cb658d6cc7211e6b7 --- Architecture.md | 27 ++++++++++++++ Git.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ Home.md | 1 + 3 files changed, 127 insertions(+) create mode 100644 Architecture.md create mode 100644 Git.md create mode 100644 Home.md diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 0000000..c20d825 --- /dev/null +++ b/Architecture.md @@ -0,0 +1,27 @@ +# Folder Structure +- app\ + expo routes +- components +- hooks +- test +- data + - data-sources\ + Local data source\ + Remote data source + - models\ + watermelon database models + - schemas + database schemas + - repositories\ + Implementations of repository interfaces from the domain layer +- domain + - entities + - repositories\ + Interfaces of repositories + repositories should return entities + - use-cases +- di\ + container\ + hook + + diff --git a/Git.md b/Git.md new file mode 100644 index 0000000..ee9fc57 --- /dev/null +++ b/Git.md @@ -0,0 +1,99 @@ +## Git Strategy + +## Commit Message Formats + +### Default +
+<type>(<optional scope>): <description>
+empty separator line
+<optional body>
+empty separator line
+<optional footer>
+
+ +### Types +* API relevant changes + * `feat` Commits, that add or remove a new feature + * `fix` Commits, that fixes a bug +* `refactor` Commits, that rewrite/restructure your code, however, does not change any API behavior + * `perf` Commits are special `refactor` commits, that improve performance +* `style` Commits, that do not affect the meaning (white-space, formatting, missing semi-colons, etc) +* `test` Commits, that add missing tests or correct existing tests +* `docs` Commits, that affect documentation only +* `build` Commits affect build components like build tool, ci pipeline, dependencies, project version, ... +* `ops` Commits affect operational components like infrastructure, deployment, backup, recovery, ... +* `chore` Miscellaneous commits e.g. modifying `.gitignore` + +### Scopes +The `scope` provides additional contextual information. +* Is an **optional** part of the format +* Allowed Scopes depends on the specific project +* Don't use issue identifiers as scopes + +### Breaking Changes Indicator +Breaking changes should be indicated by an `!` before the `:` in the subject line e.g. `feat(api)!: remove status endpoint` +* Is an **optional** part of the format + +### Description +The `description` contains a concise description of the change. +* Is a **mandatory** part of the format +* Use the imperative, present tense: "change" not "changed" nor "changes" + * Think of `This commit will...` or `This commit should...` +* Don't capitalize the first letter +* No dot (`.`) at the end + +### Body +The `body` should include the motivation for the change and contrast this with previous behavior. +* Is an **optional** part of the format +* Use the imperative, present tense: "change" not "changed" nor "changes" +* This is the place to mention issue identifiers and their relations + +### Footer +The `footer` should contain any information about **Breaking Changes** and is also the place to **reference Issues** that this commit refers to. +* Is an **optional** part of the format +* **optionally** reference an issue by its id. +* **Breaking Changes** should start with the word `BREAKING CHANGES:` followed by space or two newlines. The rest of the commit message is then used for this. + + +### Examples +* ``` + feat: add email notifications on new direct messages + ``` +* ``` + feat(shopping cart): add the amazing button + ``` +* ``` + feat!: remove ticket list endpoint + + refers to JIRA-1337 + + BREAKING CHANGES: ticket endpoints no longer support listing all entities. + ``` +* ``` + fix(shopping-cart): prevent ordering an empty shopping cart + ``` +* ``` + fix(api): fix the wrong calculation of the request body checksum + ``` +* ``` + fix: add a missing parameter to a service call + + The error occurred because of . + ``` +* ``` + perf: decrease memory footprint to determine uniqe visitors by using HyperLogLog + ``` +* ``` + build: update dependencies + ``` +* ``` + build(release): bump version to 1.0.0 + ``` +* ``` + refactor: Implement Fibonacci number calculation as recursion + ``` +* ``` + style: remove empty line + ``` + +--- diff --git a/Home.md b/Home.md new file mode 100644 index 0000000..4831e34 --- /dev/null +++ b/Home.md @@ -0,0 +1 @@ +Welcome to the OnTime-front wiki! From 2c4a42168bef7b811217269c14ebca6495001db0 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 2 Sep 2025 00:29:38 +0900 Subject: [PATCH 02/17] docs: add comprehensive wiki management guide and improve home page navigation --- docs/Home.md | 43 ++++++- docs/Wiki-Management.md | 245 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 docs/Wiki-Management.md diff --git a/docs/Home.md b/docs/Home.md index 4831e34..7de1938 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -1 +1,42 @@ -Welcome to the OnTime-front wiki! +# OnTime Flutter Project Wiki + +Welcome to the OnTime-front project documentation! This wiki contains everything new developers need to understand and contribute to the project. + +## πŸ“š Documentation Index + +### Getting Started +- [Wiki Management Guide](./Wiki-Management.md) - How to modify and upload wiki content +- [Project Architecture](./Architecture.md) - Understanding the project structure +- [Git Workflow](./Git.md) - Git strategy and commit message formats + +### Development Resources +- 🚧 **Getting Started Guide** *(Coming Soon)* - Setup and installation +- 🚧 **Development Guide** *(Coming Soon)* - Development workflow and best practices +- 🚧 **API Documentation** *(Coming Soon)* - Backend API reference +- 🚧 **Testing Guide** *(Coming Soon)* - Testing procedures and guidelines + +## 🎯 Quick Links + +- [Main Repository](https://github.com/DevKor-github/OnTime-front) +- [Issues](https://github.com/DevKor-github/OnTime-front/issues) +- [Pull Requests](https://github.com/DevKor-github/OnTime-front/pulls) +- [Widgetbook](https://on-time-front-widgetbook.web.app/) + +## 🀝 Contributing + +New to the project? Start here: + +1. **Read the [Wiki Management Guide](./Wiki-Management.md)** to understand how documentation works +2. **Review the [Architecture](./Architecture.md)** to understand the project structure +3. **Follow the [Git Workflow](./Git.md)** for consistent development practices +4. **Set up your development environment** *(guide coming soon)* + +## πŸ“ Need Help? + +- Create an issue for bugs or feature requests +- Ask questions in team discussions +- Contribute to documentation improvements + +--- + +*This wiki is maintained by the OnTime development team and integrated with the main repository.* diff --git a/docs/Wiki-Management.md b/docs/Wiki-Management.md new file mode 100644 index 0000000..3cb8b74 --- /dev/null +++ b/docs/Wiki-Management.md @@ -0,0 +1,245 @@ +# Wiki Management Guide + +This guide explains how to modify, create, and upload wiki documentation for the OnTime Flutter project. + +## πŸ“– Overview + +Our project wiki is integrated directly into the main repository using Git subtrees. This means: +- Wiki content is stored in the `docs/` folder +- Changes are version-controlled with the main codebase +- Documentation stays in sync with code changes +- New developers can access docs offline + +## πŸš€ Quick Start + +### Prerequisites +- Git configured with your GitHub account +- Access to the OnTime-front repository +- Basic knowledge of Markdown + +### Current Wiki Structure +``` +docs/ +β”œβ”€β”€ Architecture.md # Project structure and architecture +β”œβ”€β”€ Git.md # Git workflow and commit guidelines +β”œβ”€β”€ Home.md # Wiki homepage +└── Wiki-Management.md # This guide +``` + +## ✏️ Modifying Existing Wiki Pages + +### 1. Edit Files Locally +Navigate to the `docs/` folder and edit any `.md` file: + +```bash +# Navigate to docs folder +cd docs/ + +# Edit existing files with your preferred editor +code Architecture.md +# or +vim Home.md +# or +nano Git.md +``` + +### 2. Preview Your Changes +Use any Markdown preview tool or your IDE's built-in preview to review changes before committing. + +### 3. Commit Changes to Main Repository +```bash +# From project root +git add docs/ +git commit -m "docs: update [filename] with [brief description]" +``` + +## πŸ“ Creating New Wiki Pages + +### 1. Create New Markdown File +```bash +# From project root +touch docs/New-Page-Name.md +``` + +### 2. Add Content +Use standard Markdown syntax. Here's a template: + +```markdown +# Page Title + +Brief description of what this page covers. + +## Section 1 + +Content here... + +### Subsection + +More detailed content... + +## Code Examples + +\```dart +// Flutter/Dart code examples +void main() { + print('Hello OnTime!'); +} +\``` + +## Links and References + +- [Internal Link](./Other-Page.md) +- [External Link](https://flutter.dev) +``` + +### 3. Update Navigation +If creating a major new page, consider updating `Home.md` to include a link to your new page. + +## πŸ”„ Syncing with GitHub Wiki + +Our project uses Git subtree to keep the main repository and GitHub wiki synchronized. + +### Push Local Changes to GitHub Wiki + +After committing your documentation changes to the main repository: + +```bash +# Push documentation changes to GitHub wiki +git subtree push --prefix=docs wiki master +``` + +**What this does:** +- Takes all changes from the `docs/` folder +- Pushes them to the GitHub wiki repository +- Updates the online wiki at `https://github.com/DevKor-github/OnTime-front/wiki` + +### Pull Changes from GitHub Wiki + +If someone edits the wiki directly on GitHub: + +```bash +# Pull changes from GitHub wiki to local docs folder +git subtree pull --prefix=docs wiki master --squash +``` + +**When to use this:** +- Someone edited wiki pages directly on GitHub +- You want to sync external wiki changes to your local repository +- Before starting major documentation work (to avoid conflicts) + +## πŸ”§ Advanced Workflows + +### Working on Documentation-Heavy Features + +1. **Create a documentation branch:** + ```bash + git checkout -b docs/feature-name + ``` + +2. **Make your documentation changes** + +3. **Commit and push to main repository:** + ```bash + git add docs/ + git commit -m "docs: add documentation for feature-name" + git push origin docs/feature-name + ``` + +4. **Create PR for review** + +5. **After PR merge, sync to wiki:** + ```bash + git checkout main + git pull origin main + git subtree push --prefix=docs wiki master + ``` + +### Handling Merge Conflicts + +If you encounter conflicts when pulling from the wiki: + +1. **Resolve conflicts in the docs/ folder** +2. **Commit the resolution:** + ```bash + git add docs/ + git commit -m "docs: resolve wiki merge conflicts" + ``` +3. **Push resolved changes:** + ```bash + git subtree push --prefix=docs wiki master + ``` + +## πŸ“‹ Documentation Best Practices + +### File Naming Convention +- Use kebab-case: `Getting-Started.md`, `API-Guide.md` +- Be descriptive but concise +- Avoid spaces and special characters + +### Content Guidelines +1. **Start with a clear title and overview** +2. **Use consistent heading hierarchy (H1 β†’ H2 β†’ H3)** +3. **Include code examples where relevant** +4. **Add links to related documentation** +5. **Keep content up-to-date with code changes** + +### Markdown Tips +- Use `backticks` for inline code +- Use triple backticks with language for code blocks +- Use `**bold**` for emphasis +- Use `> blockquotes` for important notes +- Create tables for structured data + +## πŸ› οΈ Troubleshooting + +### Common Issues + +**Issue: `fatal: working tree has modifications`** +```bash +# Solution: Commit or stash changes first +git add . +git commit -m "docs: work in progress" +# or +git stash +``` + +**Issue: Wiki changes not appearing on GitHub** +```bash +# Solution: Ensure you pushed to the wiki remote +git subtree push --prefix=docs wiki master +``` + +**Issue: Local docs out of sync** +```bash +# Solution: Pull latest changes from wiki +git subtree pull --prefix=docs wiki master --squash +``` + +### Getting Help +- Check Git status: `git status` +- View recent commits: `git log --oneline -10` +- Check remotes: `git remote -v` +- Ask team members or create an issue + +## 🎯 Recommended Documentation + +For new developers, consider creating these essential pages: + +- [ ] **Getting-Started.md** - Setup and installation guide +- [ ] **Development-Guide.md** - Development workflow and tools +- [ ] **API-Documentation.md** - Backend API reference +- [ ] **Testing-Guide.md** - How to run and write tests +- [ ] **Deployment.md** - Build and deployment procedures +- [ ] **Contributing.md** - Contribution guidelines +- [ ] **Troubleshooting.md** - Common issues and solutions + +## πŸ“š Resources + +- [Markdown Guide](https://www.markdownguide.org/) +- [GitHub Wiki Documentation](https://docs.github.com/en/communities/documenting-your-project-with-wikis) +- [Git Subtree Documentation](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_subtree_merge) + +--- + +*Last updated: $(date)* +*Maintainer: OnTime Development Team* From cc9ce3106e61cb3cab28ef9e4c409f1d321a68ef Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 2 Sep 2025 00:36:04 +0900 Subject: [PATCH 03/17] docs: completely rewrite Architecture.md with comprehensive Flutter project analysis --- docs/Architecture.md | 388 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 363 insertions(+), 25 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index c20d825..a0b6b20 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -1,27 +1,365 @@ -# Folder Structure -- app\ - expo routes -- components -- hooks -- test -- data - - data-sources\ - Local data source\ - Remote data source - - models\ - watermelon database models - - schemas - database schemas - - repositories\ - Implementations of repository interfaces from the domain layer -- domain - - entities - - repositories\ - Interfaces of repositories - repositories should return entities - - use-cases -- di\ - container\ - hook +# OnTime Flutter Project Architecture +This document provides a comprehensive overview of the OnTime Flutter project's architecture, folder structure, and design patterns. + +## πŸ—οΈ Architecture Overview + +OnTime follows **Clean Architecture** principles with a clear separation of concerns across three main layers: + +- **Presentation Layer**: UI components, state management (BLoC/Cubit), and navigation +- **Domain Layer**: Business logic, entities, use cases, and repository interfaces +- **Data Layer**: Repository implementations, data sources (local/remote), and data models + +### Architecture Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ PRESENTATION LAYER β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β€’ Screens & Components β”‚ +β”‚ β€’ BLoC/Cubit (State Management) β”‚ +β”‚ β€’ Navigation (GoRouter) β”‚ +β”‚ β€’ Theme & Localization β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ DOMAIN LAYER β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β€’ Entities (Business Objects) β”‚ +β”‚ β€’ Use Cases (Business Logic) β”‚ +β”‚ β€’ Repository Interfaces β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ DATA LAYER β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β€’ Repository Implementations β”‚ +β”‚ β€’ Data Sources (Remote API, Local Database) β”‚ +β”‚ β€’ Data Models (JSON Serialization) β”‚ +β”‚ β€’ Database Tables & DAOs β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“ Project Structure + +``` +lib/ +β”œβ”€β”€ πŸ“± presentation/ # UI Layer (Screens, Widgets, State Management) +β”‚ β”œβ”€β”€ alarm/ # Alarm and timer functionality +β”‚ β”œβ”€β”€ app/ # Main app setup and global BLoC +β”‚ β”œβ”€β”€ calendar/ # Calendar views and components +β”‚ β”œβ”€β”€ early_late/ # Early/late arrival screens +β”‚ β”œβ”€β”€ home/ # Home screen and dashboard +β”‚ β”œβ”€β”€ login/ # Authentication screens +β”‚ β”œβ”€β”€ my_page/ # User profile and settings +β”‚ β”œβ”€β”€ notification_allow/ # Notification permission +β”‚ β”œβ”€β”€ onboarding/ # User onboarding flow +β”‚ β”œβ”€β”€ schedule_create/ # Schedule creation and editing +β”‚ └── shared/ # Shared UI components and utilities +β”‚ β”œβ”€β”€ components/ # Reusable widgets +β”‚ β”œβ”€β”€ router/ # Navigation configuration +β”‚ β”œβ”€β”€ theme/ # App theming +β”‚ └── utils/ # UI utilities +β”‚ +β”œβ”€β”€ 🏒 domain/ # Business Logic Layer +β”‚ β”œβ”€β”€ entities/ # Business objects (User, Schedule, etc.) +β”‚ β”œβ”€β”€ repositories/ # Repository interfaces +β”‚ └── use-cases/ # Business logic operations +β”‚ +β”œβ”€β”€ πŸ’Ύ data/ # Data Access Layer +β”‚ β”œβ”€β”€ daos/ # Database Access Objects (Drift) +β”‚ β”œβ”€β”€ data_sources/ # Data source interfaces/implementations +β”‚ β”œβ”€β”€ models/ # API request/response models +β”‚ β”œβ”€β”€ repositories/ # Repository implementations +β”‚ └── tables/ # Database table definitions +β”‚ +β”œβ”€β”€ πŸ”§ core/ # Core Infrastructure +β”‚ β”œβ”€β”€ constants/ # App constants and environment variables +β”‚ β”œβ”€β”€ database/ # Database configuration (Drift) +β”‚ β”œβ”€β”€ di/ # Dependency injection setup (Injectable) +β”‚ β”œβ”€β”€ dio/ # HTTP client configuration +β”‚ β”œβ”€β”€ services/ # Core services (navigation, notifications) +β”‚ └── utils/ # Core utilities and converters +β”‚ +β”œβ”€β”€ 🌍 l10n/ # Localization files +β”œβ”€β”€ firebase_options.dart # Firebase configuration +└── main.dart # Application entry point +``` + +## πŸ› οΈ Technology Stack + +### Core Framework & Language +- **Flutter**: Cross-platform mobile framework +- **Dart**: Programming language + +### State Management +- **BLoC/Cubit**: Business Logic Component pattern for state management +- **Riverpod**: Additional state management for specific use cases +- **Equatable**: Value equality for state objects + +### Dependency Injection +- **Injectable**: Code generation for dependency injection +- **GetIt**: Service locator pattern + +### Database & Persistence +- **Drift**: Type-safe SQL database library +- **SharedPreferences**: Simple key-value storage +- **FlutterSecureStorage**: Secure token storage + +### Networking +- **Dio**: HTTP client with interceptors +- **JSON Annotation**: JSON serialization code generation + +### Navigation +- **GoRouter**: Declarative routing solution + +### UI Components +- **Material Design**: Google's design system +- **Flutter SVG**: SVG asset support +- **TableCalendar**: Calendar widget + +### Authentication +- **Google Sign-In**: Google OAuth integration +- **Kakao SDK**: Kakao social login +- **Firebase Auth**: Authentication backend + +### Notifications +- **Firebase Messaging**: Push notifications +- **Flutter Local Notifications**: Local notifications + +### Development Tools +- **Freezed**: Code generation for immutable classes +- **Build Runner**: Code generation runner +- **Widgetbook**: Component development environment + +## πŸ“‹ Architecture Patterns + +### 1. Clean Architecture Layers + +#### **Presentation Layer** +- **Screens**: Full-screen widgets representing app pages +- **Components**: Reusable UI widgets +- **BLoC/Cubit**: State management following BLoC pattern +- **Navigation**: Declarative routing with GoRouter + +#### **Domain Layer** +- **Entities**: Core business objects using Freezed for immutability +- **Use Cases**: Single-responsibility business logic operations +- **Repository Interfaces**: Contracts for data access + +#### **Data Layer** +- **Repository Implementations**: Concrete implementations of domain interfaces +- **Data Sources**: Abstraction over remote APIs and local storage +- **Models**: Data transfer objects with JSON serialization + +### 2. Dependency Injection + +```dart +// Injectable annotation for automatic registration +@Injectable() +class UserRepository implements UserRepositoryInterface { + // Implementation +} + +// GetIt service locator configuration +final getIt = GetIt.instance; + +@InjectableInit() +void configureDependencies() => getIt.init(); +``` + +### 3. State Management with BLoC + +```dart +// BLoC for handling user authentication state +@Injectable() +class AppBloc extends Bloc { + AppBloc(this._streamUserUseCase, this._signOutUseCase) + : super(AppState(user: const UserEntity.empty())) { + on(_onUserSubscriptionRequested); + on(_onSignOutPressed); + } +} +``` + +### 4. Data Models with JSON Serialization + +```dart +@JsonSerializable() +class GetUserResponseModel { + final int userId; + final String email; + final String name; + // Conversion to domain entity + UserEntity toEntity() { + return UserEntity( + id: userId.toString(), + email: email, + name: name, + ); + } +} +``` + +### 5. Database Layer with Drift + +```dart +@DriftDatabase(tables: [Users, Schedules, Places], daos: [UserDao, ScheduleDao]) +class AppDatabase extends _$AppDatabase { + @override + int get schemaVersion => 3; + + @override + MigrationStrategy get migration => MigrationStrategy( + onCreate: (Migrator m) async => await m.createAll(), + ); +} +``` + +## πŸ”„ Data Flow + +### 1. User Interaction Flow +``` +User Input β†’ Widget β†’ BLoC Event β†’ Use Case β†’ Repository β†’ Data Source β†’ API/DB + ↓ +Widget ← BLoC State ← Use Case ← Repository ← Data Source ← Response +``` + +### 2. Authentication Flow +``` +Login Screen β†’ AppBloc β†’ SignInUseCase β†’ UserRepository β†’ AuthDataSource β†’ API + ↓ +Navigation ← AppBloc ← UserEntity ← UserRepository ← AuthDataSource ← Token +``` + +### 3. Schedule Management Flow +``` +Schedule Form β†’ ScheduleBloc β†’ CreateScheduleUseCase β†’ ScheduleRepository + ↓ +Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity +``` + +## 🎯 Key Features Architecture + +### 1. **Authentication System** +- **Google OAuth** and **Kakao Login** integration +- **JWT token** management with secure storage +- **Stream-based** user state management +- **Automatic token refresh** via interceptors + +### 2. **Schedule Management** +- **CRUD operations** for schedules +- **Calendar integration** with multiple view modes +- **Preparation time calculation** and management +- **Real-time synchronization** between local and remote data + +### 3. **Notification System** +- **Firebase push notifications** for schedule reminders +- **Local notifications** for preparation alerts +- **Permission handling** for notification access + +### 4. **Offline Support** +- **Local database** with Drift for offline data access +- **Synchronization strategy** for online/offline data consistency +- **Caching mechanisms** for improved performance + +## πŸ§ͺ Testing Strategy + +### Structure +``` +test/ +β”œβ”€β”€ config/ # Test configuration +β”œβ”€β”€ data/ # Data layer tests +└── helpers/ # Test utilities +``` + +### Testing Approach +- **Unit Tests**: Use cases, repositories, and business logic +- **Widget Tests**: UI components and screen interactions +- **Integration Tests**: End-to-end user flows +- **Mocking**: Using Mockito for external dependencies + +## πŸ“¦ Build & Deployment + +### Development Environment +- **Flutter SDK**: ^3.5.4 +- **Dart SDK**: Latest stable +- **Build Tools**: build_runner for code generation + +### Code Generation Commands +```bash +# Generate all code (models, dependency injection, etc.) +flutter packages pub run build_runner build + +# Watch for changes and regenerate +flutter packages pub run build_runner watch +``` + +### Platform Support +- **Android**: Native Android build +- **iOS**: Native iOS build +- **Web**: Progressive Web App support + +## πŸ”§ Development Guidelines + +### 1. **Code Organization** +- Follow **Clean Architecture** principles +- Separate concerns across layers +- Use **feature-based** folder structure in presentation layer + +### 2. **State Management** +- Use **BLoC pattern** for complex state management +- Use **Cubit** for simpler state scenarios +- Keep business logic in **use cases**, not in BLoCs + +### 3. **Data Handling** +- Always use **entities** in the domain layer +- Convert **models to entities** at repository boundaries +- Implement **proper error handling** throughout the layers + +### 4. **Naming Conventions** +- **Entities**: `UserEntity`, `ScheduleEntity` +- **Use Cases**: `GetUserUseCase`, `CreateScheduleUseCase` +- **Repositories**: `UserRepository`, `ScheduleRepository` +- **BLoCs**: `UserBloc`, `ScheduleFormBloc` +- **Models**: `GetUserResponseModel`, `CreateScheduleRequestModel` + +### 5. **Dependencies** +- Register all dependencies using **@Injectable()** annotation +- Use **interfaces** for repository contracts +- Avoid direct dependencies between layers + +## πŸš€ Getting Started for New Developers + +1. **Setup Environment** + - Install Flutter SDK + - Configure IDE (VS Code/Android Studio) + - Run `flutter pub get` to install dependencies + +2. **Code Generation** + - Run `flutter packages pub run build_runner build` + - This generates dependency injection, JSON serialization, and Freezed code + +3. **Database Setup** + - Database migrations are handled automatically + - Local database files are created on first run + +4. **Understanding the Flow** + - Start with `main.dart` to understand app initialization + - Explore `presentation/app/` for global app state + - Check `domain/use-cases/` for business logic + - Review `data/repositories/` for data access patterns + +5. **Making Changes** + - Create new features following the existing layer structure + - Add new entities in `domain/entities/` + - Implement use cases in `domain/use-cases/` + - Create repository interfaces and implementations + - Build UI components in `presentation/` + +--- + +*This architecture documentation is maintained alongside the codebase. Please update it when making significant architectural changes.* From 0d3a42239983315811664aee666d669ca0328e92 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 2 Sep 2025 01:03:36 +0900 Subject: [PATCH 04/17] docs: update Architecture.md and Wiki-Management.md for improved clarity and structure - Enhanced the Architecture.md with additional details on technology stack, development guidelines, and testing strategies. - Revised Wiki-Management.md to clarify the process for modifying and creating wiki documentation, including best practices and common issues. --- docs/Architecture.md | 46 +++++++++++++++++++++++++++++++++++------ docs/Wiki-Management.md | 37 ++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index a0b6b20..3098b61 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -91,45 +91,55 @@ lib/ ## πŸ› οΈ Technology Stack ### Core Framework & Language + - **Flutter**: Cross-platform mobile framework - **Dart**: Programming language ### State Management + - **BLoC/Cubit**: Business Logic Component pattern for state management - **Riverpod**: Additional state management for specific use cases - **Equatable**: Value equality for state objects ### Dependency Injection + - **Injectable**: Code generation for dependency injection - **GetIt**: Service locator pattern ### Database & Persistence + - **Drift**: Type-safe SQL database library - **SharedPreferences**: Simple key-value storage - **FlutterSecureStorage**: Secure token storage ### Networking + - **Dio**: HTTP client with interceptors - **JSON Annotation**: JSON serialization code generation ### Navigation + - **GoRouter**: Declarative routing solution ### UI Components + - **Material Design**: Google's design system - **Flutter SVG**: SVG asset support - **TableCalendar**: Calendar widget ### Authentication + - **Google Sign-In**: Google OAuth integration - **Kakao SDK**: Kakao social login - **Firebase Auth**: Authentication backend ### Notifications + - **Firebase Messaging**: Push notifications - **Flutter Local Notifications**: Local notifications ### Development Tools + - **Freezed**: Code generation for immutable classes - **Build Runner**: Code generation runner - **Widgetbook**: Component development environment @@ -139,17 +149,20 @@ lib/ ### 1. Clean Architecture Layers #### **Presentation Layer** + - **Screens**: Full-screen widgets representing app pages - **Components**: Reusable UI widgets - **BLoC/Cubit**: State management following BLoC pattern - **Navigation**: Declarative routing with GoRouter #### **Domain Layer** + - **Entities**: Core business objects using Freezed for immutability - **Use Cases**: Single-responsibility business logic operations - **Repository Interfaces**: Contracts for data access #### **Data Layer** + - **Repository Implementations**: Concrete implementations of domain interfaces - **Data Sources**: Abstraction over remote APIs and local storage - **Models**: Data transfer objects with JSON serialization @@ -192,7 +205,7 @@ class GetUserResponseModel { final int userId; final String email; final String name; - + // Conversion to domain entity UserEntity toEntity() { return UserEntity( @@ -211,7 +224,7 @@ class GetUserResponseModel { class AppDatabase extends _$AppDatabase { @override int get schemaVersion => 3; - + @override MigrationStrategy get migration => MigrationStrategy( onCreate: (Migrator m) async => await m.createAll(), @@ -222,6 +235,7 @@ class AppDatabase extends _$AppDatabase { ## πŸ”„ Data Flow ### 1. User Interaction Flow + ``` User Input β†’ Widget β†’ BLoC Event β†’ Use Case β†’ Repository β†’ Data Source β†’ API/DB ↓ @@ -229,6 +243,7 @@ Widget ← BLoC State ← Use Case ← Repository ← Data Source ← Response ``` ### 2. Authentication Flow + ``` Login Screen β†’ AppBloc β†’ SignInUseCase β†’ UserRepository β†’ AuthDataSource β†’ API ↓ @@ -236,6 +251,7 @@ Navigation ← AppBloc ← UserEntity ← UserRepository ← AuthDataSource ← ``` ### 3. Schedule Management Flow + ``` Schedule Form β†’ ScheduleBloc β†’ CreateScheduleUseCase β†’ ScheduleRepository ↓ @@ -245,23 +261,27 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity ## 🎯 Key Features Architecture ### 1. **Authentication System** + - **Google OAuth** and **Kakao Login** integration - **JWT token** management with secure storage - **Stream-based** user state management - **Automatic token refresh** via interceptors ### 2. **Schedule Management** + - **CRUD operations** for schedules - **Calendar integration** with multiple view modes - **Preparation time calculation** and management - **Real-time synchronization** between local and remote data ### 3. **Notification System** + - **Firebase push notifications** for schedule reminders - **Local notifications** for preparation alerts - **Permission handling** for notification access ### 4. **Offline Support** + - **Local database** with Drift for offline data access - **Synchronization strategy** for online/offline data consistency - **Caching mechanisms** for improved performance @@ -269,6 +289,7 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity ## πŸ§ͺ Testing Strategy ### Structure + ``` test/ β”œβ”€β”€ config/ # Test configuration @@ -277,6 +298,7 @@ test/ ``` ### Testing Approach + - **Unit Tests**: Use cases, repositories, and business logic - **Widget Tests**: UI components and screen interactions - **Integration Tests**: End-to-end user flows @@ -285,20 +307,23 @@ test/ ## πŸ“¦ Build & Deployment ### Development Environment + - **Flutter SDK**: ^3.5.4 - **Dart SDK**: Latest stable - **Build Tools**: build_runner for code generation ### Code Generation Commands + ```bash # Generate all code (models, dependency injection, etc.) -flutter packages pub run build_runner build +dart run build_runner build # Watch for changes and regenerate -flutter packages pub run build_runner watch +dart run build_runner watch ``` ### Platform Support + - **Android**: Native Android build - **iOS**: Native iOS build - **Web**: Progressive Web App support @@ -306,21 +331,25 @@ flutter packages pub run build_runner watch ## πŸ”§ Development Guidelines ### 1. **Code Organization** + - Follow **Clean Architecture** principles - Separate concerns across layers - Use **feature-based** folder structure in presentation layer ### 2. **State Management** + - Use **BLoC pattern** for complex state management - Use **Cubit** for simpler state scenarios - Keep business logic in **use cases**, not in BLoCs ### 3. **Data Handling** + - Always use **entities** in the domain layer - Convert **models to entities** at repository boundaries - Implement **proper error handling** throughout the layers ### 4. **Naming Conventions** + - **Entities**: `UserEntity`, `ScheduleEntity` - **Use Cases**: `GetUserUseCase`, `CreateScheduleUseCase` - **Repositories**: `UserRepository`, `ScheduleRepository` @@ -328,6 +357,7 @@ flutter packages pub run build_runner watch - **Models**: `GetUserResponseModel`, `CreateScheduleRequestModel` ### 5. **Dependencies** + - Register all dependencies using **@Injectable()** annotation - Use **interfaces** for repository contracts - Avoid direct dependencies between layers @@ -335,19 +365,23 @@ flutter packages pub run build_runner watch ## πŸš€ Getting Started for New Developers 1. **Setup Environment** + - Install Flutter SDK - Configure IDE (VS Code/Android Studio) - Run `flutter pub get` to install dependencies 2. **Code Generation** - - Run `flutter packages pub run build_runner build` + + - Run `dart run build_runner build` - This generates dependency injection, JSON serialization, and Freezed code 3. **Database Setup** + - Database migrations are handled automatically - Local database files are created on first run 4. **Understanding the Flow** + - Start with `main.dart` to understand app initialization - Explore `presentation/app/` for global app state - Check `domain/use-cases/` for business logic @@ -362,4 +396,4 @@ flutter packages pub run build_runner watch --- -*This architecture documentation is maintained alongside the codebase. Please update it when making significant architectural changes.* +_This architecture documentation is maintained alongside the codebase. Please update it when making significant architectural changes._ diff --git a/docs/Wiki-Management.md b/docs/Wiki-Management.md index 3cb8b74..25dbcf1 100644 --- a/docs/Wiki-Management.md +++ b/docs/Wiki-Management.md @@ -5,6 +5,7 @@ This guide explains how to modify, create, and upload wiki documentation for the ## πŸ“– Overview Our project wiki is integrated directly into the main repository using Git subtrees. This means: + - Wiki content is stored in the `docs/` folder - Changes are version-controlled with the main codebase - Documentation stays in sync with code changes @@ -13,11 +14,13 @@ Our project wiki is integrated directly into the main repository using Git subtr ## πŸš€ Quick Start ### Prerequisites + - Git configured with your GitHub account - Access to the OnTime-front repository - Basic knowledge of Markdown ### Current Wiki Structure + ``` docs/ β”œβ”€β”€ Architecture.md # Project structure and architecture @@ -29,6 +32,7 @@ docs/ ## ✏️ Modifying Existing Wiki Pages ### 1. Edit Files Locally + Navigate to the `docs/` folder and edit any `.md` file: ```bash @@ -44,9 +48,11 @@ nano Git.md ``` ### 2. Preview Your Changes + Use any Markdown preview tool or your IDE's built-in preview to review changes before committing. ### 3. Commit Changes to Main Repository + ```bash # From project root git add docs/ @@ -56,15 +62,17 @@ git commit -m "docs: update [filename] with [brief description]" ## πŸ“ Creating New Wiki Pages ### 1. Create New Markdown File + ```bash # From project root touch docs/New-Page-Name.md ``` ### 2. Add Content + Use standard Markdown syntax. Here's a template: -```markdown +````markdown # Page Title Brief description of what this page covers. @@ -82,7 +90,7 @@ More detailed content... \```dart // Flutter/Dart code examples void main() { - print('Hello OnTime!'); +print('Hello OnTime!'); } \``` @@ -90,9 +98,10 @@ void main() { - [Internal Link](./Other-Page.md) - [External Link](https://flutter.dev) -``` +```` ### 3. Update Navigation + If creating a major new page, consider updating `Home.md` to include a link to your new page. ## πŸ”„ Syncing with GitHub Wiki @@ -109,6 +118,7 @@ git subtree push --prefix=docs wiki master ``` **What this does:** + - Takes all changes from the `docs/` folder - Pushes them to the GitHub wiki repository - Updates the online wiki at `https://github.com/DevKor-github/OnTime-front/wiki` @@ -123,6 +133,7 @@ git subtree pull --prefix=docs wiki master --squash ``` **When to use this:** + - Someone edited wiki pages directly on GitHub - You want to sync external wiki changes to your local repository - Before starting major documentation work (to avoid conflicts) @@ -132,6 +143,7 @@ git subtree pull --prefix=docs wiki master --squash ### Working on Documentation-Heavy Features 1. **Create a documentation branch:** + ```bash git checkout -b docs/feature-name ``` @@ -139,6 +151,7 @@ git subtree pull --prefix=docs wiki master --squash 2. **Make your documentation changes** 3. **Commit and push to main repository:** + ```bash git add docs/ git commit -m "docs: add documentation for feature-name" @@ -172,11 +185,13 @@ If you encounter conflicts when pulling from the wiki: ## πŸ“‹ Documentation Best Practices ### File Naming Convention + - Use kebab-case: `Getting-Started.md`, `API-Guide.md` - Be descriptive but concise - Avoid spaces and special characters ### Content Guidelines + 1. **Start with a clear title and overview** 2. **Use consistent heading hierarchy (H1 β†’ H2 β†’ H3)** 3. **Include code examples where relevant** @@ -184,6 +199,7 @@ If you encounter conflicts when pulling from the wiki: 5. **Keep content up-to-date with code changes** ### Markdown Tips + - Use `backticks` for inline code - Use triple backticks with language for code blocks - Use `**bold**` for emphasis @@ -195,6 +211,7 @@ If you encounter conflicts when pulling from the wiki: ### Common Issues **Issue: `fatal: working tree has modifications`** + ```bash # Solution: Commit or stash changes first git add . @@ -204,18 +221,21 @@ git stash ``` **Issue: Wiki changes not appearing on GitHub** + ```bash # Solution: Ensure you pushed to the wiki remote git subtree push --prefix=docs wiki master ``` **Issue: Local docs out of sync** + ```bash # Solution: Pull latest changes from wiki git subtree pull --prefix=docs wiki master --squash ``` ### Getting Help + - Check Git status: `git status` - View recent commits: `git log --oneline -10` - Check remotes: `git remote -v` @@ -232,14 +252,3 @@ For new developers, consider creating these essential pages: - [ ] **Deployment.md** - Build and deployment procedures - [ ] **Contributing.md** - Contribution guidelines - [ ] **Troubleshooting.md** - Common issues and solutions - -## πŸ“š Resources - -- [Markdown Guide](https://www.markdownguide.org/) -- [GitHub Wiki Documentation](https://docs.github.com/en/communities/documenting-your-project-with-wikis) -- [Git Subtree Documentation](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_subtree_merge) - ---- - -*Last updated: $(date)* -*Maintainer: OnTime Development Team* From 8a08069f96c756ac3458b92a666b5ffdd316bf2c Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 4 Sep 2025 22:59:16 +0900 Subject: [PATCH 05/17] feat: manage upcoming schedule subscription in AppBloc - Added a StreamSubscription to handle the upcoming schedule updates. - Updated the subscription management in the AppUpcomingScheduleSubscriptionRequested method to ensure proper cancellation of previous subscriptions. - Enhanced the close method to cancel the upcoming schedule subscription, ensuring resource cleanup. --- lib/presentation/app/bloc/app_bloc.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/presentation/app/bloc/app_bloc.dart b/lib/presentation/app/bloc/app_bloc.dart index 4ff9929..40d4847 100644 --- a/lib/presentation/app/bloc/app_bloc.dart +++ b/lib/presentation/app/bloc/app_bloc.dart @@ -38,6 +38,8 @@ class AppBloc extends Bloc { final GetNearestUpcomingScheduleUseCase _getNearestUpcomingScheduleUseCase; final NavigationService _navigationService; Timer? _timer; + StreamSubscription? + _upcomingScheduleSubscription; Future _appUserSubscriptionRequested( AppUserSubscriptionRequested event, @@ -79,7 +81,8 @@ class AppBloc extends Bloc { FutureOr _appUpcomingScheduleSubscriptionRequested( AppUpcomingScheduleSubscriptionRequested event, Emitter emit) async { - _getNearestUpcomingScheduleUseCase() + await _upcomingScheduleSubscription?.cancel(); + _upcomingScheduleSubscription = _getNearestUpcomingScheduleUseCase() .listen((schedule) => add(AppUpcomingScheduleReceived(schedule))); } @@ -152,4 +155,11 @@ class AppBloc extends Bloc { ), ); } + + @override + Future close() { + _timer?.cancel(); + _upcomingScheduleSubscription?.cancel(); + return super.close(); + } } From 6013267607c94a0459f3b653bc63d2c5d923dc04 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 4 Sep 2025 22:59:22 +0900 Subject: [PATCH 06/17] fix: adjust scroll behavior in HomeScreenContent - Added ClampingScrollPhysics to the SingleChildScrollView in HomeScreenContent to improve scroll performance and user experience. --- lib/presentation/home/screens/home_screen_tmp.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/home/screens/home_screen_tmp.dart b/lib/presentation/home/screens/home_screen_tmp.dart index 69cbecd..1129bc7 100644 --- a/lib/presentation/home/screens/home_screen_tmp.dart +++ b/lib/presentation/home/screens/home_screen_tmp.dart @@ -51,6 +51,7 @@ class HomeScreenContent extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return SingleChildScrollView( + physics: const ClampingScrollPhysics(), child: Column( mainAxisSize: MainAxisSize.max, children: [ From ae733b7ac019d59ee579ed7bc81897d871591cc4 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 4 Sep 2025 23:06:10 +0900 Subject: [PATCH 07/17] refactor: move app_bloc files to auth folder --- lib/presentation/app/bloc/{ => auth}/app_bloc.dart | 0 lib/presentation/app/bloc/{ => auth}/app_event.dart | 0 lib/presentation/app/bloc/{ => auth}/app_state.dart | 0 lib/presentation/app/screens/app.dart | 2 +- lib/presentation/home/components/home_app_bar.dart | 2 +- lib/presentation/home/screens/home_screen.dart | 2 +- lib/presentation/home/screens/home_screen_tmp.dart | 2 +- lib/presentation/my_page/my_page_screen.dart | 2 +- .../preparation_spare_time_edit_screen.dart | 2 +- .../screens/schedule_spare_and_preparing_time_form.dart | 2 +- lib/presentation/shared/router/go_router.dart | 2 +- 11 files changed, 8 insertions(+), 8 deletions(-) rename lib/presentation/app/bloc/{ => auth}/app_bloc.dart (100%) rename lib/presentation/app/bloc/{ => auth}/app_event.dart (100%) rename lib/presentation/app/bloc/{ => auth}/app_state.dart (100%) diff --git a/lib/presentation/app/bloc/app_bloc.dart b/lib/presentation/app/bloc/auth/app_bloc.dart similarity index 100% rename from lib/presentation/app/bloc/app_bloc.dart rename to lib/presentation/app/bloc/auth/app_bloc.dart diff --git a/lib/presentation/app/bloc/app_event.dart b/lib/presentation/app/bloc/auth/app_event.dart similarity index 100% rename from lib/presentation/app/bloc/app_event.dart rename to lib/presentation/app/bloc/auth/app_event.dart diff --git a/lib/presentation/app/bloc/app_state.dart b/lib/presentation/app/bloc/auth/app_state.dart similarity index 100% rename from lib/presentation/app/bloc/app_state.dart rename to lib/presentation/app/bloc/auth/app_state.dart diff --git a/lib/presentation/app/screens/app.dart b/lib/presentation/app/screens/app.dart index 4af5fd3..4e16e54 100644 --- a/lib/presentation/app/screens/app.dart +++ b/lib/presentation/app/screens/app.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/shared/router/go_router.dart'; import 'package:on_time_front/presentation/shared/theme/theme.dart'; diff --git a/lib/presentation/home/components/home_app_bar.dart b/lib/presentation/home/components/home_app_bar.dart index a17bb8b..fd17260 100644 --- a/lib/presentation/home/components/home_app_bar.dart +++ b/lib/presentation/home/components/home_app_bar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/shared/constants/constants.dart'; class HomeAppBar extends StatelessWidget implements PreferredSizeWidget { diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart index 5ada6ae..06e47e4 100644 --- a/lib/presentation/home/screens/home_screen.dart +++ b/lib/presentation/home/screens/home_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/home/bloc/weekly_schedules_bloc.dart'; import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; import 'package:on_time_front/presentation/home/components/week_calendar.dart'; diff --git a/lib/presentation/home/screens/home_screen_tmp.dart b/lib/presentation/home/screens/home_screen_tmp.dart index 1129bc7..a7f344e 100644 --- a/lib/presentation/home/screens/home_screen_tmp.dart +++ b/lib/presentation/home/screens/home_screen_tmp.dart @@ -3,7 +3,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart'; import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/presentation/my_page/my_page_screen.dart b/lib/presentation/my_page/my_page_screen.dart index 8927c15..3dc386b 100644 --- a/lib/presentation/my_page/my_page_screen.dart +++ b/lib/presentation/my_page/my_page_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; class MyPageScreen extends StatelessWidget { const MyPageScreen({super.key}); diff --git a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart index 958f09f..deca84c 100644 --- a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart +++ b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/my_page/preparation_spare_time_edit/bloc/default_preparation_spare_time_form_bloc.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart'; diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart index c72b4c1..6416428 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/schedule_create/bloc/schedule_form_bloc.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/cubit/schedule_form_spare_time_cubit.dart'; import 'package:on_time_front/presentation/shared/components/cupertino_picker_modal.dart'; diff --git a/lib/presentation/shared/router/go_router.dart b/lib/presentation/shared/router/go_router.dart index 7ab5693..a49860d 100644 --- a/lib/presentation/shared/router/go_router.dart +++ b/lib/presentation/shared/router/go_router.dart @@ -8,8 +8,8 @@ import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; import 'package:on_time_front/presentation/alarm/screens/alarm_screen.dart'; import 'package:on_time_front/presentation/alarm/screens/schedule_start_screen.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; import 'package:on_time_front/presentation/early_late/screens/early_late_screen.dart'; -import 'package:on_time_front/presentation/app/bloc/app_bloc.dart'; import 'package:on_time_front/presentation/calendar/screens/calendar_screen.dart'; import 'package:on_time_front/presentation/home/screens/home_screen_tmp.dart'; import 'package:on_time_front/presentation/login/screens/sign_in_main_screen.dart'; From 38fd7c0b8a397d3fae1a3db049a6049a52ab87c7 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 4 Sep 2025 23:11:01 +0900 Subject: [PATCH 08/17] refactor: replace AppBloc with AuthBloc and restructure authentication events and states - Removed the old AppEvent and AppState files, consolidating authentication logic into AuthBloc. - Introduced AuthEvent and AuthState to manage user authentication and upcoming schedule subscriptions. - Updated references throughout the application to use AuthBloc, ensuring consistent authentication handling. --- lib/presentation/app/bloc/auth/app_event.dart | 29 --------- .../auth/{app_bloc.dart => auth_bloc.dart} | 62 +++++++++---------- .../app/bloc/auth/auth_event.dart | 29 +++++++++ .../auth/{app_state.dart => auth_state.dart} | 26 ++++---- lib/presentation/app/screens/app.dart | 8 +-- .../home/components/home_app_bar.dart | 4 +- .../home/screens/home_screen.dart | 4 +- .../home/screens/home_screen_tmp.dart | 4 +- lib/presentation/my_page/my_page_screen.dart | 6 +- .../preparation_spare_time_edit_screen.dart | 4 +- ...chedule_spare_and_preparing_time_form.dart | 4 +- lib/presentation/shared/router/go_router.dart | 12 ++-- 12 files changed, 96 insertions(+), 96 deletions(-) delete mode 100644 lib/presentation/app/bloc/auth/app_event.dart rename lib/presentation/app/bloc/auth/{app_bloc.dart => auth_bloc.dart} (72%) create mode 100644 lib/presentation/app/bloc/auth/auth_event.dart rename lib/presentation/app/bloc/auth/{app_state.dart => auth_state.dart} (58%) diff --git a/lib/presentation/app/bloc/auth/app_event.dart b/lib/presentation/app/bloc/auth/app_event.dart deleted file mode 100644 index be5ac32..0000000 --- a/lib/presentation/app/bloc/auth/app_event.dart +++ /dev/null @@ -1,29 +0,0 @@ -part of 'app_bloc.dart'; - -abstract class AppEvent { - const AppEvent(); -} - -final class AppUserSubscriptionRequested extends AppEvent { - const AppUserSubscriptionRequested(); -} - -final class AppSignOutPressed extends AppEvent { - const AppSignOutPressed(); -} - -final class AppUpcomingScheduleSubscriptionRequested extends AppEvent { - const AppUpcomingScheduleSubscriptionRequested(); -} - -final class AppUpcomingScheduleReceived extends AppEvent { - final ScheduleWithPreparationEntity? nearestUpcomingSchedule; - - const AppUpcomingScheduleReceived(this.nearestUpcomingSchedule); -} - -final class AppPreparationStarted extends AppEvent { - final ScheduleWithPreparationEntity schedule; - - const AppPreparationStarted(this.schedule); -} diff --git a/lib/presentation/app/bloc/auth/app_bloc.dart b/lib/presentation/app/bloc/auth/auth_bloc.dart similarity index 72% rename from lib/presentation/app/bloc/auth/app_bloc.dart rename to lib/presentation/app/bloc/auth/auth_bloc.dart index 40d4847..c68d690 100644 --- a/lib/presentation/app/bloc/auth/app_bloc.dart +++ b/lib/presentation/app/bloc/auth/auth_bloc.dart @@ -11,23 +11,23 @@ import 'package:on_time_front/domain/use-cases/load_user_use_case.dart'; import 'package:on_time_front/domain/use-cases/sign_out_use_case.dart'; import 'package:on_time_front/domain/use-cases/stream_user_use_case.dart'; -part 'app_event.dart'; -part 'app_state.dart'; +part 'auth_event.dart'; +part 'auth_state.dart'; @Injectable() -class AppBloc extends Bloc { - AppBloc(this._streamUserUseCase, this._signOutUseCase, this._loadUserUseCase, +class AuthBloc extends Bloc { + AuthBloc(this._streamUserUseCase, this._signOutUseCase, this._loadUserUseCase, this._getNearestUpcomingScheduleUseCase, this._navigationService) - : super(AppState(user: const UserEntity.empty())) { - on(_appUserSubscriptionRequested); - on(_appLogoutPressed); - on( + : super(AuthState(user: const UserEntity.empty())) { + on(_appUserSubscriptionRequested); + on(_appLogoutPressed); + on( _appUpcomingScheduleSubscriptionRequested, ); - on( + on( _appUpcomingScheduleReceived, ); - on( + on( _appPreparationStarted, ); } @@ -42,8 +42,8 @@ class AppBloc extends Bloc { _upcomingScheduleSubscription; Future _appUserSubscriptionRequested( - AppUserSubscriptionRequested event, - Emitter emit, + AuthUserSubscriptionRequested event, + Emitter emit, ) { _loadUserUseCase(); return emit.onEach( @@ -52,17 +52,17 @@ class AppBloc extends Bloc { emit( state.copyWith( user: user, - status: user.map( + status: user.map( (entity) => entity.isOnboardingCompleted - ? AppStatus.authenticated - : AppStatus.onboardingNotCompleted, - empty: (_) => AppStatus.unauthenticated, + ? AuthStatus.authenticated + : AuthStatus.onboardingNotCompleted, + empty: (_) => AuthStatus.unauthenticated, ), ), ); await Future.delayed(const Duration(milliseconds: 0)); - if (state.status == AppStatus.authenticated) { - add(const AppUpcomingScheduleSubscriptionRequested()); + if (state.status == AuthStatus.authenticated) { + add(const AuthUpcomingScheduleSubscriptionRequested()); } }, onError: addError, @@ -70,8 +70,8 @@ class AppBloc extends Bloc { } void _appLogoutPressed( - AppSignOutPressed event, - Emitter emit, + AuthSignOutPressed event, + Emitter emit, ) { _signOutUseCase(); } @@ -79,20 +79,20 @@ class AppBloc extends Bloc { /// This method is called when the user is authenticated and the app is /// waiting for the nearest upcoming schedule. FutureOr _appUpcomingScheduleSubscriptionRequested( - AppUpcomingScheduleSubscriptionRequested event, - Emitter emit) async { + AuthUpcomingScheduleSubscriptionRequested event, + Emitter emit) async { await _upcomingScheduleSubscription?.cancel(); _upcomingScheduleSubscription = _getNearestUpcomingScheduleUseCase() - .listen((schedule) => add(AppUpcomingScheduleReceived(schedule))); + .listen((schedule) => add(AuthUpcomingScheduleReceived(schedule))); } /// This method is called when the nearest upcoming schedule is received. void _appUpcomingScheduleReceived( - AppUpcomingScheduleReceived event, Emitter emit) { + AuthUpcomingScheduleReceived event, Emitter emit) { final nearestUpcomingSchedule = event.nearestUpcomingSchedule; // If the app is in preparation started state, we only need to update the schedule. - if (state.status == AppStatus.preparationStarted) { + if (state.status == AuthStatus.preparationStarted) { emit( state.copyWith( schedule: nearestUpcomingSchedule, @@ -106,7 +106,7 @@ class AppBloc extends Bloc { nearestUpcomingSchedule.scheduleTime.isBefore(DateTime.now())) { emit( state.copyWith( - status: AppStatus.authenticated, + status: AuthStatus.authenticated, ), ); return; @@ -116,7 +116,7 @@ class AppBloc extends Bloc { if (_isPreparationOnGoing(nearestUpcomingSchedule)) { emit( state.copyWith( - status: AppStatus.preparationStarted, + status: AuthStatus.preparationStarted, schedule: nearestUpcomingSchedule, ), ); @@ -129,11 +129,11 @@ class AppBloc extends Bloc { assert(!durationUntilSchedule.isNegative); _timer?.cancel(); _timer = Timer(durationUntilSchedule, () { - add(AppPreparationStarted(nearestUpcomingSchedule)); + add(AuthPreparationStarted(nearestUpcomingSchedule)); }); emit( state.copyWith( - status: AppStatus.authenticated, + status: AuthStatus.authenticated, ), ); } @@ -146,11 +146,11 @@ class AppBloc extends Bloc { } void _appPreparationStarted( - AppPreparationStarted event, Emitter emit) async { + AuthPreparationStarted event, Emitter emit) async { _navigationService.push('/scheduleStart', extra: event.schedule); emit( state.copyWith( - status: AppStatus.preparationStarted, + status: AuthStatus.preparationStarted, schedule: event.schedule, ), ); diff --git a/lib/presentation/app/bloc/auth/auth_event.dart b/lib/presentation/app/bloc/auth/auth_event.dart new file mode 100644 index 0000000..21dde42 --- /dev/null +++ b/lib/presentation/app/bloc/auth/auth_event.dart @@ -0,0 +1,29 @@ +part of 'auth_bloc.dart'; + +abstract class AuthEvent { + const AuthEvent(); +} + +final class AuthUserSubscriptionRequested extends AuthEvent { + const AuthUserSubscriptionRequested(); +} + +final class AuthSignOutPressed extends AuthEvent { + const AuthSignOutPressed(); +} + +final class AuthUpcomingScheduleSubscriptionRequested extends AuthEvent { + const AuthUpcomingScheduleSubscriptionRequested(); +} + +final class AuthUpcomingScheduleReceived extends AuthEvent { + final ScheduleWithPreparationEntity? nearestUpcomingSchedule; + + const AuthUpcomingScheduleReceived(this.nearestUpcomingSchedule); +} + +final class AuthPreparationStarted extends AuthEvent { + final ScheduleWithPreparationEntity schedule; + + const AuthPreparationStarted(this.schedule); +} diff --git a/lib/presentation/app/bloc/auth/app_state.dart b/lib/presentation/app/bloc/auth/auth_state.dart similarity index 58% rename from lib/presentation/app/bloc/auth/app_state.dart rename to lib/presentation/app/bloc/auth/auth_state.dart index a9afd34..0e376d0 100644 --- a/lib/presentation/app/bloc/auth/app_state.dart +++ b/lib/presentation/app/bloc/auth/auth_state.dart @@ -1,38 +1,38 @@ -part of 'app_bloc.dart'; +part of 'auth_bloc.dart'; -enum AppStatus { +enum AuthStatus { authenticated, unauthenticated, preparationStarted, onboardingNotCompleted, } -class AppState extends Equatable { - AppState({UserEntity user = const UserEntity.empty()}) +class AuthState extends Equatable { + AuthState({UserEntity user = const UserEntity.empty()}) : this._( - status: user.map( + status: user.map( (entity) => entity.isOnboardingCompleted - ? AppStatus.unauthenticated - : AppStatus.onboardingNotCompleted, - empty: (_) => AppStatus.unauthenticated, + ? AuthStatus.unauthenticated + : AuthStatus.onboardingNotCompleted, + empty: (_) => AuthStatus.unauthenticated, ), user: user, ); - const AppState._( + const AuthState._( {required this.status, this.user = const UserEntity.empty(), this.schedule}); - final AppStatus status; + final AuthStatus status; final UserEntity user; final ScheduleWithPreparationEntity? schedule; - AppState copyWith( - {AppStatus? status, + AuthState copyWith( + {AuthStatus? status, UserEntity? user, ScheduleWithPreparationEntity? schedule}) { - return AppState._( + return AuthState._( status: status ?? this.status, user: user ?? this.user, schedule: schedule ?? this.schedule, diff --git a/lib/presentation/app/screens/app.dart b/lib/presentation/app/screens/app.dart index 4e16e54..552c6da 100644 --- a/lib/presentation/app/screens/app.dart +++ b/lib/presentation/app/screens/app.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/shared/router/go_router.dart'; import 'package:on_time_front/presentation/shared/theme/theme.dart'; @@ -11,9 +11,9 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( + return BlocProvider( create: (context) => - getIt.get()..add(const AppUserSubscriptionRequested()), + getIt.get()..add(const AuthUserSubscriptionRequested()), child: const AppView(), ); } @@ -26,7 +26,7 @@ class AppView extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp.router( theme: themeData, - routerConfig: goRouterConfig(context.read()), + routerConfig: goRouterConfig(context.read()), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, ); diff --git a/lib/presentation/home/components/home_app_bar.dart b/lib/presentation/home/components/home_app_bar.dart index fd17260..e90ecbf 100644 --- a/lib/presentation/home/components/home_app_bar.dart +++ b/lib/presentation/home/components/home_app_bar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/shared/constants/constants.dart'; class HomeAppBar extends StatelessWidget implements PreferredSizeWidget { @@ -35,7 +35,7 @@ class HomeAppBar extends StatelessWidget implements PreferredSizeWidget { IconButton( icon: friendsSvg, onPressed: () { - context.read().add(AppSignOutPressed()); + context.read().add(AuthSignOutPressed()); }, ), IconButton( diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart index 06e47e4..2d8f882 100644 --- a/lib/presentation/home/screens/home_screen.dart +++ b/lib/presentation/home/screens/home_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/home/bloc/weekly_schedules_bloc.dart'; import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; import 'package:on_time_front/presentation/home/components/week_calendar.dart'; @@ -28,7 +28,7 @@ class _HomeScreenState extends State { Widget build(BuildContext context) { final dateOfToday = DateTime( DateTime.now().year, DateTime.now().month, DateTime.now().day, 0, 0, 0); - final double score = context.select((AppBloc bloc) => + final double score = context.select((AuthBloc bloc) => bloc.state.user.mapOrNull((user) => user.score) ?? -1); return BlocProvider( diff --git a/lib/presentation/home/screens/home_screen_tmp.dart b/lib/presentation/home/screens/home_screen_tmp.dart index a7f344e..5719c82 100644 --- a/lib/presentation/home/screens/home_screen_tmp.dart +++ b/lib/presentation/home/screens/home_screen_tmp.dart @@ -3,7 +3,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart'; import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -46,7 +46,7 @@ class HomeScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { final double score = userScore ?? - context.select((AppBloc bloc) => + context.select((AuthBloc bloc) => bloc.state.user.mapOrNull((user) => user.score) ?? -1); final colorScheme = Theme.of(context).colorScheme; diff --git a/lib/presentation/my_page/my_page_screen.dart b/lib/presentation/my_page/my_page_screen.dart index 3dc386b..f510ab6 100644 --- a/lib/presentation/my_page/my_page_screen.dart +++ b/lib/presentation/my_page/my_page_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; class MyPageScreen extends StatelessWidget { const MyPageScreen({super.key}); @@ -56,9 +56,9 @@ class _MyAccountView extends StatelessWidget { Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - if (state.status == AppStatus.authenticated) { + if (state.status == AuthStatus.authenticated) { final user = state.user.mapOrNull( (user) => user, empty: (_) => null, diff --git a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart index deca84c..5ab142b 100644 --- a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart +++ b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/my_page/preparation_spare_time_edit/bloc/default_preparation_spare_time_form_bloc.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart'; @@ -17,7 +17,7 @@ class PreparationSpareTimeEditScreen extends StatelessWidget { return BlocProvider( create: (context) { final spareTime = context - .read() + .read() .state .user .mapOrNull((user) => user.spareTime) ?? diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart index 6416428..51e9de1 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/schedule_create/bloc/schedule_form_bloc.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/cubit/schedule_form_spare_time_cubit.dart'; import 'package:on_time_front/presentation/shared/components/cupertino_picker_modal.dart'; @@ -62,7 +62,7 @@ class _ScheduleSpareAndPreparingTimeFormState builder: (context, spareTimeState) { final Duration spareTime = spareTimeState.spareTime.value ?? spareTimeState.spareTime.value ?? - context.select((AppBloc appBloc) => + context.select((AuthBloc appBloc) => appBloc.state.user.mapOrNull((user) => user.spareTime))!; return Expanded( flex: 1, diff --git a/lib/presentation/shared/router/go_router.dart b/lib/presentation/shared/router/go_router.dart index a49860d..6defd81 100644 --- a/lib/presentation/shared/router/go_router.dart +++ b/lib/presentation/shared/router/go_router.dart @@ -8,7 +8,7 @@ import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; import 'package:on_time_front/presentation/alarm/screens/alarm_screen.dart'; import 'package:on_time_front/presentation/alarm/screens/schedule_start_screen.dart'; -import 'package:on_time_front/presentation/app/bloc/auth/app_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; import 'package:on_time_front/presentation/early_late/screens/early_late_screen.dart'; import 'package:on_time_front/presentation/calendar/screens/calendar_screen.dart'; import 'package:on_time_front/presentation/home/screens/home_screen_tmp.dart'; @@ -27,7 +27,7 @@ import 'package:on_time_front/presentation/shared/utils/stream_to_listenable.dar final GlobalKey navigatorKey = GlobalKey(); -GoRouter goRouterConfig(AppBloc bloc) { +GoRouter goRouterConfig(AuthBloc bloc) { return GoRouter( refreshListenable: StreamToListenable([bloc.stream]), navigatorKey: getIt.get().navigatorKey, @@ -39,9 +39,9 @@ GoRouter goRouterConfig(AppBloc bloc) { final bool onOnboardingScreen = state.fullPath == '/onboarding'; switch (status) { - case AppStatus.unauthenticated: + case AuthStatus.unauthenticated: return '/signIn'; - case AppStatus.authenticated: + case AuthStatus.authenticated: if (onSignInScreen || onOnboardingScreen || onOnbaordingStartScreen) { final permission = await NotificationService.instance .checkNotificationPermission(); @@ -52,13 +52,13 @@ GoRouter goRouterConfig(AppBloc bloc) { } else { return null; } - case AppStatus.onboardingNotCompleted: + case AuthStatus.onboardingNotCompleted: if (onOnboardingScreen || onOnbaordingStartScreen) { return null; } else { return '/onboarding/start'; } - case AppStatus.preparationStarted: + case AuthStatus.preparationStarted: return null; } }, From ed6e8359a2cced69376c4d11c56a35e30484cc1d Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Fri, 5 Sep 2025 19:55:53 +0900 Subject: [PATCH 09/17] feat: implement ScheduleBloc for managing schedule events and states - Introduced ScheduleBloc to handle schedule-related events and states. - Added ScheduleEvent and ScheduleState classes to manage the lifecycle of schedules. - Implemented subscription management for upcoming schedules with proper cleanup on close. - Enhanced state management to reflect ongoing, upcoming, and started schedules. --- .../app/bloc/schedule/schedule_bloc.dart | 113 ++++++++++++++++++ .../app/bloc/schedule/schedule_event.dart | 31 +++++ .../app/bloc/schedule/schedule_state.dart | 45 +++++++ 3 files changed, 189 insertions(+) create mode 100644 lib/presentation/app/bloc/schedule/schedule_bloc.dart create mode 100644 lib/presentation/app/bloc/schedule/schedule_event.dart create mode 100644 lib/presentation/app/bloc/schedule/schedule_state.dart diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart new file mode 100644 index 0000000..b57231a --- /dev/null +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; +import 'package:on_time_front/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart'; + +part 'schedule_event.dart'; +part 'schedule_state.dart'; + +@Injectable() +class ScheduleBloc extends Bloc { + ScheduleBloc(this._getNearestUpcomingScheduleUseCase) + : super(const ScheduleState.initial()) { + on(_onSubscriptionRequested); + on(_onUpcomingReceived); + on(_onScheduleStarted); + } + + final GetNearestUpcomingScheduleUseCase _getNearestUpcomingScheduleUseCase; + StreamSubscription? + _upcomingScheduleSubscription; + Timer? _scheduleStartTimer; + String? _currentScheduleId; + + Future _onSubscriptionRequested( + ScheduleSubscriptionRequested event, Emitter emit) async { + await _upcomingScheduleSubscription?.cancel(); + _upcomingScheduleSubscription = + _getNearestUpcomingScheduleUseCase().listen((upcomingSchedule) { + // βœ… Safety check: Only add events if bloc is still active + if (!isClosed) { + add(ScheduleUpcomingReceived(upcomingSchedule)); + } + }); + } + + Future _onUpcomingReceived( + ScheduleUpcomingReceived event, Emitter emit) async { + // Cancel any existing timer + _scheduleStartTimer?.cancel(); + _scheduleStartTimer = null; + + if (event.upcomingSchedule == null || + event.upcomingSchedule!.scheduleTime.isBefore(DateTime.now())) { + emit(const ScheduleState.notExists()); + _currentScheduleId = null; + } else if (_isPreparationOnGoing(event.upcomingSchedule!)) { + emit(ScheduleState.ongoing(event.upcomingSchedule!)); + _currentScheduleId = event.upcomingSchedule!.id; + _startScheduleTimer(event.upcomingSchedule!); + } else { + emit(ScheduleState.upcoming(event.upcomingSchedule!)); + _currentScheduleId = event.upcomingSchedule!.id; + _startScheduleTimer(event.upcomingSchedule!); + } + } + + Future _onScheduleStarted( + ScheduleStarted event, Emitter emit) async { + // Only process if this event is for the current schedule + if (state.schedule != null && state.schedule!.id == _currentScheduleId) { + // Mark the schedule as started by updating the state + emit(ScheduleState.started(state.schedule!)); + } + } + + void _startScheduleTimer(ScheduleWithPreparationEntity schedule) { + final now = DateTime.now(); + final scheduleTime = schedule.scheduleTime; + + // Calculate time until the next minute boundary at schedule time + final targetTime = DateTime( + scheduleTime.year, + scheduleTime.month, + scheduleTime.day, + scheduleTime.hour, + scheduleTime.minute, + 0, // Always trigger at 00 seconds + 0, // 0 milliseconds + ); + + // If the target time is in the past or now, don't set a timer + if (targetTime.isBefore(now) || targetTime.isAtSameMomentAs(now)) { + return; + } + + final duration = targetTime.difference(now); + + _scheduleStartTimer = Timer(duration, () { + // Only add event if bloc is still active and schedule ID matches + if (!isClosed && _currentScheduleId == schedule.id) { + add(const ScheduleStarted()); + } + }); + } + + @override + Future close() { + // βœ… Proper cleanup: Cancel subscription and timer before closing + _upcomingScheduleSubscription?.cancel(); + _scheduleStartTimer?.cancel(); + return super.close(); + } + + bool _isPreparationOnGoing( + ScheduleWithPreparationEntity nearestUpcomingSchedule) { + return nearestUpcomingSchedule.preparationStartTime + .isBefore(DateTime.now()) && + nearestUpcomingSchedule.scheduleTime.isAfter(DateTime.now()); + } +} diff --git a/lib/presentation/app/bloc/schedule/schedule_event.dart b/lib/presentation/app/bloc/schedule/schedule_event.dart new file mode 100644 index 0000000..9c58eb1 --- /dev/null +++ b/lib/presentation/app/bloc/schedule/schedule_event.dart @@ -0,0 +1,31 @@ +part of 'schedule_bloc.dart'; + +class ScheduleEvent extends Equatable { + const ScheduleEvent(); + + @override + List get props => []; +} + +final class ScheduleSubscriptionRequested extends ScheduleEvent { + const ScheduleSubscriptionRequested(); + + @override + List get props => []; +} + +final class ScheduleUpcomingReceived extends ScheduleEvent { + final ScheduleWithPreparationEntity? upcomingSchedule; + + const ScheduleUpcomingReceived(this.upcomingSchedule); + + @override + List get props => [upcomingSchedule]; +} + +final class ScheduleStarted extends ScheduleEvent { + const ScheduleStarted(); + + @override + List get props => []; +} diff --git a/lib/presentation/app/bloc/schedule/schedule_state.dart b/lib/presentation/app/bloc/schedule/schedule_state.dart new file mode 100644 index 0000000..179859d --- /dev/null +++ b/lib/presentation/app/bloc/schedule/schedule_state.dart @@ -0,0 +1,45 @@ +part of 'schedule_bloc.dart'; + +enum ScheduleStatus { + initial, + notExists, + upcoming, + ongoing, + started, +} + +class ScheduleState extends Equatable { + const ScheduleState._({ + required this.status, + this.schedule, + }); + + const ScheduleState.initial() : this._(status: ScheduleStatus.initial); + + const ScheduleState.notExists() : this._(status: ScheduleStatus.notExists); + + const ScheduleState.upcoming(ScheduleWithPreparationEntity schedule) + : this._(status: ScheduleStatus.upcoming, schedule: schedule); + + const ScheduleState.ongoing(ScheduleWithPreparationEntity schedule) + : this._(status: ScheduleStatus.ongoing, schedule: schedule); + + const ScheduleState.started(ScheduleWithPreparationEntity schedule) + : this._(status: ScheduleStatus.started, schedule: schedule); + + final ScheduleStatus status; + final ScheduleWithPreparationEntity? schedule; + + ScheduleState copyWith({ + ScheduleStatus? status, + ScheduleWithPreparationEntity? schedule, + }) { + return ScheduleState._( + status: status ?? this.status, + schedule: schedule ?? this.schedule, + ); + } + + @override + List get props => [status, schedule]; +} From baaa79b55fe011a51406452a52d0c35639782711 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Fri, 5 Sep 2025 19:55:59 +0900 Subject: [PATCH 10/17] feat: add automatic timer system for schedule notifications - Introduced an automatic timer system within the ScheduleBloc to manage precise timing for schedule start notifications. - Updated Architecture.md to include details about the new timer system and its functionality. - Created Schedule-Timer-System.md for comprehensive documentation on the timer's implementation, flow, and safety features. --- docs/Architecture.md | 3 + docs/Schedule-Timer-System.md | 132 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 docs/Schedule-Timer-System.md diff --git a/docs/Architecture.md b/docs/Architecture.md index 3098b61..e9a36a4 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -273,6 +273,9 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity - **Calendar integration** with multiple view modes - **Preparation time calculation** and management - **Real-time synchronization** between local and remote data +- **Automatic timer system** for schedule start notifications + +πŸ“– **Detailed Documentation**: For comprehensive information about the automatic timer system, see [Schedule Timer System](./Schedule-Timer-System.md) ### 3. **Notification System** diff --git a/docs/Schedule-Timer-System.md b/docs/Schedule-Timer-System.md new file mode 100644 index 0000000..7ffbd55 --- /dev/null +++ b/docs/Schedule-Timer-System.md @@ -0,0 +1,132 @@ +# Schedule Timer System + +The OnTime app includes an automatic timer system within the `ScheduleBloc` that manages precise timing for schedule start notifications. This system ensures that schedule start events are triggered at the exact scheduled time. + +## 🎯 Overview + +The timer system automatically: + +- Starts when an upcoming schedule is received +- Triggers a `ScheduleStarted` event at exactly x minute 00 seconds +- Handles proper cleanup and timer management +- Ensures thread-safety and prevents memory leaks + +## πŸ”„ Timer Flow + +```mermaid +sequenceDiagram + participant UC as UseCase + participant SB as ScheduleBloc + participant Timer as Timer + participant State as State + + UC->>SB: ScheduleUpcomingReceived(schedule) + SB->>SB: Cancel existing timer + SB->>SB: _startScheduleTimer(schedule) + SB->>Timer: Create Timer(targetTime) + Note over Timer: Timer set for schedule.scheduleTime
at x minute 00 seconds + SB->>State: emit(ScheduleState.upcoming/ongoing) + + Timer-->>SB: Timer fires at exact minute + SB->>SB: add(ScheduleStarted()) + SB->>SB: _onScheduleStarted() + SB->>State: emit(ScheduleState.started) + + Note over SB: Timer cleanup on close()
or new schedule received +``` + +## πŸ“‹ Implementation Details + +### Key Components + +1. **Timer Management** + + - `_scheduleStartTimer`: Dart Timer instance that handles the countdown + - `_currentScheduleId`: Tracks the active schedule to prevent stale events + +2. **Event Handling** + + - `ScheduleUpcomingReceived`: Triggers timer setup + - `ScheduleStarted`: Fired when timer completes + +3. **State Transitions** + - `upcoming` β†’ `started`: When timer fires for upcoming schedules + - `ongoing` β†’ `started`: When timer fires for preparation-in-progress schedules + +### Timer Calculation + +The timer calculates the exact target time as: + +```dart +final targetTime = DateTime( + scheduleTime.year, + scheduleTime.month, + scheduleTime.day, + scheduleTime.hour, + scheduleTime.minute, + 0, // Always trigger at 00 seconds + 0, // 0 milliseconds +); +``` + +### Safety Features + +- **Timer Cancellation**: Previous timers are automatically cancelled when new schedules arrive +- **Bloc State Validation**: Timer callbacks verify the bloc is still active before firing events +- **Schedule ID Matching**: Events only fire for the currently tracked schedule +- **Proper Cleanup**: All timers are cancelled when the bloc is disposed + +## πŸ›‘οΈ Error Handling + +The system includes several safety mechanisms: + +1. **Past Schedule Protection**: Timers are not set for schedules in the past +2. **Bloc Lifecycle Management**: Timer callbacks check `isClosed` before adding events +3. **Memory Leak Prevention**: All timers are properly cancelled in `close()` +4. **Race Condition Prevention**: Schedule ID tracking prevents stale timer events + +## πŸ“± Usage Example + +The timer system works automatically within the `ScheduleBloc`: + +```dart +// When a new schedule is received +bloc.add(ScheduleSubscriptionRequested()); + +// The bloc will: +// 1. Listen for upcoming schedules +// 2. Automatically start timers for each schedule +// 3. Emit ScheduleStarted events at the exact scheduled time +// 4. Transition to 'started' state + +// Listen for state changes +bloc.stream.listen((state) { + if (state.status == ScheduleStatus.started) { + // Handle schedule start (e.g., show notification, start tracking) + } +}); +``` + +## πŸ”§ Configuration + +The timer system requires no additional configuration and works automatically with: + +- Any `ScheduleWithPreparationEntity` that has a valid `scheduleTime` +- Both upcoming and ongoing schedule states +- All timezone-aware DateTime calculations + +## πŸ“Š Performance Considerations + +- **Single Timer**: Only one timer runs at a time per bloc instance +- **Minimal Memory Footprint**: Timers are created/destroyed as needed +- **Precise Timing**: Uses Dart's native Timer for accurate scheduling +- **Efficient Cleanup**: No lingering resources after bloc disposal + +## πŸš€ Future Enhancements + +Potential improvements to consider: + +- Multiple concurrent schedule timers +- Configurable timer precision (seconds vs milliseconds) +- Timer persistence across app restarts +- Integration with system-level scheduling APIs From 22aa1ff06cbc5cff36d67bce7d2d1f8f34b2e141 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Fri, 5 Sep 2025 21:08:53 +0900 Subject: [PATCH 11/17] refactor: integrate ScheduleBloc into AuthBloc for schedule management - Removed upcoming schedule subscription logic from AuthBloc and replaced it with ScheduleBloc integration. - Updated AuthBloc to handle authentication events while delegating schedule-related events to ScheduleBloc. - Cleaned up unused events and states in AuthEvent and AuthState related to schedule management. - Enhanced app structure by providing a clearer separation of concerns between authentication and scheduling functionalities. --- lib/presentation/app/bloc/auth/auth_bloc.dart | 99 +------------------ .../app/bloc/auth/auth_event.dart | 16 --- .../app/bloc/auth/auth_state.dart | 1 - .../app/bloc/schedule/schedule_bloc.dart | 26 ++--- lib/presentation/app/screens/app.dart | 17 +++- 5 files changed, 26 insertions(+), 133 deletions(-) diff --git a/lib/presentation/app/bloc/auth/auth_bloc.dart b/lib/presentation/app/bloc/auth/auth_bloc.dart index c68d690..4605f43 100644 --- a/lib/presentation/app/bloc/auth/auth_bloc.dart +++ b/lib/presentation/app/bloc/auth/auth_bloc.dart @@ -3,13 +3,12 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; -import 'package:on_time_front/core/services/navigation_service.dart'; import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; -import 'package:on_time_front/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart'; import 'package:on_time_front/domain/use-cases/load_user_use_case.dart'; import 'package:on_time_front/domain/use-cases/sign_out_use_case.dart'; import 'package:on_time_front/domain/use-cases/stream_user_use_case.dart'; +import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart'; part 'auth_event.dart'; part 'auth_state.dart'; @@ -17,26 +16,16 @@ part 'auth_state.dart'; @Injectable() class AuthBloc extends Bloc { AuthBloc(this._streamUserUseCase, this._signOutUseCase, this._loadUserUseCase, - this._getNearestUpcomingScheduleUseCase, this._navigationService) + this._scheduleBloc) : super(AuthState(user: const UserEntity.empty())) { on(_appUserSubscriptionRequested); on(_appLogoutPressed); - on( - _appUpcomingScheduleSubscriptionRequested, - ); - on( - _appUpcomingScheduleReceived, - ); - on( - _appPreparationStarted, - ); } final StreamUserUseCase _streamUserUseCase; final LoadUserUseCase _loadUserUseCase; final SignOutUseCase _signOutUseCase; - final GetNearestUpcomingScheduleUseCase _getNearestUpcomingScheduleUseCase; - final NavigationService _navigationService; + final ScheduleBloc _scheduleBloc; Timer? _timer; StreamSubscription? _upcomingScheduleSubscription; @@ -62,7 +51,7 @@ class AuthBloc extends Bloc { ); await Future.delayed(const Duration(milliseconds: 0)); if (state.status == AuthStatus.authenticated) { - add(const AuthUpcomingScheduleSubscriptionRequested()); + _scheduleBloc.add(const ScheduleSubscriptionRequested()); } }, onError: addError, @@ -76,86 +65,6 @@ class AuthBloc extends Bloc { _signOutUseCase(); } - /// This method is called when the user is authenticated and the app is - /// waiting for the nearest upcoming schedule. - FutureOr _appUpcomingScheduleSubscriptionRequested( - AuthUpcomingScheduleSubscriptionRequested event, - Emitter emit) async { - await _upcomingScheduleSubscription?.cancel(); - _upcomingScheduleSubscription = _getNearestUpcomingScheduleUseCase() - .listen((schedule) => add(AuthUpcomingScheduleReceived(schedule))); - } - - /// This method is called when the nearest upcoming schedule is received. - void _appUpcomingScheduleReceived( - AuthUpcomingScheduleReceived event, Emitter emit) { - final nearestUpcomingSchedule = event.nearestUpcomingSchedule; - - // If the app is in preparation started state, we only need to update the schedule. - if (state.status == AuthStatus.preparationStarted) { - emit( - state.copyWith( - schedule: nearestUpcomingSchedule, - ), - ); - return; - } - - // If there is no upcoming schedule or the schedule is in the past, the app - if (nearestUpcomingSchedule == null || - nearestUpcomingSchedule.scheduleTime.isBefore(DateTime.now())) { - emit( - state.copyWith( - status: AuthStatus.authenticated, - ), - ); - return; - } - - // If the preparation is ongoing, we need to update the state - if (_isPreparationOnGoing(nearestUpcomingSchedule)) { - emit( - state.copyWith( - status: AuthStatus.preparationStarted, - schedule: nearestUpcomingSchedule, - ), - ); - return; - } - - // If the preparation is not ongoing, we need to set a timer for the preparation start time - final durationUntilSchedule = - nearestUpcomingSchedule.preparationStartTime.difference(DateTime.now()); - assert(!durationUntilSchedule.isNegative); - _timer?.cancel(); - _timer = Timer(durationUntilSchedule, () { - add(AuthPreparationStarted(nearestUpcomingSchedule)); - }); - emit( - state.copyWith( - status: AuthStatus.authenticated, - ), - ); - } - - bool _isPreparationOnGoing( - ScheduleWithPreparationEntity nearestUpcomingSchedule) { - return nearestUpcomingSchedule.preparationStartTime - .isBefore(DateTime.now()) && - nearestUpcomingSchedule.scheduleTime.isAfter(DateTime.now()); - } - - void _appPreparationStarted( - AuthPreparationStarted event, Emitter emit) async { - _navigationService.push('/scheduleStart', extra: event.schedule); - emit( - state.copyWith( - status: AuthStatus.preparationStarted, - schedule: event.schedule, - ), - ); - } - @override Future close() { _timer?.cancel(); diff --git a/lib/presentation/app/bloc/auth/auth_event.dart b/lib/presentation/app/bloc/auth/auth_event.dart index 21dde42..7f1fae1 100644 --- a/lib/presentation/app/bloc/auth/auth_event.dart +++ b/lib/presentation/app/bloc/auth/auth_event.dart @@ -11,19 +11,3 @@ final class AuthUserSubscriptionRequested extends AuthEvent { final class AuthSignOutPressed extends AuthEvent { const AuthSignOutPressed(); } - -final class AuthUpcomingScheduleSubscriptionRequested extends AuthEvent { - const AuthUpcomingScheduleSubscriptionRequested(); -} - -final class AuthUpcomingScheduleReceived extends AuthEvent { - final ScheduleWithPreparationEntity? nearestUpcomingSchedule; - - const AuthUpcomingScheduleReceived(this.nearestUpcomingSchedule); -} - -final class AuthPreparationStarted extends AuthEvent { - final ScheduleWithPreparationEntity schedule; - - const AuthPreparationStarted(this.schedule); -} diff --git a/lib/presentation/app/bloc/auth/auth_state.dart b/lib/presentation/app/bloc/auth/auth_state.dart index 0e376d0..98f3eeb 100644 --- a/lib/presentation/app/bloc/auth/auth_state.dart +++ b/lib/presentation/app/bloc/auth/auth_state.dart @@ -3,7 +3,6 @@ part of 'auth_bloc.dart'; enum AuthStatus { authenticated, unauthenticated, - preparationStarted, onboardingNotCompleted, } diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart index b57231a..ede0520 100644 --- a/lib/presentation/app/bloc/schedule/schedule_bloc.dart +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -41,17 +41,16 @@ class ScheduleBloc extends Bloc { // Cancel any existing timer _scheduleStartTimer?.cancel(); _scheduleStartTimer = null; - if (event.upcomingSchedule == null || event.upcomingSchedule!.scheduleTime.isBefore(DateTime.now())) { emit(const ScheduleState.notExists()); _currentScheduleId = null; } else if (_isPreparationOnGoing(event.upcomingSchedule!)) { emit(ScheduleState.ongoing(event.upcomingSchedule!)); - _currentScheduleId = event.upcomingSchedule!.id; - _startScheduleTimer(event.upcomingSchedule!); + print('ongoingSchedule: ${event.upcomingSchedule}'); } else { emit(ScheduleState.upcoming(event.upcomingSchedule!)); + print('upcomingSchedule: ${event.upcomingSchedule}'); _currentScheduleId = event.upcomingSchedule!.id; _startScheduleTimer(event.upcomingSchedule!); } @@ -62,31 +61,24 @@ class ScheduleBloc extends Bloc { // Only process if this event is for the current schedule if (state.schedule != null && state.schedule!.id == _currentScheduleId) { // Mark the schedule as started by updating the state + print('schedule started: ${state.schedule}'); emit(ScheduleState.started(state.schedule!)); } } void _startScheduleTimer(ScheduleWithPreparationEntity schedule) { final now = DateTime.now(); - final scheduleTime = schedule.scheduleTime; - - // Calculate time until the next minute boundary at schedule time - final targetTime = DateTime( - scheduleTime.year, - scheduleTime.month, - scheduleTime.day, - scheduleTime.hour, - scheduleTime.minute, - 0, // Always trigger at 00 seconds - 0, // 0 milliseconds - ); + final preparationStartTime = schedule.preparationStartTime; // If the target time is in the past or now, don't set a timer - if (targetTime.isBefore(now) || targetTime.isAtSameMomentAs(now)) { + if (preparationStartTime.isBefore(now) || + preparationStartTime.isAtSameMomentAs(now)) { return; } - final duration = targetTime.difference(now); + final duration = preparationStartTime.difference(now); + + print('duration: $duration'); _scheduleStartTimer = Timer(duration, () { // Only add event if bloc is still active and schedule ID matches diff --git a/lib/presentation/app/screens/app.dart b/lib/presentation/app/screens/app.dart index 552c6da..9a6c3cc 100644 --- a/lib/presentation/app/screens/app.dart +++ b/lib/presentation/app/screens/app.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart'; import 'package:on_time_front/presentation/shared/router/go_router.dart'; import 'package:on_time_front/presentation/shared/theme/theme.dart'; @@ -11,9 +12,16 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - getIt.get()..add(const AuthUserSubscriptionRequested()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt.get()..add(const AuthUserSubscriptionRequested()), + ), + BlocProvider( + create: (context) => getIt.get(), + ), + ], child: const AppView(), ); } @@ -26,7 +34,8 @@ class AppView extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp.router( theme: themeData, - routerConfig: goRouterConfig(context.read()), + routerConfig: goRouterConfig( + context.read(), context.read()), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, ); From 3143f9c5a7b4a3296baad96d622ba2e225940c49 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 7 Sep 2025 12:32:22 +0900 Subject: [PATCH 12/17] refactor: update ScheduleBloc to use Singleton and rename state emission - Changed the ScheduleBloc annotation from @Injectable to @Singleton for improved instance management. - Renamed the state emission from ScheduleState.started to ScheduleState.starting to better reflect the state transition during schedule initiation. --- lib/presentation/app/bloc/schedule/schedule_bloc.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart index ede0520..faf0593 100644 --- a/lib/presentation/app/bloc/schedule/schedule_bloc.dart +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -9,7 +9,7 @@ import 'package:on_time_front/domain/use-cases/get_nearest_upcoming_schedule_use part 'schedule_event.dart'; part 'schedule_state.dart'; -@Injectable() +@Singleton() class ScheduleBloc extends Bloc { ScheduleBloc(this._getNearestUpcomingScheduleUseCase) : super(const ScheduleState.initial()) { @@ -62,7 +62,7 @@ class ScheduleBloc extends Bloc { if (state.schedule != null && state.schedule!.id == _currentScheduleId) { // Mark the schedule as started by updating the state print('schedule started: ${state.schedule}'); - emit(ScheduleState.started(state.schedule!)); + emit(ScheduleState.starting(state.schedule!)); } } From 2a52a8a160da29e37d6b600e9962870d169c50a3 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 7 Sep 2025 12:32:34 +0900 Subject: [PATCH 13/17] feat: enhance schedule state management and routing - Added a new ScheduleStatus 'starting' to represent the initial phase of a schedule. - Introduced a new ScheduleState constructor for 'starting' to manage the corresponding state. - Updated GoRouter configuration to include ScheduleBloc, allowing for routing based on the new 'starting' state. - Enhanced the redirect logic to navigate to the '/scheduleStart' route when the schedule is in the 'starting' state. --- .../app/bloc/schedule/schedule_state.dart | 4 ++++ lib/presentation/shared/router/go_router.dart | 16 ++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/presentation/app/bloc/schedule/schedule_state.dart b/lib/presentation/app/bloc/schedule/schedule_state.dart index 179859d..04b6af2 100644 --- a/lib/presentation/app/bloc/schedule/schedule_state.dart +++ b/lib/presentation/app/bloc/schedule/schedule_state.dart @@ -5,6 +5,7 @@ enum ScheduleStatus { notExists, upcoming, ongoing, + starting, started, } @@ -24,6 +25,9 @@ class ScheduleState extends Equatable { const ScheduleState.ongoing(ScheduleWithPreparationEntity schedule) : this._(status: ScheduleStatus.ongoing, schedule: schedule); + const ScheduleState.starting(ScheduleWithPreparationEntity schedule) + : this._(status: ScheduleStatus.starting, schedule: schedule); + const ScheduleState.started(ScheduleWithPreparationEntity schedule) : this._(status: ScheduleStatus.started, schedule: schedule); diff --git a/lib/presentation/shared/router/go_router.dart b/lib/presentation/shared/router/go_router.dart index 6defd81..3489f03 100644 --- a/lib/presentation/shared/router/go_router.dart +++ b/lib/presentation/shared/router/go_router.dart @@ -9,6 +9,7 @@ import 'package:on_time_front/domain/entities/schedule_entity.dart'; import 'package:on_time_front/presentation/alarm/screens/alarm_screen.dart'; import 'package:on_time_front/presentation/alarm/screens/schedule_start_screen.dart'; import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart'; import 'package:on_time_front/presentation/early_late/screens/early_late_screen.dart'; import 'package:on_time_front/presentation/calendar/screens/calendar_screen.dart'; import 'package:on_time_front/presentation/home/screens/home_screen_tmp.dart'; @@ -27,18 +28,21 @@ import 'package:on_time_front/presentation/shared/utils/stream_to_listenable.dar final GlobalKey navigatorKey = GlobalKey(); -GoRouter goRouterConfig(AuthBloc bloc) { +GoRouter goRouterConfig(AuthBloc authBloc, ScheduleBloc scheduleBloc) { return GoRouter( - refreshListenable: StreamToListenable([bloc.stream]), + refreshListenable: + StreamToListenable([scheduleBloc.stream, authBloc.stream]), navigatorKey: getIt.get().navigatorKey, redirect: (BuildContext context, GoRouterState state) async { - final status = bloc.state.status; + print('state.fullPath: ${state.fullPath}'); + final authStatus = authBloc.state.status; + final scheduleStatus = scheduleBloc.state.status; final bool onSignInScreen = state.fullPath == '/signIn'; final bool onOnbaordingStartScreen = state.fullPath == '/onboarding/start'; final bool onOnboardingScreen = state.fullPath == '/onboarding'; - switch (status) { + switch (authStatus) { case AuthStatus.unauthenticated: return '/signIn'; case AuthStatus.authenticated: @@ -49,6 +53,8 @@ GoRouter goRouterConfig(AuthBloc bloc) { return '/allowNotification'; } return '/home'; + } else if (scheduleStatus == ScheduleStatus.starting) { + return '/scheduleStart'; } else { return null; } @@ -58,8 +64,6 @@ GoRouter goRouterConfig(AuthBloc bloc) { } else { return '/onboarding/start'; } - case AuthStatus.preparationStarted: - return null; } }, initialLocation: '/home', From 324801e243bc628044a8a1f828252856f1cf7099 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 8 Sep 2025 14:18:24 +0900 Subject: [PATCH 14/17] feat: enhance ScheduleBloc with preparation step management and navigation - Added new entities for preparation steps and their management within the ScheduleBloc. - Updated ScheduleState to include the current preparation step during ongoing schedules. - Refactored state emissions to accommodate the new preparation logic and improved navigation to the schedule start screen. - Introduced methods to convert schedules to include preparation timing details. --- .../app/bloc/schedule/schedule_bloc.dart | 76 +++++++++++++++++-- .../app/bloc/schedule/schedule_event.dart | 7 ++ .../app/bloc/schedule/schedule_state.dart | 46 +++++++++-- lib/presentation/shared/router/go_router.dart | 4 +- 4 files changed, 119 insertions(+), 14 deletions(-) diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart index faf0593..f202d07 100644 --- a/lib/presentation/app/bloc/schedule/schedule_bloc.dart +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; +import 'package:on_time_front/core/services/navigation_service.dart'; +import 'package:on_time_front/domain/entities/preparation_entity.dart'; +import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; import 'package:on_time_front/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart'; @@ -11,7 +14,7 @@ part 'schedule_state.dart'; @Singleton() class ScheduleBloc extends Bloc { - ScheduleBloc(this._getNearestUpcomingScheduleUseCase) + ScheduleBloc(this._getNearestUpcomingScheduleUseCase, this._navigationService) : super(const ScheduleState.initial()) { on(_onSubscriptionRequested); on(_onUpcomingReceived); @@ -19,6 +22,7 @@ class ScheduleBloc extends Bloc { } final GetNearestUpcomingScheduleUseCase _getNearestUpcomingScheduleUseCase; + final NavigationService _navigationService; StreamSubscription? _upcomingScheduleSubscription; Timer? _scheduleStartTimer; @@ -31,7 +35,10 @@ class ScheduleBloc extends Bloc { _getNearestUpcomingScheduleUseCase().listen((upcomingSchedule) { // βœ… Safety check: Only add events if bloc is still active if (!isClosed) { - add(ScheduleUpcomingReceived(upcomingSchedule)); + final scheduleWithTimePreparation = upcomingSchedule != null + ? _convertToScheduleWithTimePreparation(upcomingSchedule) + : null; + add(ScheduleUpcomingReceived(scheduleWithTimePreparation)); } }); } @@ -46,8 +53,13 @@ class ScheduleBloc extends Bloc { emit(const ScheduleState.notExists()); _currentScheduleId = null; } else if (_isPreparationOnGoing(event.upcomingSchedule!)) { - emit(ScheduleState.ongoing(event.upcomingSchedule!)); - print('ongoingSchedule: ${event.upcomingSchedule}'); + final currentStep = _findCurrentPreparationStep( + event.upcomingSchedule!, + DateTime.now(), + ); + emit(ScheduleState.ongoing(event.upcomingSchedule!, currentStep)); + print( + 'ongoingSchedule: ${event.upcomingSchedule}, currentStep: $currentStep'); } else { emit(ScheduleState.upcoming(event.upcomingSchedule!)); print('upcomingSchedule: ${event.upcomingSchedule}'); @@ -61,8 +73,9 @@ class ScheduleBloc extends Bloc { // Only process if this event is for the current schedule if (state.schedule != null && state.schedule!.id == _currentScheduleId) { // Mark the schedule as started by updating the state - print('schedule started: ${state.schedule}'); - emit(ScheduleState.starting(state.schedule!)); + print('scheddle started: ${state.schedule}'); + emit(ScheduleState.started(state.schedule!)); + _navigationService.push('/scheduleStart', extra: state.schedule); } } @@ -102,4 +115,55 @@ class ScheduleBloc extends Bloc { .isBefore(DateTime.now()) && nearestUpcomingSchedule.scheduleTime.isAfter(DateTime.now()); } + + PreparationStepEntity _findCurrentPreparationStep( + ScheduleWithPreparationEntity schedule, DateTime now) { + final List steps = + schedule.preparation.preparationStepList.cast(); + + if (steps.isEmpty) { + throw StateError('Preparation steps are empty'); + } + + final DateTime preparationStartTime = schedule.preparationStartTime; + + // If called when not in preparation window, clamp to bounds + if (now.isBefore(preparationStartTime)) { + return steps.first; + } + + Duration elapsed = now.difference(preparationStartTime); + + for (final PreprationStepWithTime step in steps) { + if (elapsed < step.preparationTime) { + step.elapsedTime = elapsed; + return step; + } + elapsed -= step.preparationTime; + step.elapsedTime = step.preparationTime; + } + + // If elapsed exceeds total preparation duration (e.g., during move/spare time), + // return the last preparation step as current by convention. + return steps.last; + } + + ScheduleWithPreparationEntity _convertToScheduleWithTimePreparation( + ScheduleWithPreparationEntity schedule) { + final preparationWithTime = PreparationWithTime( + preparationStepList: schedule.preparation.preparationStepList + .map((step) => PreprationStepWithTime( + id: step.id, + preparationName: step.preparationName, + preparationTime: step.preparationTime, + nextPreparationId: step.nextPreparationId, + )) + .toList(), + ); + + return ScheduleWithPreparationEntity.fromScheduleAndPreparationEntity( + schedule, + preparationWithTime, + ); + } } diff --git a/lib/presentation/app/bloc/schedule/schedule_event.dart b/lib/presentation/app/bloc/schedule/schedule_event.dart index 9c58eb1..98544f5 100644 --- a/lib/presentation/app/bloc/schedule/schedule_event.dart +++ b/lib/presentation/app/bloc/schedule/schedule_event.dart @@ -29,3 +29,10 @@ final class ScheduleStarted extends ScheduleEvent { @override List get props => []; } + +final class SchedulePreparationStarted extends ScheduleEvent { + const SchedulePreparationStarted(); + + @override + List get props => []; +} diff --git a/lib/presentation/app/bloc/schedule/schedule_state.dart b/lib/presentation/app/bloc/schedule/schedule_state.dart index 04b6af2..b54fc5f 100644 --- a/lib/presentation/app/bloc/schedule/schedule_state.dart +++ b/lib/presentation/app/bloc/schedule/schedule_state.dart @@ -5,7 +5,6 @@ enum ScheduleStatus { notExists, upcoming, ongoing, - starting, started, } @@ -13,6 +12,7 @@ class ScheduleState extends Equatable { const ScheduleState._({ required this.status, this.schedule, + this.currentStep, }); const ScheduleState.initial() : this._(status: ScheduleStatus.initial); @@ -22,28 +22,62 @@ class ScheduleState extends Equatable { const ScheduleState.upcoming(ScheduleWithPreparationEntity schedule) : this._(status: ScheduleStatus.upcoming, schedule: schedule); - const ScheduleState.ongoing(ScheduleWithPreparationEntity schedule) - : this._(status: ScheduleStatus.ongoing, schedule: schedule); - - const ScheduleState.starting(ScheduleWithPreparationEntity schedule) - : this._(status: ScheduleStatus.starting, schedule: schedule); + const ScheduleState.ongoing( + ScheduleWithPreparationEntity schedule, PreparationStepEntity currentStep) + : this._( + status: ScheduleStatus.ongoing, + schedule: schedule, + currentStep: currentStep); const ScheduleState.started(ScheduleWithPreparationEntity schedule) : this._(status: ScheduleStatus.started, schedule: schedule); final ScheduleStatus status; final ScheduleWithPreparationEntity? schedule; + final PreparationStepEntity? currentStep; ScheduleState copyWith({ ScheduleStatus? status, ScheduleWithPreparationEntity? schedule, + PreparationStepEntity? currentStep, }) { return ScheduleState._( status: status ?? this.status, schedule: schedule ?? this.schedule, + currentStep: currentStep ?? this.currentStep, ); } @override List get props => [status, schedule]; } + +class PreprationStepWithTime extends PreparationStepEntity + implements Equatable { + Duration elapsedTime; + + PreprationStepWithTime({ + required super.id, + required super.preparationName, + required super.preparationTime, + required super.nextPreparationId, + this.elapsedTime = Duration.zero, + }); + + @override + List get props => + [id, preparationName, preparationTime, nextPreparationId, elapsedTime]; +} + +class PreparationWithTime extends PreparationEntity implements Equatable { + const PreparationWithTime({ + required List preparationStepList, + }) : super(preparationStepList: preparationStepList); + + @override + List get preparationStepList => + super.preparationStepList.cast(); + + @override + List get props => [preparationStepList]; +} diff --git a/lib/presentation/shared/router/go_router.dart b/lib/presentation/shared/router/go_router.dart index 3489f03..d455844 100644 --- a/lib/presentation/shared/router/go_router.dart +++ b/lib/presentation/shared/router/go_router.dart @@ -53,8 +53,8 @@ GoRouter goRouterConfig(AuthBloc authBloc, ScheduleBloc scheduleBloc) { return '/allowNotification'; } return '/home'; - } else if (scheduleStatus == ScheduleStatus.starting) { - return '/scheduleStart'; + } else if (scheduleStatus == ScheduleStatus.started) { + return null; } else { return null; } From 34e1e11ce0ece53c7bdb6830fc7118c4278fe143 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 8 Sep 2025 21:43:13 +0900 Subject: [PATCH 15/17] refactor: update TokenInterceptor to use dependency injection and improve logout handling - Replaced direct instantiation of TokenLocalDataSource with dependency injection using getIt. - Enhanced logout logic to utilize SignOutUseCase, ensuring a more robust handling of token deletion upon refresh failure. - Adjusted UserRepositoryImpl to correctly handle authentication state changes by checking for false instead of true. --- lib/core/dio/interceptors/token_interceptor.dart | 12 ++++++++++-- lib/data/repositories/user_repository_impl.dart | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/core/dio/interceptors/token_interceptor.dart b/lib/core/dio/interceptors/token_interceptor.dart index cd39127..c151ce6 100644 --- a/lib/core/dio/interceptors/token_interceptor.dart +++ b/lib/core/dio/interceptors/token_interceptor.dart @@ -1,11 +1,14 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; +import 'package:on_time_front/core/di/di_setup.dart'; +import 'package:on_time_front/domain/use-cases/sign_out_use_case.dart'; class TokenInterceptor implements InterceptorsWrapper { final Dio dio; TokenInterceptor(this.dio); - final TokenLocalDataSource tokenLocalDataSource = TokenLocalDataSourceImpl(); + final TokenLocalDataSource tokenLocalDataSource = + getIt.get(); // when accessToken is expired & having multiple requests call // this variable to lock others request to make sure only trigger call refresh token 01 times @@ -73,7 +76,12 @@ class TokenInterceptor implements InterceptorsWrapper { } else { _requestsNeedRetry.clear(); // if refresh fail, force logout user here - await tokenLocalDataSource.deleteToken(); + try { + await getIt.get().call(); + } catch (_) { + await tokenLocalDataSource.deleteToken(); + } + _isRefreshing = false; } } else { // if refresh flow is processing, add this request to queue and wait to retry later diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index 9dddf5f..189ad0a 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -27,7 +27,7 @@ class UserRepositoryImpl implements UserRepository { UserRepositoryImpl( this._authenticationRemoteDataSource, this._tokenLocalDataSource) { _tokenLocalDataSource.authenticationStream.listen((state) { - if (state) { + if (!state) { _userStreamController.add(const UserEntity.empty()); } }); From 83ceeef0e096cd3d7c981630170998ec96918455 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 8 Sep 2025 21:56:40 +0900 Subject: [PATCH 16/17] refactor: replace print statements with debugPrint for better logging - Updated UserRepositoryImpl to remove print statement for idToken. - Replaced print statements with debugPrint in AlarmTimerBloc for null checks and timer ticks. - Changed print statements to debugPrint in ScheduleBloc for ongoing and upcoming schedule logs. - Enhanced logging consistency across the application by using debugPrint. --- lib/data/repositories/user_repository_impl.dart | 1 - .../alarm/bloc/alarm_timer/alarm_timer_bloc.dart | 7 ++++--- lib/presentation/app/bloc/schedule/schedule_bloc.dart | 9 +++++---- lib/presentation/shared/router/go_router.dart | 1 - 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index 189ad0a..2327a14 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -87,7 +87,6 @@ class UserRepositoryImpl implements UserRepository { final signInWithGoogleRequestModel = SignInWithGoogleRequestModel( idToken: idToken, ); - print(idToken); await _tokenLocalDataSource.deleteToken(); final result = await _authenticationRemoteDataSource .signInWithGoogle(signInWithGoogleRequestModel); diff --git a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart b/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart index f42ce82..864fd44 100644 --- a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart +++ b/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart @@ -1,6 +1,7 @@ library; import 'dart:async'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:injectable/injectable.dart'; @@ -117,7 +118,7 @@ class AlarmTimerBloc extends Bloc { _tickerSubscription?.cancel(); if (_stepStartTime == null || _beforeOutStartTime == null) { - print("_stepStartTime or _beforeOutStartTime is null"); + debugPrint("_stepStartTime or _beforeOutStartTime is null"); return; } @@ -136,7 +137,7 @@ class AlarmTimerBloc extends Bloc { final updatedIsLate = updatedBeforeOutTime <= 0; if (newRemaining >= 0) { - print("타이머 tick: $newRemaining초 λ‚¨μŒ"); + debugPrint("타이머 tick: $newRemaining초 λ‚¨μŒ"); add(AlarmTimerStepTicked( preparationRemainingTime: newRemaining, preparationElapsedTime: elapsed, @@ -145,7 +146,7 @@ class AlarmTimerBloc extends Bloc { isLate: updatedIsLate, )); } else { - print("타이머 μ™„λ£Œλ¨"); + debugPrint("타이머 μ™„λ£Œλ¨"); add(const AlarmTimerStepNextShifted()); } }); diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart index f202d07..66cb1c7 100644 --- a/lib/presentation/app/bloc/schedule/schedule_bloc.dart +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:on_time_front/core/services/navigation_service.dart'; @@ -58,11 +59,11 @@ class ScheduleBloc extends Bloc { DateTime.now(), ); emit(ScheduleState.ongoing(event.upcomingSchedule!, currentStep)); - print( + debugPrint( 'ongoingSchedule: ${event.upcomingSchedule}, currentStep: $currentStep'); } else { emit(ScheduleState.upcoming(event.upcomingSchedule!)); - print('upcomingSchedule: ${event.upcomingSchedule}'); + debugPrint('upcomingSchedule: ${event.upcomingSchedule}'); _currentScheduleId = event.upcomingSchedule!.id; _startScheduleTimer(event.upcomingSchedule!); } @@ -73,7 +74,7 @@ class ScheduleBloc extends Bloc { // Only process if this event is for the current schedule if (state.schedule != null && state.schedule!.id == _currentScheduleId) { // Mark the schedule as started by updating the state - print('scheddle started: ${state.schedule}'); + debugPrint('scheddle started: ${state.schedule}'); emit(ScheduleState.started(state.schedule!)); _navigationService.push('/scheduleStart', extra: state.schedule); } @@ -91,7 +92,7 @@ class ScheduleBloc extends Bloc { final duration = preparationStartTime.difference(now); - print('duration: $duration'); + debugPrint('duration: $duration'); _scheduleStartTimer = Timer(duration, () { // Only add event if bloc is still active and schedule ID matches diff --git a/lib/presentation/shared/router/go_router.dart b/lib/presentation/shared/router/go_router.dart index d455844..75e7d47 100644 --- a/lib/presentation/shared/router/go_router.dart +++ b/lib/presentation/shared/router/go_router.dart @@ -34,7 +34,6 @@ GoRouter goRouterConfig(AuthBloc authBloc, ScheduleBloc scheduleBloc) { StreamToListenable([scheduleBloc.stream, authBloc.stream]), navigatorKey: getIt.get().navigatorKey, redirect: (BuildContext context, GoRouterState state) async { - print('state.fullPath: ${state.fullPath}'); final authStatus = authBloc.state.status; final scheduleStatus = scheduleBloc.state.status; final bool onSignInScreen = state.fullPath == '/signIn'; From 6f5233c99ecb8adb7108a951aae9495000b8f28e Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 11 Sep 2025 17:13:39 +0900 Subject: [PATCH 17/17] refactor: correct spelling and enhance PreparationStepWithTime handling - Renamed PreprationStepWithTime to PreparationStepWithTime for consistency. - Updated methods to use the new class name and improved elapsed time handling with a copyWithElapsed method. - Ensured proper casting of preparation step lists in ScheduleBloc and ScheduleState. --- .../app/bloc/schedule/schedule_bloc.dart | 15 ++++++------- .../app/bloc/schedule/schedule_state.dart | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart index 66cb1c7..f1ed80c 100644 --- a/lib/presentation/app/bloc/schedule/schedule_bloc.dart +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -119,8 +119,9 @@ class ScheduleBloc extends Bloc { PreparationStepEntity _findCurrentPreparationStep( ScheduleWithPreparationEntity schedule, DateTime now) { - final List steps = - schedule.preparation.preparationStepList.cast(); + final List steps = schedule + .preparation.preparationStepList + .cast(); if (steps.isEmpty) { throw StateError('Preparation steps are empty'); @@ -135,25 +136,23 @@ class ScheduleBloc extends Bloc { Duration elapsed = now.difference(preparationStartTime); - for (final PreprationStepWithTime step in steps) { + for (final PreparationStepWithTime step in steps) { if (elapsed < step.preparationTime) { - step.elapsedTime = elapsed; - return step; + return step.copyWithElapsed(elapsed); } elapsed -= step.preparationTime; - step.elapsedTime = step.preparationTime; } // If elapsed exceeds total preparation duration (e.g., during move/spare time), // return the last preparation step as current by convention. - return steps.last; + return steps.last.copyWithElapsed(steps.last.preparationTime); } ScheduleWithPreparationEntity _convertToScheduleWithTimePreparation( ScheduleWithPreparationEntity schedule) { final preparationWithTime = PreparationWithTime( preparationStepList: schedule.preparation.preparationStepList - .map((step) => PreprationStepWithTime( + .map((step) => PreparationStepWithTime( id: step.id, preparationName: step.preparationName, preparationTime: step.preparationTime, diff --git a/lib/presentation/app/bloc/schedule/schedule_state.dart b/lib/presentation/app/bloc/schedule/schedule_state.dart index b54fc5f..c9259d7 100644 --- a/lib/presentation/app/bloc/schedule/schedule_state.dart +++ b/lib/presentation/app/bloc/schedule/schedule_state.dart @@ -52,11 +52,11 @@ class ScheduleState extends Equatable { List get props => [status, schedule]; } -class PreprationStepWithTime extends PreparationStepEntity +class PreparationStepWithTime extends PreparationStepEntity implements Equatable { - Duration elapsedTime; + final Duration elapsedTime; - PreprationStepWithTime({ + const PreparationStepWithTime({ required super.id, required super.preparationName, required super.preparationTime, @@ -64,6 +64,16 @@ class PreprationStepWithTime extends PreparationStepEntity this.elapsedTime = Duration.zero, }); + PreparationStepWithTime copyWithElapsed(Duration elapsed) { + return PreparationStepWithTime( + id: id, + preparationName: preparationName, + preparationTime: preparationTime, + nextPreparationId: nextPreparationId, + elapsedTime: elapsed, + ); + } + @override List get props => [id, preparationName, preparationTime, nextPreparationId, elapsedTime]; @@ -71,12 +81,12 @@ class PreprationStepWithTime extends PreparationStepEntity class PreparationWithTime extends PreparationEntity implements Equatable { const PreparationWithTime({ - required List preparationStepList, + required List preparationStepList, }) : super(preparationStepList: preparationStepList); @override - List get preparationStepList => - super.preparationStepList.cast(); + List get preparationStepList => + super.preparationStepList.cast(); @override List get props => [preparationStepList];