Skip to content

Conversation

@geeksbaek
Copy link
Collaborator

@geeksbaek geeksbaek commented Dec 31, 2025

Summary

  • 무 가격 입력 및 패턴 예측 기능 구현
  • 변동형/3기형(대폭등)/감소형/4기형(소폭등) 패턴 분석
  • CoreData를 통한 주간 무 가격 데이터 저장
  • 대시보드 메뉴에서 접근 가능

Changes

  • TurnipPrice 모델 및 TurnipPricePredictor 패턴 예측 알고리즘
  • CoreData TurnipPriceEntityTurnipPriceStorage 구현
  • TurnipCalculatorViewController/TurnipCalculatorReactor 구현
  • UI 컴포넌트: TurnipPriceInputCell, TurnipPredictionView, TurnipSummaryView

Test plan

  • 일요일 구매 가격 입력 테스트
  • 월~토 오전/오후 가격 입력 테스트
  • 패턴 예측 결과 확인
  • 앱 재시작 후 데이터 유지 확인

Closes #29

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Turnip Calculator added to dashboard with full UI for weekly buy/sell inputs, per-slot price entry, summary (min/max) and prediction views.
    • New turnip data model and prediction engine providing pattern forecasts and expected price ranges.
    • Persistent turnip price storage and public storage API with fetch, save, update and delete operations; integrated into app flows.
  • Bug Fixes

    • Improved error messaging for data save failures.

✏️ Tip: You can customize this high-level summary in your review settings.

- TurnipPrice 모델 및 TurnipPricePredictor 패턴 예측 알고리즘 구현
- CoreData TurnipPriceEntity 추가
- TurnipPriceStorage 구현 (CoreData 저장/조회)
- TurnipCalculatorViewController/Reactor 구현
- 무 가격 패턴 분석 (변동형/3기형/감소형/4기형) 기능
- Dashboard 메뉴에서 무값 계산기 접근 가능

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

Adds a Turnip Calculator feature: Core Data model/entity and storage, domain models and predictor, ReactorKit reactor, UI (view controller, cells, prediction/summary views), dashboard route, and CoreData error extension.

Changes

Cohort / File(s) Summary
Core Data Model
CoreDataStorage.xcdatamodeld/.../contents
Added TurnipPriceEntity (id, buyPrice, prices, weekStartDate, createdDate) with to-one userCollection; UserCollectionEntity gains to-many turnipPrices inverse.
Storage Protocol & Impl
.../TurnipPriceStorage/TurnipPriceStorage.swift, .../TurnipPriceStorage/CoreDataTurnipPriceStorage.swift
New TurnipPriceStorage protocol (RxSwift Single CRUD) and CoreDataTurnipPriceStorage implementing background Core Data fetch/upsert/delete with error mapping and merge-policy control.
Entity Mapping
.../TurnipPriceStorage/EntityMapping/TurnipPriceEntity+Mapping.swift
Added init from domain TurnipPrice and toDomain() mapping; encodes nil prices as -1, handles defaults for dates, buyPrice, id, and prices array length.
Domain & Predictor
Sources/Models/TurnipPrice.swift, Sources/Models/TurnipPricePredictor.swift
New TurnipPrice model and TurnipPattern enum; TurnipPricePredictor and TurnipPrediction produce heuristic predictions and expected price ranges.
Dashboard Integration
.../Presentation/Dashboard/.../DashboardCoordinator.swift, .../DashboardReactor.swift, .../DashboardViewController.swift
Added .turnipCalculator route, Menu.turnipCalculator option, and dashboard presentation logic to open the calculator modally; added disposal for selection binding.
Turnip Calculator Reactor
.../TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift
New ReactorKit TurnipCalculatorReactor (Action/Mutation/State) handling load/save/update/clear, predictions, expected range, loading/error states, persistence via injected TurnipPriceStorage.
Turnip Calculator UI
.../TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift, .../Views/TurnipPredictionView.swift, .../Views/TurnipPriceInputCell.swift, .../Views/TurnipSummaryView.swift
New UI components: controller, prediction card view, price input table cell (with Rx bindings and reuse-safe DisposeBag), and summary view; bindings to reactor state and dynamic prediction rendering.
CoreData Error
Sources/CoreDataStorage/CoreDataStorage.swift
Added CoreDataStorageError.saveError(Error) with localized description handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant VC as TurnipCalculatorVC
    participant Reactor as TurnipCalculatorReactor
    participant Storage as TurnipPriceStorage
    participant Predictor as TurnipPricePredictor
    participant CoreData as Core Data

    Note over User,VC: Open calculator
    User->>VC: open
    VC->>Reactor: viewDidLoad
    Reactor->>Storage: fetchCurrentWeekPrice()
    Storage->>CoreData: query TurnipPriceEntity
    CoreData-->>Storage: entity?
    Storage-->>Reactor: TurnipPrice?
    Reactor-->>VC: set state (turnipPrice)

    Note over User,VC: Enter prices
    User->>VC: updateBuyPrice / updatePrice(index)
    VC->>Reactor: actions
    Reactor->>Predictor: predict(turnipPrice)
    Predictor-->>Reactor: [TurnipPrediction]
    Reactor-->>VC: update predictions/range

    Note over User,VC: Save flow
    User->>VC: tap Save
    VC->>Reactor: save()
    Reactor->>Storage: savePrice(turnipPrice)
    Storage->>CoreData: upsert TurnipPriceEntity
    CoreData-->>Storage: persisted
    Storage-->>Reactor: saved TurnipPrice
    Reactor-->>VC: reflect saved state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I dug through rows and plotted each spike,
Bells in twelve slots, morning to night,
I nibble inputs and whisper a trend,
Predictions in paw, from start to weekend,
Hopful forecasts—turnips take flight!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a summary of changes, implementation details, UI components, and a test plan, but does not fully follow the provided template structure with required sections like 📕 Issue, 📙 작업 내역, 📘 작업 유형, and 📋 체크리스트. Complete the PR description by following the template: add the Issue section with #29 reference, expand 작업 내역 with checkboxes, mark the 작업 유형 (신규 기능 추가), and fill out all 체크리스트 items.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.31% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature addition: a turnip calculator (무값 계산기) feature being added to the application.
Linked Issues check ✅ Passed The PR implements the primary coding requirements from issue #29: turnip calculator UI, price input screen, pattern prediction algorithm, and CoreData storage. However, push notification feature (마지막 체크리스트 항목) is not implemented in this PR.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the turnip calculator feature. The additions to DashboardCoordinator, DashboardViewController, and DashboardReactor integrate the feature into the dashboard navigation, which is within the defined scope of issue #29.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (5)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/TurnipPriceStorage.swift (1)

16-16: Consider returning Completable for consistency.

The updatePrice method is the only operation that doesn't return an observable result. While fire-and-forget updates may be intentional, returning Completable would provide callers the ability to:

  • Wait for the update to complete before proceeding
  • Handle errors consistently with other operations
  • Chain reactive operations properly
🔎 Proposed signature change
-func updatePrice(_ turnipPrice: TurnipPrice)
+func updatePrice(_ turnipPrice: TurnipPrice) -> Completable

Then update the implementation in CoreDataTurnipPriceStorage to return Completable.create { ... }.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPriceInputCell.swift (1)

86-101: Potential subscription accumulation if configure is called multiple times.

If configure is called multiple times on the same cell before prepareForReuse, the RxSwift subscription will accumulate since the disposeBag is only reset in prepareForReuse. Consider resetting the disposeBag at the start of configure to ensure clean state.

🔎 Proposed fix
 func configure(dayLabel: String, price: Int?, index: Int, onPriceChanged: @escaping (Int, String?) -> Void) {
+    disposeBag = DisposeBag()
     self.dayLabel.text = dayLabel.localized

     if let price = price {
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (2)

37-71: Unused lazy properties.

buyPriceSection, priceInputSection, and predictionSection are declared but never accessed. The setUpViews() method uses createSectionContainer directly instead. Consider removing these unused properties to reduce code clutter.


175-193: Unused helper method.

createSectionView(title:) is never called. Only createSectionContainer(title:content:) is used. Consider removing this dead code.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift (1)

80-82: save action is defined but never triggered.

The Action.save case exists and has implementation, but the TurnipCalculatorViewController never dispatches this action. If auto-save via updatePrice is the intended behavior, consider removing the unused save action to reduce confusion.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 185dea2 and dec10ba.

📒 Files selected for processing (14)
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.xcdatamodeld/UserCollectionEntity.xcdatamodel/contents
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/EntityMapping/TurnipPriceEntity+Mapping.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/TurnipPriceStorage.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewControllers/DashboardViewController.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/DashboardReactor.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPredictionView.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPriceInputCell.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipSummaryView.swift
🧰 Additional context used
🧬 Code graph analysis (7)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.xcdatamodeld/UserCollectionEntity.xcdatamodel/contents (4)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/VillagersLikeStorage/EntityMapping/VillagersLikeEntity+Mapping.swift (1)
  • _ (11-61)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/VillagersLikeStorage/CoreDataNPCLikeStorage.swift (2)
  • coreDataStorage (11-59)
  • update (42-58)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/VillagersHouseStorage/EntityMapping/VillagersHouseEntity+Mapping.swift (1)
  • _ (11-62)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/VillagersLikeStorage/EntityMapping/NPCLikeEntity+Mapping.swift (1)
  • _ (11-41)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (4)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPredictionView.swift (2)
  • setUpViews (60-85)
  • configure (87-112)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPriceInputCell.swift (2)
  • setUpViews (59-84)
  • configure (86-101)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipSummaryView.swift (4)
  • setUpViews (47-73)
  • setUpViews (116-130)
  • configure (75-78)
  • configure (132-135)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (2)
  • updateBuyPrice (81-89)
  • updatePrice (67-78)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/DashboardReactor.swift (1)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift (1)
  • transition (55-151)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (1)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift (1)
  • updatePrice (77-97)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift (2)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Animals/Coordinator/AnimalsCoordinator.swift (3)
  • transition (33-64)
  • animals (10-69)
  • showItemList (72-86)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Catalog/Coordinator/CatalogCoordinator.swift (2)
  • transition (34-55)
  • items (10-60)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/TurnipPriceStorage.swift (2)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift (5)
  • fetchCurrentWeekPrice (20-40)
  • fetchAllPrices (42-58)
  • savePrice (60-75)
  • updatePrice (77-97)
  • deletePrice (99-120)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (1)
  • updatePrice (67-78)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift (3)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (2)
  • updateBuyPrice (81-89)
  • updatePrice (67-78)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift (3)
  • updatePrice (77-97)
  • fetchCurrentWeekPrice (20-40)
  • savePrice (60-75)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (2)
  • predict (33-58)
  • getExpectedPriceRange (61-71)
🪛 SwiftLint (0.57.0)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift

[Warning] 68-68: Conditional statements should always return on the next line

(conditional_returns_on_newline)

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift

[Warning] 39-39: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 62-62: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 257-257: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 174-174: where clauses are preferred over a single if inside a for

(for_where)


[Warning] 39-39: Variable name 'p' should be between 2 and 40 characters long

(identifier_name)


[Warning] 119-119: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)


[Warning] 173-173: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)


[Warning] 212-212: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift

[Warning] 117-117: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 185-185: Conditional statements should always return on the next line

(conditional_returns_on_newline)

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift

[Warning] 29-29: Conditional statements should always return on the next line

(conditional_returns_on_newline)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check Build
🔇 Additional comments (21)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewControllers/DashboardViewController.swift (1)

113-120: LGTM!

The menu integration follows the established pattern from similar components like PreferencesViewController. The action binding is properly disposed, and the localized menu items are correctly mapped to reactor actions.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/DashboardReactor.swift (1)

16-62: LGTM!

The turnip calculator menu integration is consistent with existing menu items. The flow from raw value → localized transformation → coordinator transition is properly implemented.

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.xcdatamodeld/UserCollectionEntity.xcdatamodel/contents (2)

82-89: LGTM!

The Core Data entity definition is correctly structured with appropriate attribute types and optionality. The bidirectional relationship with UserCollectionEntity follows the same pattern as existing entities (e.g., DailyTaskEntity, NPCLikeEntity).


99-99: LGTM!

The relationship definition follows the established pattern for other UserCollectionEntity relationships and maintains proper bidirectional consistency.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift (1)

25-25: LGTM!

The turnip calculator route and transition implementation follow the established coordinator pattern for modal presentations, consistent with the .about and .setting routes.

Also applies to: 139-143

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipSummaryView.swift (2)

10-79: LGTM!

The view structure is well-organized with clear separation of concerns. The use of stack views and constraint-based layout follows iOS best practices.


137-139: LGTM!

The price display logic correctly treats non-positive values as invalid and displays a placeholder, which aligns with the domain where turnip prices should be positive integers.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPredictionView.swift (1)

87-112: LGTM!

The configuration method properly formats the prediction data and provides visual feedback through pattern-specific colors. The probability rounding to whole percentages and animated progress updates enhance the user experience.

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/EntityMapping/TurnipPriceEntity+Mapping.swift (2)

13-20: LGTM!

The convenience initializer properly handles optional values using sentinel values (-1 for nil prices, 0 for nil buyPrice) that are correctly reversed in the toDomain() method.


22-35: LGTM!

The toDomain() mapping correctly reverses the sentinel value encoding and provides sensible defaults. The 12-element prices array corresponds to Monday-Saturday AM/PM slots, which is correct for the turnip price tracking domain.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPriceInputCell.swift (1)

11-57: LGTM!

The cell implementation follows good practices: proper reuse identifier, lazy UI components, and correct disposeBag reset in prepareForReuse.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (1)

335-341: LGTM!

The safe subscript extension is a good defensive pattern for array access.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift (1)

111-146: LGTM!

The loadCurrentWeekPrice method handles both success and error cases appropriately, with proper loading state management and error propagation.

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (2)

105-111: startOfWeek may vary by locale.

The comment indicates Sunday as the week start, but Calendar.current respects the user's locale settings where the first day of week may be Monday (ISO 8601) or other days. For Animal Crossing's turnip mechanics (bought on Sunday, sold Mon-Sat), consider explicitly using a calendar with Sunday as the first weekday.

🔎 Proposed fix for consistent Sunday-based week start
 var startOfWeek: Date {
-    let calendar = Calendar.current
+    var calendar = Calendar.current
+    calendar.firstWeekday = 1 // Sunday
     let components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self)
     return calendar.date(from: components) ?? self
 }

10-59: LGTM!

The TurnipPattern enum and TurnipPrice struct are well-designed with proper immutability patterns and sensible defaults.

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift (2)

20-40: LGTM!

The fetch implementation properly uses background context, handles the optional result, and maps errors appropriately.


84-87: Sentinel values for nil prices are correctly handled.

The toDomain() method in TurnipPriceEntity+Mapping.swift properly interprets the sentinel values: -1 for nil slot prices (line 25) and 0 (or negative) for nil buyPrice (line 31). The bidirectional mapping is consistent—nil values are stored as sentinels during persistence and correctly restored on retrieval.

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (4)

10-25: LGTM!

The TurnipPrediction struct is well-defined with a sensible static unknown fallback.


32-58: LGTM!

The prediction logic is structured well, computing all four pattern predictions and sorting by probability.


256-261: LGTM!

The variance calculation is mathematically correct.


226-243: Range creation in analyzeSmallSpike could also have min > max.

Similar to analyzeLargeSpike, the range calculations here could result in min > max for certain buyPrice values and indices. Consider adding the same safeguard.

🔎 Proposed fix pattern
             let min = Int(Double(buyPrice) * max(0.4, 0.9 - Double(index) * 0.08))
             let max = Int(Double(buyPrice) * max(0.5, 0.95 - Double(index) * 0.06))
-            return min...max
+            return Swift.min(min, max)...Swift.max(min, max)

Likely an incorrect or invalid review comment.

Comment on lines 137 to 154
let expectedPrices: [ClosedRange<Int>?] = (0..<12).map { index in
if index < 4 {
// 초반 감소
let min = Int(Double(buyPrice) * (0.4 - Double(index) * 0.05))
let max = Int(Double(buyPrice) * (0.9 - Double(index) * 0.03))
return max(min, 40)...max
} else if index < 7 {
// 스파이크 구간
let min = Int(Double(buyPrice) * 0.9)
let max = Int(Double(buyPrice) * 6.0)
return min...max
} else {
// 후반 감소
let min = Int(Double(buyPrice) * 0.4)
let max = Int(Double(buyPrice) * 0.9)
return min...max
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential invalid ClosedRange when min > max.

At high indices (e.g., index=3), the calculation 0.4 - Double(index) * 0.05 = 0.25 produces a min ratio, and 0.9 - Double(index) * 0.03 = 0.81 produces max ratio. After max(min, 40) clamping, if buyPrice is low enough (e.g., 100 bells), the clamped min (40) could exceed max (81), causing a runtime crash when creating min...max.

🔎 Proposed fix
         if index < 4 {
             // 초반 감소
             let min = Int(Double(buyPrice) * (0.4 - Double(index) * 0.05))
             let max = Int(Double(buyPrice) * (0.9 - Double(index) * 0.03))
-            return max(min, 40)...max
+            let clampedMin = Swift.max(min, 40)
+            let safeMax = Swift.max(max, clampedMin)
+            return clampedMin...safeMax
🤖 Prompt for AI Agents
In Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift
around lines 137–154, the computed ClosedRange can be invalid when the clamped
min exceeds the computed max; compute explicit minVal and maxVal integers (e.g.
let minVal = max(Int(Double(buyPrice) * ratioMin), 40) and let maxVal =
Int(Double(buyPrice) * ratioMax)), then ensure maxVal >= minVal (if not, set
maxVal = minVal or swap) before returning minVal...maxVal; apply this pattern to
all branches so you never construct a ClosedRange with min > max.

Comment on lines 148 to 160
private func updatePriceAndPredict(_ turnipPrice: TurnipPrice) -> Observable<Mutation> {
let predictions = TurnipPricePredictor.predict(turnipPrice: turnipPrice)
let range = TurnipPricePredictor.getExpectedPriceRange(turnipPrice: turnipPrice)

// 자동 저장
storage.updatePrice(turnipPrice)

return Observable.concat([
Observable.just(Mutation.setTurnipPrice(turnipPrice)),
Observable.just(Mutation.setPredictions(predictions)),
Observable.just(Mutation.setExpectedRange(min: range?.min ?? 0, max: range?.max ?? 0))
])
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent failure on storage update.

storage.updatePrice(turnipPrice) returns Void and any errors are only logged via debugPrint. Consider providing user feedback if the save fails, or at minimum logging this more prominently for debugging purposes.

🤖 Prompt for AI Agents
In
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift
around lines 148–160, the call to storage.updatePrice(turnipPrice) silently
swallows failures (returns Void and only debugPrints). Change the storage API to
surface outcome (preferably return Observable<Void> or throw/Result) or wrap it
in a do/catch, then handle the result: if success continue emitting the three
existing Mutations, if failure log with a non-debug logger and emit a new
Mutation representing save failure (or a user-facing error state) so the UI can
show feedback; ensure any Observable returned from storage is merged into the
Observable.concat sequence and errors are caught/mapped to a failure Mutation
instead of being ignored.

beans-go and others added 3 commits January 1, 2026 07:16
- TurnipPricePredictor 예측 알고리즘 개선
- CoreDataTurnipPriceStorage 저장 로직 개선
- TurnipCalculatorReactor 리팩토링
- TurnipPredictionView UI 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Observable 체인을 별도 변수로 분리
- 타입 추론 힌트 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (1)

137-154: Fix invalid ClosedRange when clamped min exceeds max.

At high indices (e.g., index=3) with low buy prices (e.g., 40 bells), the calculation produces min = 10 and max = 32. After clamping min to 40, you get 40...32, which is an invalid range and will crash at runtime.

🔎 Proposed fix
         let expectedPrices: [ClosedRange<Int>?] = (0..<12).map { index in
             if index < 4 {
                 // 초반 감소
                 let min = Int(Double(buyPrice) * (0.4 - Double(index) * 0.05))
                 let max = Int(Double(buyPrice) * (0.9 - Double(index) * 0.03))
-                return max(min, 40)...max
+                let clampedMin = max(min, 40)
+                let safeMax = max(max, clampedMin)
+                return clampedMin...safeMax
             } else if index < 7 {

Based on past review comments.

🧹 Nitpick comments (4)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.swift (1)

59-70: Consider surfacing save errors.

The saveContext() method catches errors but only logs them via debugPrint. With the new saveError case added to CoreDataStorageError, consider refactoring this method to throw or return errors so callers (like CoreDataTurnipPriceStorage) can detect and handle save failures appropriately.

🔎 Proposed refactor
-extension NSManagedObjectContext {
-    func saveContext() {
-        if self.hasChanges {
-            do {
-                try save()
-            } catch {
-                let nsError = error as NSError
-                debugPrint("Unresolved error \(nsError), \(nsError.userInfo)")
-            }
-        }
-    }
-}
+extension NSManagedObjectContext {
+    func saveContext() throws {
+        if self.hasChanges {
+            do {
+                try save()
+            } catch {
+                throw CoreDataStorageError.saveError(error)
+            }
+        }
+    }
+}

Note: This change would require updating all call sites to handle the thrown error.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (1)

250-279: Reactive binding correctly implemented.

The table view now properly binds to reactor.state.map { $0.turnipPrice.prices } with distinctUntilChanged(), ensuring cells automatically update when state changes. This correctly addresses the previous review concern about stale cell data.

The PriceItem typealias uses a 3-member tuple, which exceeds SwiftLint's recommended limit. Consider using a lightweight struct for better semantics:

🔎 Optional refactor
-    private func setUpTableView(reactor: TurnipCalculatorReactor) {
-        typealias PriceItem = (dayLabel: String, price: Int?, index: Int)
+    private struct PriceItem {
+        let dayLabel: String
+        let price: Int?
+        let index: Int
+    }
+
+    private func setUpTableView(reactor: TurnipCalculatorReactor) {
-        let pricesObservable: Observable<[PriceItem]> = reactor.state
+        let pricesObservable: Observable<[PriceItem]> = reactor.state
             .map { $0.turnipPrice.prices }
             .distinctUntilChanged()
             .map { prices -> [PriceItem] in
                 let dayLabels = TurnipPrice.dayLabels
                 return dayLabels.enumerated().map { index, dayLabel in
                     let price: Int? = prices[safe: index] ?? nil
-                    return (dayLabel: dayLabel, price: price, index: index)
+                    return PriceItem(dayLabel: dayLabel, price: price, index: index)
                 }
             }

Based on learnings, SwiftLint prefers tuples with at most 2 members.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift (1)

164-182: Consider surfacing storage errors to users.

The saveMutation observable silently swallows all storage errors by returning .empty() on both success and failure (lines 176-179). While the fire-and-forget approach provides responsive UI, repeated save failures could lead to data loss without user awareness. Consider adding error tracking or occasional user notification for persistent failures.

🔎 Recommended enhancement
 private func updatePriceAndPredict(_ turnipPrice: TurnipPrice) -> Observable<Mutation> {
     let predictions = TurnipPricePredictor.predict(turnipPrice: turnipPrice)
     let range = TurnipPricePredictor.getExpectedPriceRange(turnipPrice: turnipPrice)

     let uiMutations = Observable.concat([
         Observable.just(Mutation.setTurnipPrice(turnipPrice)),
         Observable.just(Mutation.setPredictions(predictions)),
         Observable.just(Mutation.setExpectedRange(min: range?.min ?? 0, max: range?.max ?? 0))
     ])

-    let saveMutation = storage.updatePrice(turnipPrice)
-        .asObservable()
-        .flatMap { _ -> Observable<Mutation> in .empty() }
-        .catch { _ -> Observable<Mutation> in .empty() }
+    let saveMutation = storage.updatePrice(turnipPrice)
+        .asObservable()
+        .flatMap { _ -> Observable<Mutation> in .empty() }
+        .catch { error -> Observable<Mutation> in
+            debugPrint("⚠️ Failed to save turnip price: \(error)")
+            // Optionally: track consecutive failures and emit setError mutation if > threshold
+            return .empty()
+        }

     return Observable.merge(uiMutations, saveMutation)
 }
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (1)

172-178: Consider using where clause for cleaner iteration.

The for loop with a single if condition can be simplified using a where clause.

🔎 Proposed refactor
             var isDecreasing = true
-            for i in 1..<validRatios.count {
-                if validRatios[i] > validRatios[i-1] + 0.02 {
-                    isDecreasing = false
-                    break
-                }
+            for i in 1..<validRatios.count where validRatios[i] > validRatios[i-1] + 0.02 {
+                isDecreasing = false
+                break
             }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dec10ba and 74b456d.

📒 Files selected for processing (8)
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/TurnipPriceStorage.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPredictionView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPredictionView.swift
🧰 Additional context used
🧬 Code graph analysis (3)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.swift (2)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/DailyTaskStorage/CoreDataDailyTaskStorage.swift (2)
  • updateTask (59-79)
  • insertTask (41-57)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/UserInfoStorage/CoreDataUserInfoStorage.swift (1)
  • updateUserInfo (30-44)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (4)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPredictionView.swift (2)
  • setUpViews (60-85)
  • configure (98-123)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipPriceInputCell.swift (2)
  • setUpViews (59-84)
  • configure (86-101)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/Views/TurnipSummaryView.swift (4)
  • setUpViews (47-73)
  • setUpViews (116-130)
  • configure (75-78)
  • configure (132-135)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (2)
  • updateBuyPrice (81-89)
  • updatePrice (67-78)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/TurnipPriceStorage.swift (2)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift (5)
  • fetchCurrentWeekPrice (20-40)
  • fetchAllPrices (42-58)
  • savePrice (60-62)
  • updatePrice (64-67)
  • deletePrice (107-128)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (1)
  • updatePrice (67-78)
🪛 SwiftLint (0.57.0)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift

[Warning] 133-133: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 207-207: Conditional statements should always return on the next line

(conditional_returns_on_newline)

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift

[Warning] 29-29: Conditional statements should always return on the next line

(conditional_returns_on_newline)

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift

[Warning] 251-251: Tuples should have at most 2 members

(large_tuple)

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift

[Warning] 68-68: Conditional statements should always return on the next line

(conditional_returns_on_newline)

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift

[Warning] 39-39: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 62-62: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 257-257: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 174-174: where clauses are preferred over a single if inside a for

(for_where)


[Warning] 39-39: Variable name 'p' should be between 2 and 40 characters long

(identifier_name)


[Warning] 119-119: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)


[Warning] 173-173: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)


[Warning] 212-212: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)

🔇 Additional comments (24)
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.swift (1)

13-13: LGTM! New error case properly defined.

The saveError case is correctly added with an appropriate localized error description, following the same pattern as existing error cases.

Also applies to: 21-22

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/TurnipPriceStorage.swift (1)

11-19: LGTM! Clean protocol design.

The protocol provides a well-defined contract for turnip price storage operations using RxSwift Single types for asynchronous operations. The optional return type for fetchCurrentWeekPrice() appropriately handles the case when no data exists for the current week, and the @discardableResult on updatePrice enables fire-and-forget updates when needed.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (3)

281-288: LGTM! Removed conflicting reloadData() call.

The method now only updates buyPriceTextField and correctly relies on the reactive binding to update the table view automatically. The inline comment at line 287 appropriately documents why reloadData() is unnecessary.


290-310: LGTM! Prediction display logic is sound.

The method correctly rebuilds the prediction stack, handles the empty/unknown state with appropriate placeholder text, and limits display to the top 3 predictions for a clean UI.


315-319: Remove the private Array extension; use the existing Collection extension instead.

The codebase already provides a safe subscript operator via Collection+extension.swift, which is automatically available to Array since it conforms to the Collection protocol. The private Array extension at these lines is redundant and should be removed.

Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift (4)

72-94: LGTM! Clear action now persists properly.

The clearAll action now correctly persists the cleared state to storage (line 78) and includes graceful error handling. The optimistic UI update in the catch block (lines 87-94) ensures a responsive user experience even if storage fails.


127-162: LGTM! Loading logic properly handles all states.

The method correctly manages loading state, handles missing data by creating a new TurnipPrice with the current week start, and only calculates predictions when buyPrice is available. Error handling appropriately emits setError mutation.


184-204: LGTM! Explicit save with proper error handling.

Unlike the fire-and-forget save in updatePriceAndPredict, this method properly surfaces storage errors via the setError mutation (line 199), providing user feedback when manual saves fail.


206-209: LGTM! Price parsing logic is straightforward.

The helper correctly converts string input to optional integer, returning nil for empty or invalid input. The guard statement style is readable despite the SwiftLint warning.

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift (5)

10-36: LGTM! Well-defined pattern enum.

The TurnipPattern enum correctly models the turnip price patterns from Animal Crossing with appropriate localized names and user-friendly descriptions for each pattern type.


38-59: LGTM! Robust domain model with validation.

The TurnipPrice struct is well-designed with appropriate defaults and validation. The initializer ensures the prices array always contains exactly 12 elements (line 56), preventing potential index-out-of-bounds issues.


61-103: LGTM! Helper methods are well-implemented.

The helper methods correctly implement immutable updates, include proper bounds checking (lines 68, 98), and guard against edge cases like division by zero in priceRatio. This addresses the previous review concern about missing bounds validation.


116-119: LGTM! Week comparison logic is straightforward.

The isSameWeek method correctly leverages the startOfWeek property to compare normalized week start dates.


105-114: No action required. The startOfWeek implementation correctly handles year boundaries using yearForWeekOfYear and weekOfYear components, which is the proper Calendar API usage for this use case.

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (6)

33-58: LGTM!

The prediction entry point correctly validates the buy price, computes ratios, and aggregates pattern analyses. The sorting by probability ensures the most likely pattern appears first.


61-71: LGTM!

The method provides a sensible default range when no pattern can be determined, and correctly delegates to the top prediction otherwise.


76-108: LGTM!

The fluctuating pattern analysis correctly uses variance to detect genuine fluctuation and adjusts probability based on how well the observed ratios fit the expected range.


166-202: LGTM!

The decreasing pattern analysis correctly detects monotonic decline with reasonable tolerance (+0.02) and safely constructs price ranges with proper clamping.


205-252: LGTM!

The small spike pattern analysis correctly identifies moderate price increases (1.0-2.0x) and generates valid price ranges across all three phases.


256-261: LGTM!

The variance calculation is mathematically correct and properly guards against empty input.

Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift (4)

20-40: LGTM!

The method correctly uses background task execution for thread safety and appropriately finds the current week's price by comparing date components.


42-58: LGTM!

The fetch-all operation correctly retrieves and sorts prices by week in descending order, which is suitable for displaying recent data first.


60-98: LGTM!

The upsert logic correctly prevents duplicate entries by checking for existing entities by ID before creating new ones. The past concern about duplicate creation has been properly addressed through the conditional logic at line 82.


101-105: LGTM!

The entity update logic correctly handles optional values, using -1 as a sentinel for nil prices, which aligns with the mapping layer conventions.

Comment on lines +107 to +128
func deletePrice(_ turnipPrice: TurnipPrice) -> Single<TurnipPrice> {
return Single.create { single in
self.coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let priceEntities = object.turnipPrices?.allObjects as? [TurnipPriceEntity] ?? []

guard let entityToDelete = priceEntities.first(where: { $0.id == turnipPrice.id }) else {
single(.failure(CoreDataStorageError.notFound))
return
}

object.removeFromTurnipPrices(entityToDelete)
context.saveContext()
single(.success(entityToDelete.toDomain()))
} catch {
single(.failure(CoreDataStorageError.readError(error)))
}
}
return Disposables.create()
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider more specific error mapping for delete operations.

Line 123 maps caught errors to CoreDataStorageError.readError, but this method performs a delete operation. While the error could originate from the initial fetch, mapping to a more specific error type (like deleteError if available) would provide clearer error semantics.

🤖 Prompt for AI Agents
In
Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/TurnipPriceStorage/CoreDataTurnipPriceStorage.swift
around lines 107 to 128, the catch block currently maps all errors to
CoreDataStorageError.readError even though this function performs a delete;
change the mapping to a more specific delete error (e.g.
CoreDataStorageError.deleteError) or create that case if it doesn't exist, and
pass the caught error into that deleteError case so delete failures are reported
with correct semantics; keep the existing notFound handling as-is and ensure the
rest of the flow (removal, context.saveContext, and returning the deleted domain
object) is unchanged.

- named tuple 사용으로 타입 추론 단순화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (2)

248-277: Reactive table binding correctly addresses previous review feedback.

The implementation now properly binds the table view to reactive state changes, which resolves the prior concern about cells not reflecting updates. The use of a typealias for PriceItem improves readability.

Minor: Redundant nil coalescing on line 258.

The expression prices[safe: index] ?? nil is redundant since prices[safe: index] already returns Int?. The nil coalescing operator adds no value here.

🔎 Suggested simplification
                let dayLabels = TurnipPrice.dayLabels
                return dayLabels.enumerated().map { index, dayLabel in
-                    let price: Int? = prices[safe: index] ?? nil
+                    let price: Int? = prices[safe: index]
                    return (dayLabel: dayLabel, price: price, index: index)
                }

313-317: LGTM!

The safe array subscript implementation is correct and follows a common pattern for defensive array access. The private scope is appropriate for file-local usage.

If this pattern is used in multiple files, consider extracting it to a shared utilities file to promote reuse and maintain consistency across the codebase.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74b456d and 3f7ad5e.

📒 Files selected for processing (1)
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check Build
🔇 Additional comments (7)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (7)

1-17: LGTM!

The imports and class structure follow standard ReactorKit patterns correctly. The final class modifier and proper protocol conformance are appropriate.


19-73: LGTM!

UI component declarations are well-structured with appropriate lazy initialization. The use of .numberPad keyboard type for price inputs and PublishRelay for handling cell input events demonstrates good reactive programming practices.


77-181: LGTM!

The view setup and layout logic is comprehensive and well-organized. The fixed table height calculation on line 137 (CGFloat(12 * 52)) is appropriate given the fixed 12-slot weekly price structure. The createSectionContainer helper promotes good code reuse.


185-246: LGTM!

The ReactorKit binding logic is well-implemented with proper patterns:

  • Correct use of distinctUntilChanged() to prevent unnecessary updates
  • Proper memory management with [weak self]
  • Main thread observation for UI updates
  • Clean separation of input actions and output state bindings

279-286: LGTM!

The updatePriceInputs method correctly updates only the buy price text field while relying on reactive table view bindings for price updates, which addresses the previous review concern about conflicting data source management.


288-308: LGTM!

The prediction view updates are handled cleanly with proper empty state handling. The use of prefix(3) to limit displayed predictions is a sensible UX choice.


248-277: No memory leak issue present. TurnipPriceInputCell already correctly resets its disposeBag in prepareForReuse(), which disposes of all previous subscriptions before the cell is reused.

Comment on lines +194 to +198
buyPriceTextField.rx.controlEvent(.editingDidEnd)
.withLatestFrom(buyPriceTextField.rx.text)
.map { Reactor.Action.updateBuyPrice($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "*TurnipCalculatorReactor*" -type f

Repository: leeari95/ACNH-wiki

Length of output: 176


🏁 Script executed:

rg -l "TurnipCalculatorReactor" --type swift | head -5

Repository: leeari95/ACNH-wiki

Length of output: 403


🏁 Script executed:

rg "case \.updateBuyPrice" --type swift -A 5 -B 2

Repository: leeari95/ACNH-wiki

Length of output: 1294


🏁 Script executed:

rg "class TurnipCalculatorReactor" --type swift -A 50

Repository: leeari95/ACNH-wiki

Length of output: 7298


🏁 Script executed:

cat Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewModels/TurnipCalculatorReactor.swift

Repository: leeari95/ACNH-wiki

Length of output: 7615


🏁 Script executed:

rg "parsePrice" --type swift -A 10

Repository: leeari95/ACNH-wiki

Length of output: 3319


🏁 Script executed:

rg "TurnipPrice" --type swift -l | head -10

Repository: leeari95/ACNH-wiki

Length of output: 882


🏁 Script executed:

cat Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPrice.swift

Repository: leeari95/ACNH-wiki

Length of output: 3712


Add input validation for price values in TurnipCalculatorReactor and TurnipPrice.

The parsePrice method only validates that the input string is non-empty, then directly converts it to Int. This allows negative values and arbitrarily large numbers to be accepted. Similarly, both updateBuyPrice and updatePrice in the TurnipPrice model bypass validation entirely.

Add validation to reject:

  • Negative values
  • Unrealistic bounds (e.g., prices exceeding expected Animal Crossing turnip price ranges)

Consider adding validation in parsePrice or as guard statements in the action handlers to ensure only sensible prices reach the state.

beans-go and others added 2 commits January 1, 2026 19:54
- analyzeLargeSpike, analyzeDecreasing, analyzeSmallSpike 메서드의 map 클로저를 별도 메서드로 분리
- Swift 표준 라이브러리의 min/max 함수와 변수명 충돌 방지를 위해 Swift.min/Swift.max 사용
- 컴파일러 type-check 타임아웃 문제 해결

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (1)

154-157: Critical: Invalid ClosedRange when maxValue < adjustedMin.

This issue was previously flagged but remains unresolved. When buyPrice is low (less than ~50), the calculation Int(Double(buyPrice) * (0.9 - Double(index) * 0.03)) at higher indices (e.g., index=3) can produce a maxValue that is less than the clamped adjustedMin (40), causing a runtime crash when constructing adjustedMin...maxValue.

For example, with buyPrice = 45 and index = 3:

  • maxValue = Int(45 * 0.81) = 36
  • adjustedMin = max(Int(45 * 0.25), 40) = 40
  • Range 40...36 is invalid and will crash.
🔎 Proposed fix
             if index < 4 {
                 // 초반 감소
                 let minValue = Int(Double(buyPrice) * (0.4 - Double(index) * 0.05))
                 let maxValue = Int(Double(buyPrice) * (0.9 - Double(index) * 0.03))
                 let adjustedMin = Swift.max(minValue, 40)
-                range = adjustedMin...maxValue
+                let adjustedMax = Swift.max(maxValue, adjustedMin)
+                range = adjustedMin...adjustedMax
🧹 Nitpick comments (2)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (2)

137-137: Consider extracting magic numbers as constants.

The hard-coded calculation CGFloat(12 * 52) embeds the row count and row height. Extracting these as named constants would improve maintainability if these values need to change.

🔎 Proposed refactor

At the top of the class, add constants:

private enum Constants {
    static let priceInputRowCount = 12
    static let priceInputRowHeight: CGFloat = 52
}

Then update line 137:

-            priceTableView.heightAnchor.constraint(equalToConstant: CGFloat(12 * 52))
+            priceTableView.heightAnchor.constraint(equalToConstant: CGFloat(Constants.priceInputRowCount) * Constants.priceInputRowHeight)

And line 54:

-        tableView.rowHeight = 52
+        tableView.rowHeight = Constants.priceInputRowHeight

308-308: Consider documenting the prediction limit.

The prefix(3) limit restricts the displayed predictions to three. Adding a brief comment explaining the rationale (e.g., UI space constraints, user experience) would improve code clarity.

🔎 Proposed documentation
-        // Add prediction views (max 3)
-        for prediction in predictions.prefix(3) {
+        // Add prediction views (limit to top 3 for readability)
+        let maxPredictions = 3
+        for prediction in predictions.prefix(maxPredictions) {
             let predictionView = TurnipPredictionView()
             predictionView.configure(with: prediction)
             predictionStackView.addArrangedSubview(predictionView)
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f7ad5e and 44c51cc.

📒 Files selected for processing (2)
  • Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift
  • Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift

[Warning] 39-39: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 62-62: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 280-280: Conditional statements should always return on the next line

(conditional_returns_on_newline)


[Warning] 183-183: where clauses are preferred over a single if inside a for

(for_where)


[Warning] 39-39: Variable name 'p' should be between 2 and 40 characters long

(identifier_name)


[Warning] 119-119: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)


[Warning] 182-182: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)


[Warning] 227-227: Variable name 'i' should be between 2 and 40 characters long

(identifier_name)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check Build
🔇 Additional comments (11)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift (1)

253-282: Table view binding properly implements reactive state updates.

The reactive binding correctly observes reactor.state.turnipPrice.prices and rebuilds the data source when prices change. This properly addresses the previous concern about cells not reflecting state updates.

Animal-Crossing-Wiki/Projects/App/Sources/Models/TurnipPricePredictor.swift (10)

11-25: LGTM!

The TurnipPrediction struct is well-designed with appropriate properties and a useful static unknown member for fallback cases.


33-58: LGTM!

The main prediction logic is well-structured with proper input validation, clear ratio calculations, and appropriate sorting by probability.


61-71: LGTM!

The expected price range calculation appropriately reuses the predict() method and provides a sensible fallback range when predictions are unavailable.


76-108: LGTM!

The fluctuating pattern analysis uses appropriate heuristics with variance-based probability adjustments. The range construction is safe as minRatio < maxRatio is guaranteed.


111-146: LGTM!

The large spike pattern detection logic appropriately identifies spikes above 1.5x ratio and adjusts probabilities based on early decreasing patterns.


175-205: LGTM!

The decreasing pattern analysis correctly identifies continuous decline with appropriate tolerance for minor fluctuations.


207-217: LGTM!

The decreasing pattern price generator is safe. The max() clamping ensures maxRatio (clamped to ≥0.4) is always greater than minRatio (clamped to ≥0.3), preventing invalid range construction.


220-250: LGTM!

The small spike pattern detection appropriately identifies moderate price increases (1.0-2.0x) and distinguishes them from large spikes.


252-275: LGTM!

The small spike pattern price generator is safe. The max() clamping ensures the minimum ratio floor (0.4) is less than the maximum ratio floor (0.5), preventing invalid range construction across all phases.


279-284: LGTM!

The variance calculation is mathematically correct with appropriate guard for empty input.

Comment on lines +198 to +208
buyPriceTextField.rx.controlEvent(.editingDidEnd)
.withLatestFrom(buyPriceTextField.rx.text)
.map { Reactor.Action.updateBuyPrice($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)

// Price input changes
priceInputRelay
.map { Reactor.Action.updatePrice(index: $0.0, price: $0.1) }
.bind(to: reactor.action)
.disposed(by: disposeBag)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Input validation for price values remains unimplemented.

This concern was raised in a previous review but has not yet been addressed. The buy price and weekly price inputs are passed directly to the reactor without client-side validation. This allows negative values, zero, and unrealistically large numbers (e.g., "-100" or "999999999") to be accepted and propagated to the predictor and storage layers.

In Animal Crossing, typical turnip price ranges are:

  • Buy price (Sunday): ~90–110 bells
  • Sell prices (Mon–Sat AM/PM): ~20–660 bells

Consider adding validation guards before dispatching actions:

🔎 Proposed validation logic
private func bindInputs(reactor: TurnipCalculatorReactor) {
    // View did load
    Observable.just(Reactor.Action.viewDidLoad)
        .bind(to: reactor.action)
        .disposed(by: disposeBag)

    // Buy price changes with validation
    buyPriceTextField.rx.controlEvent(.editingDidEnd)
        .withLatestFrom(buyPriceTextField.rx.text)
        .filter { text in
            guard let text = text, !text.isEmpty, let price = Int(text) else { return true }
            return price >= 0 && price <= 1000  // Reject unrealistic values
        }
        .map { Reactor.Action.updateBuyPrice($0) }
        .bind(to: reactor.action)
        .disposed(by: disposeBag)

    // Price input changes with validation
    priceInputRelay
        .filter { _, priceText in
            guard let text = priceText, !text.isEmpty, let price = Int(text) else { return true }
            return price >= 0 && price <= 1000  // Reject unrealistic values
        }
        .map { Reactor.Action.updatePrice(index: $0.0, price: $0.1) }
        .bind(to: reactor.action)
        .disposed(by: disposeBag)

    // Clear button
    clearButton.rx.tap
        .map { Reactor.Action.clearAll }
        .bind(to: reactor.action)
        .disposed(by: disposeBag)
}

Alternatively, validation could be added in the reactor's parsePrice method or action handlers. Consider showing user feedback (e.g., alert or text color change) when invalid input is detected.

🤖 Prompt for AI Agents
In
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/TurnipCalculator/ViewControllers/TurnipCalculatorViewController.swift
around lines 198–208, the buy price and weekly price inputs are sent straight to
the reactor with no validation; add input validation before mapping to
Reactor.Action.updateBuyPrice and Reactor.Action.updatePrice to reject
non-numeric, empty, negative, zero (if desired) and unrealistically large values
(e.g. >1000), by filtering the observable streams
(buyPriceTextField.rx.controlEvent(.editingDidEnd).withLatestFrom(...) and
priceInputRelay) to only allow valid Ints in the expected range, and when
filtering out invalid input emit a user-facing indication (alert, inline error
state, or text color) instead of dispatching the action; alternatively perform
the same validation in the reactor parsePrice/action handler if you prefer
centralized validation.

@leeari95 leeari95 closed this Jan 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[무값 계산기] 매일 무값의 가격을 기록하여 가격 변동을 추측하는 계산기 기능 추가

4 participants