Flutter/과제 1차 : 계산기 만들기

[FLUTTER 과제] 계산기 만들기(1) : 기본 계산기

Zeka_P 2026. 4. 17. 00:29

 이번엔 지금까지 배운 플러터의 내용들을 참고하여 실제 핸드폰에서 제공되는 기초 계산기와 같은 기능을 하는 계산기 프로그램을 구현해보겠습니다. 최종 목적은 아래와 같습니다.

 

1. 실제 iOS에서 제공되는 계산기와 거의 일치하는 UI의 프로그램을 구축합니다.

2. 실제 iOS에서 제공되는 기본, 공학 계산기와 수학 메모를 모두 동일하게 구현합니다.

3. 실제 iOS에서 제공되는 Edge Case 발생에서의 통제 기능과, 기능적으로 구현된 UX 향상 부분을 동일하게 구현합니다. 

 

금일은 프로젝트 생성과 기본 계산기를 만들었습니다. 프로젝트 깃허브 링크는 아래와 같으며 금일 구현된 기본 계산기는 basic-calculate-function  브랜치에서 구현하여 메인에 머지시켰으니, 금일 구현 부분만 조회하고자 한다면 해당 브랜치를 참고 바랍니다.

https://github.com/zeka0228/calculator/tree/master 

 

먼저 기초 UI와 사칙연산 및 양/음수 변환, %, 소수점 사용 등의 기초 기능을 만들고 해당 버튼들에 연결, 출력 가능하도록 구현된 기초 코드는 아래와 같습니다. 실제 아이폰 계산기와 최대한 유사한 색상으로 구축했으며, 각 버튼의 배치도 동일하게 맞췄습니다. 

 

계산기의 핵심은, 입력을 받고, 이를 알맞게 연산하여 출력해야 하며, 모바일에서 제공되는 "직전 연산식의 반복" 기능을 위해, 메모리 상으로 이 변수를 유지해야 한다는 핵심 조건이 주어집니다. 따라서 기초적인 변수는 다음과 같이 관리됩니다. 

_display 변수가 화면에 보이는 값을, _firstOperand 변수는 연산자 입력 시, 앞단의 수 저장을, _operator가 입력된 연산자를, _shouldResetDisplay가 숫자 슬라이싱 구분의 플래그를(예를 들어 99 에서 9를 입력하면 999가 되는게 맞지만 99 + 9가 된다면 99 에서 끊어야 하는데, 이 부분에서의 오류를 명시적으로 예방할 bool 변수가 됩니다.) 맡습니다. 

import 'package:flutter/material.dart';

void main() {
  runApp(const CalculatorApp());
}

class CalculatorApp extends StatelessWidget {
  const CalculatorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'iOS Calculator',
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: Colors.black,
      ),
      home: const CalculatorHome(),
    );
  }
}

class CalculatorHome extends StatefulWidget {
  const CalculatorHome({super.key});

  @override
  State<CalculatorHome> createState() => _CalculatorHomeState();
}

class _CalculatorHomeState extends State<CalculatorHome> {
  String _expression = '0';
  String _history = '';
  bool _isResultDisplayed = false;
  String? _lastOperator;
  double? _lastOperand;

  void _onButtonPressed(String text) {
    setState(() {
      if (RegExp(r'^[0-9]$').hasMatch(text)) {
        if (_expression == '0' || _isResultDisplayed) {
          if (_isResultDisplayed) _history = '';
          _expression = text;
          _isResultDisplayed = false;
        } else {
          List<String> tokens = _expression.split(' ');
          String lastToken = tokens.last;
          if (RegExp(r'^[0-9,.]+$').hasMatch(lastToken)) {
            String cleanNumber = lastToken.replaceAll(',', '');
            tokens[tokens.length - 1] = _addCommas(cleanNumber + text);
            _expression = tokens.join(' ');
          } else if (RegExp(r'^\(.*\)$').hasMatch(lastToken)) {
            String numberPart = lastToken.substring(2, lastToken.length - 1).replaceAll(',', '');
            tokens[tokens.length - 1] = '(-${_addCommas(numberPart + text)})';
            _expression = tokens.join(' ');
          } else {
            _expression += text;
          }
        }
      } else if (text == '.') {
        List<String> tokens = _expression.split(' ');
        String lastToken = tokens.last.replaceAll(',', '');
        if (!lastToken.contains('.')) {
          if (RegExp(r'^\(.*\)$').hasMatch(lastToken)) {
            String numberPart = lastToken.substring(2, lastToken.length - 1);
            tokens[tokens.length - 1] = '(-$numberPart.)';
            _expression = tokens.join(' ');
          } else {
            _expression += '.';
          }
        }
      } else if (text == '⌫') {
        _handleBackspace();
      } else if (text == 'AC' || text == 'C') {
        _expression = '0';
        _history = '';
        _isResultDisplayed = false;
        _lastOperator = null;
        _lastOperand = null;
      } else if (text == '+/-') {
        _toggleSign();
      } else if (text == '%') {
        if (_isResultDisplayed) {
          _isResultDisplayed = false;
          _history = '';
          _lastOperator = null;
          _lastOperand = null;
        }
        _applyPercent();
      } else if (text == '+' || text == '-' || text == '×' || text == '÷') {
        if (_isResultDisplayed) {
          _isResultDisplayed = false;
          _history = '';
        }
        if (RegExp(r'[+×÷-]$').hasMatch(_expression.trim())) {
          _expression = _expression.trim().substring(0, _expression.trim().length - 1) + text + ' ';
        } else {
          _expression = '${_expression.trim()} $text ';
        }
      } else if (text == '=') {
        if (_isResultDisplayed && _lastOperator != null && _lastOperand != null) {
          _repeatLastOperation();
        } else {
          _calculate();
        }
      }
    });
  }

  void _repeatLastOperation() {
    double currentVal = _parseToken(_expression);
    double result = 0;
    
    if (_lastOperator == '%') {
      result = currentVal / 100.0;
      _history = "${_addCommas(_formatValue(currentVal))}%";
    } else {
      switch (_lastOperator) {
        case '+': result = currentVal + _lastOperand!; break;
        case '-': result = currentVal - _lastOperand!; break;
        case '×': result = currentVal * _lastOperand!; break;
        case '÷': result = currentVal / _lastOperand!; break;
      }
      _history = "${_addCommas(_formatValue(currentVal))} $_lastOperator ${_addCommas(_formatValue(_lastOperand!))}";
    }
    
    _expression = _formatResult(result);
    _isResultDisplayed = true;
  }

  void _handleBackspace() {
    if (_expression.endsWith(' ')) {
      _expression = _expression.trim();
      _expression = _expression.substring(0, _expression.length - 1).trim();
    } else {
      List<String> tokens = _expression.split(' ');
      String lastToken = tokens.last;
      if (lastToken.length > 1) {
        if (lastToken.endsWith('%')) {
          tokens[tokens.length - 1] = lastToken.substring(0, lastToken.length - 1);
        } else if (RegExp(r'^\(.*\)$').hasMatch(lastToken)) {
          String numberPart = lastToken.substring(2, lastToken.length - 1).replaceAll(',', '');
          if (numberPart.length > 1) {
            tokens[tokens.length - 1] = '(-${_addCommas(numberPart.substring(0, numberPart.length - 1))})';
          } else {
            tokens[tokens.length - 1] = '0';
          }
        } else {
          String cleanNumber = lastToken.replaceAll(',', '');
          tokens[tokens.length - 1] = _addCommas(cleanNumber.substring(0, cleanNumber.length - 1));
        }
        _expression = tokens.join(' ');
      } else {
        if (tokens.length > 1) {
          tokens.removeLast();
          _expression = tokens.join(' ');
        } else {
          _expression = '0';
        }
      }
    }
    if (_expression.isEmpty) _expression = '0';
  }

  String _addCommas(String s) {
    if (s.isEmpty) return '';
    List<String> parts = s.split('.');
    RegExp reg = RegExp(r'\B(?=(\d{3})+(?!\d))');
    parts[0] = parts[0].replaceAll(reg, ',');
    return parts.join('.');
  }

  void _toggleSign() {
    setState(() {
      List<String> tokens = _expression.trim().split(' ');
      if (tokens.isEmpty) return;

      String lastToken = tokens.last;
      
      // 1. 이미 음수(괄호형 또는 단순형)인 경우 양수로 전환
      if (lastToken.startsWith('(-') && lastToken.endsWith(')')) {
        tokens[tokens.length - 1] = lastToken.substring(2, lastToken.length - 1);
      } else if (lastToken.startsWith('-')) {
        tokens[tokens.length - 1] = lastToken.substring(1);
      } 
      // 2. 양수인 경우 음수로 전환 (항상 괄호 사용)
      else {
        String cleanNumber = lastToken.replaceAll(',', '');
        if (double.tryParse(cleanNumber) != null && cleanNumber != '0') {
          tokens[tokens.length - 1] = '(-$lastToken)';
        }
      }
      _expression = tokens.join(' ');
    });
  }

  void _applyPercent() {
    List<String> tokens = _expression.trim().split(' ');
    String lastToken = tokens.last;
    
    if (lastToken.isNotEmpty && !lastToken.endsWith('%') && 
        (RegExp(r'[0-9,.]+$').hasMatch(lastToken) || RegExp(r'^\(.*\)$').hasMatch(lastToken))) {
      tokens[tokens.length - 1] = '$lastToken%';
      _expression = tokens.join(' ');
    }
  }

  double _parseToken(String token) {
    String clean = token.replaceAll('(', '').replaceAll(')', '').replaceAll(',', '');
    bool isPercent = clean.endsWith('%');
    if (isPercent) {
      clean = clean.substring(0, clean.length - 1);
    }
    double val = double.tryParse(clean) ?? 0;
    return isPercent ? val / 100 : val;
  }

  String _formatValue(double result) {
    String fixed = result.toStringAsFixed(10);
    double rounded = double.parse(fixed);
    String s = rounded.toString();

    if (!s.contains('e') && s.contains('.')) {
      s = s.replaceAll(RegExp(r'0+$'), '');
      if (s.endsWith('.')) s = s.substring(0, s.length - 1);
    }

    if (s.length > 13) {
      s = result.toStringAsPrecision(9);
      if (s.contains('e')) {
        List<String> parts = s.split('e');
        if (parts[0].contains('.')) {
          parts[0] = parts[0].replaceAll(RegExp(r'0+$'), '');
          if (parts[0].endsWith('.')) parts[0] = parts[0].substring(0, parts[0].length - 1);
        }
        s = parts.join('e');
      }
    }
    
    return _addCommas(s);
  }

  void _calculate() {
    try {
      String trimmed = _expression.trim();
      if (RegExp(r'[+×÷-]$').hasMatch(trimmed)) return;

      List<String> tokens = trimmed.split(' ');
      if (tokens.isEmpty) return;

      if (tokens.length == 1) {
        double val = _parseToken(tokens[0]);
        setState(() {
          _history = _expression;
          if (tokens[0].endsWith('%')) {
            _lastOperator = '%';
            _lastOperand = 100.0;
          }
          _expression = _formatResult(val);
          _isResultDisplayed = true;
        });
        return;
      }

      _history = _expression;
      List<dynamic> values = [];
      for (var t in tokens) {
        if (RegExp(r'[+×÷-]').hasMatch(t) && t.length == 1) {
          values.add(t);
        } else {
          values.add(_parseToken(t));
        }
      }

      if (tokens.length >= 3) {
        _lastOperator = tokens[tokens.length - 2];
        _lastOperand = _parseToken(tokens.last);
      }

      for (int i = 0; i < values.length; i++) {
        if (values[i] == '×' || values[i] == '÷') {
          double left = values[i - 1];
          double right = values[i + 1];
          double res = (values[i] == '×') ? left * right : left / right;
          values.removeAt(i - 1);
          values.removeAt(i - 1);
          values.removeAt(i - 1);
          values.insert(i - 1, res);
          i--;
        }
      }

      double finalRes = values[0];
      for (int i = 1; i < values.length; i += 2) {
        String op = values[i];
        double nextVal = values[i + 1];
        if (op == '+') finalRes += nextVal;
        if (op == '-') finalRes -= nextVal;
      }

      setState(() {
        _expression = _formatResult(finalRes);
        _isResultDisplayed = true;
      });
    } catch (e) {
      setState(() {
        _expression = 'Error';
        _isResultDisplayed = true;
      });
    }
  }

  String _formatResult(double result) {
    if (result.isInfinite || result.isNaN) return 'Error';
    return _formatValue(result);
  }

  Widget _buildButton(String text, Color bgColor, Color textColor) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.all(6.0),
        child: InkWell(
          onTap: () => _onButtonPressed(text),
          borderRadius: BorderRadius.circular(50),
          child: Container(
            height: 70,
            decoration: BoxDecoration(color: bgColor, shape: BoxShape.circle),
            child: Center(
              child: Text(
                text,
                style: TextStyle(
                  fontSize: text == '⌫' ? 24 : 28,
                  fontWeight: FontWeight.w500,
                  color: textColor,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: Container(
                alignment: Alignment.bottomRight,
                padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.end,
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    if (_history.isNotEmpty)
                      SingleChildScrollView(
                        reverse: true,
                        scrollDirection: Axis.horizontal,
                        child: Text(
                          _history,
                          style: const TextStyle(
                            fontSize: 24,
                            color: Colors.grey,
                            fontWeight: FontWeight.w400,
                          ),
                        ),
                      ),
                    const SizedBox(height: 8),
                    SingleChildScrollView(
                      reverse: true,
                      scrollDirection: Axis.horizontal,
                      child: Text(
                        _expression,
                        style: TextStyle(
                          fontSize: _expression.length > 10 ? 40 : 60,
                          fontWeight: FontWeight.w300,
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            Column(
              children: [
                Row(
                  children: [
                    _buildButton('⌫', Colors.grey[600]!, Colors.white),
                    _buildButton(
                      (_expression == '0' || _isResultDisplayed) ? 'AC' : 'C',
                      Colors.grey[400]!,
                      Colors.black,
                    ),
                    _buildButton('%', Colors.grey[400]!, Colors.black),
                    _buildButton('÷', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('7', Colors.grey[850]!, Colors.white),
                    _buildButton('8', Colors.grey[850]!, Colors.white),
                    _buildButton('9', Colors.grey[850]!, Colors.white),
                    _buildButton('×', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('4', Colors.grey[850]!, Colors.white),
                    _buildButton('5', Colors.grey[850]!, Colors.white),
                    _buildButton('6', Colors.grey[850]!, Colors.white),
                    _buildButton('-', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('1', Colors.grey[850]!, Colors.white),
                    _buildButton('2', Colors.grey[850]!, Colors.white),
                    _buildButton('3', Colors.grey[850]!, Colors.white),
                    _buildButton('+', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('+/-', Colors.grey[850]!, Colors.white),
                    _buildButton('0', Colors.grey[850]!, Colors.white),
                    _buildButton('.', Colors.grey[850]!, Colors.white),
                    _buildButton('=', Colors.orange, Colors.white),
                  ],
                ),
                const SizedBox(height: 20),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

기초적인 부분을 빠르게 구축하고, 테스트를 반복하며 발견되는 버그들을 대체하는 방식으로 프로젝트가 진행되다 보니 해당 초기 코드에서 변경된 부분이 매우 많습니다. 예를 들어 변수 구축 단계에서부터 발생한 실수로, display 변수 동작의 한계로 삼중 연산으로 입력하는 등의 유저 동작이 일어날 경우 위 코드는 해당 앞단의 입력을 소실하는 중대한 버그를 가지고 있습니다!! 아래는 해당 기초 코드에 대해 발생한 버그의 해결과, 기능 추가, 리펙토링 등을 진행해 최종적으로 기본 계산기를 완성 시키는 과정에서의 트러블 슈팅을 다룹니다. 

전체 작성된 커밋이 풀 리퀘스트 당 6~10 회 상당 존재하여 부분 별로가 아닌 풀 리퀘스트 단위로 코드 블럭을 첨부하겠습니다. 상세한 변경 내용은 최상단의 깃허브 링크를 통해 참고 바랍니다.

 

[REFACTOR]입력 상태에 따른 AC / C 버튼 출력 동적 변환 및 UI 조정

 먼저 AC / C 버튼의 노출 동적 변화 기능 구현입니다. AC는 All Clear, C는 Clear 역할을 맡기 때문에 각각의 기능을 구분해야 하며, 유저에게 노출되는 버튼 역시, 입력된 값이 존재하면 C 그게 아니면 AC로 노출될 수 있어야 합니다. 따라서 해당 부분을 _shouldResetDisplay 플래그가 참일 때 AC 노출, 입력 중일 땐 C로 노출되도록 분기점 처리를 설정했습니다. 그리고 C 기능을 추가하여 C로 클릭되었을 시 _display 만 초기화 되도록 변경했습니다.  

아이폰에서는 +/- 버튼을 눌러 음수 변환 시 소괄호( ) 가 감싸지도록 되어 있어 해당 UI를 개선했습니다. 동일하게 백스페이스 버튼 역시 해당 음수를 지울 땐 괄호를 모두 지우도록 추가 예외 케이스를 부여했습니다. 이 부분에선 착각하여 모든 음수에 대해 소괄호가 나오도록 출력했으나 이는 실제 어플과 달랐습니다. 이 부분은 이후 커밋에서 재수정합니다.

 

[FEATURE] 천 단위 ',' 출력 및 과거 수식 기록 출력 추가를 통한 가시성 향상

 이후 위처럼, 아이폰에서 제공되는 연산 값 위해 직전 수식을 출력하는 사용자 경험 향상을 위한 기능을 추가하는 과정에서, 저장 변수 설정의 논리적 오류와, 이로 인해 발생하는 삼중 연산에서의 원활하지 못한 연산 순서 및 값 손실 버그를 식별했고 해당 부분을 리펙토링 했습니다. _calculate 함수를 구축하고, 담는 변수를 배열로 담으며 각각의 인덱스를 토큰 변수로 공백 split()을 통해 구분하여 유지할 수 있도록 구축하였으며 동시에, 백슬라이드와 같이 직접적으로 이들을 수정하는 과정에서도 해당 구분 배열화에 맞춰 연산될 수 있도록 수정했습니다. 당장에는 반복문을 통해 사칙연산에서 우선순위가 되는 * 와 / 를 우선 탐색하여 연산 후, 해당 연산자들의 연산 종료 후 합차 연산을 진행하도록 구축했으나, 이보다 적은 시간복잡도로 탐색 및 연산 가능한 방법이 존재할 경우 추후 리펙토링 예정입니다. 그리고 위처럼 직전 기록을 유지할 수 있도록 _hitory 변수를 추가하여, = 클릭하여 연산 진행 시 해당 수식 식 자체를 해당 변수에 저장해 출력보단 작은 글씨와 짙은 색상으로 상단에 표시되도록 설정했습니다. 관련 부분에 대해, 자료 탐색을 위해 선례 기능을 조회하던 중 SingleChildScrollView를 사용하여 스크롤을 적용 시켜 해당 출력이 길어져도 ...으로 표시되는게 아닌 스크롤 스와이프 형식이 되도록 제공하는 UI적 기능이 존재함을 확인하여 이를 적용시켰습니다. 나아가, 위처럼 단위가 큰 숫자는 천 단위에서 ' , '을 통해 구분해야 하기 때문에 출력에 이를 추가했습니다. 

 

[REFACTOR] % 기능 실제 아이폰과 동일 동작 되도록 리펙토링

 과정에서 %(퍼센트 = /100) 기능이 제대로 동작하지 않음을 식별했습니다. 먼저 % 입력시 이것이 입력창에 보이는 것이 아니라, 실제 계산에 즉시 반영되어 누르자마자 즉시 출력되는 상태로 존재했습니다. 관련 기능 구현에서 단순 /100 만 처리하고, 당시 버튼 구현 단계와 실제 수식 구현과 출력 연결 단계에서의 계획 함수가 상이 했던 차이가 존재했는데, 해당 일관성 달성의 실패로 발생한 버그로 유추되었습니다. 따라서 해당 % 버튼의 함수 _applyPercent 역시 즉시 계산이 아닌 화면상 % 의 출력과 이후 연산에서 % 출력시 /100으로 연산되도록 추가 반영하여 해당 부분을 리펙토링 했으며, %라는 문자열이 추가되었기 때문에 관련 부분의 삭제 등 문자열 입출력단위에서의 기능적 부분을 추가했습니다. 추가적으로 아이폰의 계산기는 ' = '을 결과값 상태에서 그대로 클릭 했을 시 직전 연산을 반복하는 UX적 기능을 제공하고 있습니다. 따라서 이를 반영하기 위해 출력 상태에서 아무 입력없이(=> tokens의 length가 1일 때) 동일 연산이 반복되도록 _calculate() 함수를 수정했습니다. 

 

[REFACTOR] 일반적이지 않은 입력의 edge case로 인한 버그 발생 차단

예를 들어 99 + = 으로 입력 시 + 뒤에 받아야 할 인자가 오지 않아, 이를 Error 로 출력되도록, 정확히는 어떠한 조건으로든 이러한 에러가 발생했을 때 Error로 이어지도록 엔드포인트를 설정했는데, 그중 위와 같은 수식 입력 시, 아이폰에서는 아예 연산 자체가 안되고 화면 변화가 없도록 설정되어 있음을 확인했습니다. 따라서 해당 프로젝트에서도 동일하게 연산되지 않게 차단점을 구축했습니다.  _calculate() 함수에서 마지막 값이 연산자(+-*/) 중 하나라면(hasMatch() 를 통해 조회) 별도 출력 없이(미입력시와 동일하게) 리턴되도록 설정시켰습니다. 

 

[FEATURE] 추가 입력 없이 단순 = 반복시 직전 수식의 반복 실행 및 % 계열 버그 일부 수정

 위에서 언급한 직전 연산의 반복에 대해, % 기능 구현 중 확인한 내용이다보니, 해당 부분을 %에만 집중해서 구현했었습니다. 다만 타 연산의 경우 연산자 와 수의 결합으로 적용되어야 하기 때문에 해당 반복식의 정확한 함수 구현이 필요하였고 따라서, _lastOperator와 _lastOperand를 통해 마지막 연산자와 수를 명시적으로 저장하고, 이를 별도 추가 없이 = 반복이 이루어 졌을 때, 이들이 적용되도록 해당 기능을 추가했습니다. 또한 %를 지속 반복 했을 때, 소수 오른쪽으로도 0000000으로 길게 출력되는 양상을 확인할 수 있었습니다. 해당 추출력 버그 역시 해당 소수점이 깊어질수록 잘라내는 형식으로 조치했습니다. 다만 이는 자연 상수에 음수 지수를 취할 정도로 작은 수가 결과로 나왔을 때 역시 잘라버려 아예 0으로 출력하는 치명적인 논리적 오류가 존재했습니다. 이후 해당 부분을 식별하여 재조치합니다. 나아가 +/- 와 % 이용시 과거 연산 데이터로 처리되는 버그가 발견되어, 명시적으로 _history 변수와 _lastOperator, _lastOperand가 초기화되도록 구현했습니다. 물론 % 입력 후 취소 시 과거 수식은 이미 초기화된 상태라 되돌릴 수 없습니다. 다만 아이폰에서도 이와 동일하게 동작함이 확인되어, 사용자 경험 측면에서 해당 부분이 보다 만족이 높아서 이와 같이 구현되지 않았나 추론중입니다. 다만 이만큼 깊게 사용한 경험이 없어 정확한 판단이 어려워, 가장 기본되는 목적인 역설계 달성을 위해 동일하게 구현했습니다.

 

[REFACTOR] +/- 클릭 후 = 클릭 시 이전 식 반복 및 음수의 소괄호 표기 iOS 계산기 프로그램과 동일화

 여기까지 확인했을 때는, 최종 연산시에도 값이 음수일 시 그 결과도 소괄호에 감싸져 출력되었으나 이 부분이 아이폰과 상이하여 동일 출력이 나타나도록 삭제처리했습니다. 

 

여기까지가 최초 풀리퀘스트 "계산기 기본 모드 기능 구현" 이었으며, 그 전체 코드는 아래와 같습니다. 

import 'package:flutter/material.dart';

void main() {
  runApp(const CalculatorApp());
}

class CalculatorApp extends StatelessWidget {
  const CalculatorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'iOS Calculator',
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: Colors.black,
      ),
      home: const CalculatorHome(),
    );
  }
}

class CalculatorHome extends StatefulWidget {
  const CalculatorHome({super.key});

  @override
  State<CalculatorHome> createState() => _CalculatorHomeState();
}

class _CalculatorHomeState extends State<CalculatorHome> {
  String _display = '0';
  double? _firstOperand;
  String? _operator;
  bool _shouldResetDisplay = false;

  void _onButtonPressed(String text) {
    setState(() {
      if (RegExp(r'^[0-9]$').hasMatch(text)) {
        if (_display == '0' || _shouldResetDisplay) {
          _display = text;
          _shouldResetDisplay = false;
        } else {
          _display += text;
        }
      } else if (text == '.') {
        if (!_display.contains('.')) {
          _display += '.';
        }
      } else if (text == '⌫') {
        if (_display.length > 1) {
          _display = _display.substring(0, _display.length - 1);
        } else {
          _display = '0';
        }
      } else if (text == 'AC' || text == 'C') {
        _display = '0';
        _firstOperand = null;
        _operator = null;
        _shouldResetDisplay = false;
      } else if (text == '+/-') {
        if (_display != '0') {
          if (_display.startsWith('-')) {
            _display = _display.substring(1);
          } else {
            _display = '-$_display';
          }
        }
      } else if (text == '%') {
        double val = double.parse(_display) / 100;
        _display = _formatResult(val);
      } else if (text == '+' || text == '-' || text == '×' || text == '÷') {
        _firstOperand = double.parse(_display);
        _operator = text;
        _shouldResetDisplay = true;
      } else if (text == '=') {
        if (_firstOperand != null && _operator != null) {
          double secondOperand = double.parse(_display);
          double result = 0;
          switch (_operator) {
            case '+':
              result = _firstOperand! + secondOperand;
              break;
            case '-':
              result = _firstOperand! - secondOperand;
              break;
            case '×':
              result = _firstOperand! * secondOperand;
              break;
            case '÷':
              result = _firstOperand! / secondOperand;
              break;
          }
          _display = _formatResult(result);
          _firstOperand = null;
          _operator = null;
          _shouldResetDisplay = true;
        }
      }
    });
  }

  String _formatResult(double result) {
    if (result.isInfinite || result.isNaN) return 'Error';
    if (result == result.toInt()) {
      return result.toInt().toString();
    }
    String str = result.toString();
    if (str.length > 10) {
      return result.toStringAsPrecision(8);
    }
    return str;
  }

  Widget _buildButton(String text, Color bgColor, Color textColor, {bool isWide = false}) {
    return Expanded(
      flex: isWide ? 2 : 1,
      child: Padding(
        padding: const EdgeInsets.all(6.0),
        child: InkWell(
          onTap: () => _onButtonPressed(text),
          borderRadius: BorderRadius.circular(50),
          child: Container(
            height: 70,
            decoration: BoxDecoration(
              color: bgColor,
              shape: isWide ? BoxShape.rectangle : BoxShape.circle,
              borderRadius: isWide ? BorderRadius.circular(50) : null,
            ),
            child: Center(
              child: Text(
                text,
                style: TextStyle(
                  fontSize: text == '⌫' ? 24 : 28,
                  fontWeight: FontWeight.w500,
                  color: textColor,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: Container(
                alignment: Alignment.bottomRight,
                padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
                child: Text(
                  _display,
                  style: const TextStyle(
                    fontSize: 70,
                    fontWeight: FontWeight.w300,
                    color: Colors.white,
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ),
            Column(
              children: [
                Row(
                  children: [
                    _buildButton('⌫', Colors.grey[600]!, Colors.white),
                    _buildButton('AC', Colors.grey[400]!, Colors.black),
                    _buildButton('%', Colors.grey[400]!, Colors.black),
                    _buildButton('÷', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('7', Colors.grey[850]!, Colors.white),
                    _buildButton('8', Colors.grey[850]!, Colors.white),
                    _buildButton('9', Colors.grey[850]!, Colors.white),
                    _buildButton('×', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('4', Colors.grey[850]!, Colors.white),
                    _buildButton('5', Colors.grey[850]!, Colors.white),
                    _buildButton('6', Colors.grey[850]!, Colors.white),
                    _buildButton('-', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('1', Colors.grey[850]!, Colors.white),
                    _buildButton('2', Colors.grey[850]!, Colors.white),
                    _buildButton('3', Colors.grey[850]!, Colors.white),
                    _buildButton('+', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('+/-', Colors.grey[850]!, Colors.white),
                    _buildButton('0', Colors.grey[850]!, Colors.white),
                    _buildButton('.', Colors.grey[850]!, Colors.white),
                    _buildButton('=', Colors.orange, Colors.white),
                  ],
                ),
                const SizedBox(height: 20),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

기본적인 기능의 구현은 마쳤으나, 지속적인 리펙토링과 관련 연산 정책의 크고작은 변경으로 일부 버그가 발생하였으며, 아이폰에서 제공되는 일부 기능들은 여전히 미구형되었음을 식별하여 추가적인 작업을 진행했습니다. 

 

[FEATURE] 연산 결과에서, 수식 없이 상수만 입력 시 직전 수식의 최전 상수를 해당 상수로 변경하여 재연산하는 UX 기능 구현

 테스트 과정에서, 예를 들어 9 * 9 = 81로 출력된 상태에서 10만 입력했을 시, 아이폰에서는 10 * 9로 연산되는 별도 기능이 구현되어있음을 확인하여 해당 부분에 대한 추가적인 분기 생성이 필요해졌음을 식별했습니다. 따라서 토큰이 1개만 존재할 경우 (_tokens.length == 1, 다만 명시적으로 문제 생기지 않도록 마지막 연산자와 수가 존재할 때를 추가 설정했습니다.) 해당 토큰에 마지막 연산을 반복하도록(_repeatLastOperation()) 설정했습니다. 또한 위에서 언급하였던 자연상수의 음수지수 단위까지 내려갈 경우 0으로 일괄처리되는 논리적 오류 부분을 해결하기 위해 _farmatValue를 수정하여 absRes의 범위 분기를 통해 상당히 크거나 작은 수는 e를 활용한 표기법으로 노출되도록 예외처리 시켰습니다. 다만 명시적 분리이다 보니, 기능적으로는 모르겠으나 코드에 대한 가시성은 좋지 못합니다. 추후 필요 시 수정이 필요할지도 모르겠습니다. 관련해 상단의 커밋과 동일하게 N.0000e^-m 으로 출력되는 오류가 발생하여, 해당 커밋 과정에서 동시에, 자연상수를 기준점 삼아 parts로 구분시키고 이를 붙여 출력하는 방식으로 슬라이싱하여 구현하였습니다. 해당 부분에서의 버그는 탐색된 건은 없습니다. 

 

[REFACTOR] C 버튼 최초 클릭 시 최후단 상수만, 재클릭 시 전체 삭제되도록 기능 조정

 과정에서 다수의 연산 작성 후 C를 클릭 했을 때, 최후열 오퍼랜드만 삭제하고, 이후 클릭 시 전체 식이 삭제되는 로직으로 동작하는 점을 아이폰 계산기에서 식별했습니다. 따라서 해당 프로그램도 동일하게 동작 할 수 있도록 C 함수를 수정하여 반복 클릭될 시 AC로 동작하도록 설정했습니다. 

 

[FEATURE] 연산 결과에서 수식 없이 상수 입력 시 최전 상수에 연산토록 기능 추가 및 C 버튼 최초 및 반복 클릭 시 삭제 범위 조정으로 UX 기능 구축 및 소수점 입력 버그 수정

 이 커밋은 위 두커밋에 소수점 입력 버그 수정만 추가하였습니다. 해당 과정에서 커밋 실수를 저질러 그래프 분기가 추가 생성되는 실수가 발생해 이를 해결하는 중 위 두 커밋을 undo 처리 후 일괄 커밋 시킨 뒤 머지시키는 방식으로 조치하느라 작성 문구는 이와 같이 적었습니다. 다만 실제 추가된 내용은 연산 결과가 나와있는 상태에서 소수점만 입력 후 = 입력 시, 이전 수식으로만 반복되는 연산 오류가 식별되었습니다. 실제는 단순 소수점만 입력 시 0으로 인식되어 0 에 마지막 연산을 반복해야 했기 때문에, 완전히 0.0으로 인식되도록 해당 부분을 수정했습니다. 

import 'package:flutter/material.dart';

void main() {
  runApp(const CalculatorApp());
}

class CalculatorApp extends StatelessWidget {
  const CalculatorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'iOS Calculator',
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: Colors.black,
      ),
      home: const CalculatorHome(),
    );
  }
}

class CalculatorHome extends StatefulWidget {
  const CalculatorHome({super.key});

  @override
  State<CalculatorHome> createState() => _CalculatorHomeState();
}

class _CalculatorHomeState extends State<CalculatorHome> {
  String _expression = '0';
  String _history = '';
  bool _isResultDisplayed = false;
  String? _lastOperator;
  double? _lastOperand;

  void _onButtonPressed(String text) {
    setState(() {
      if (RegExp(r'^[0-9]$').hasMatch(text)) {
        if (_expression == '0' || _isResultDisplayed) {
          if (_isResultDisplayed) _history = '';
          _expression = text;
          _isResultDisplayed = false;
        } else {
          List<String> tokens = _expression.split(' ');
          String lastToken = tokens.last;
          if (RegExp(r'^[0-9,.]+$').hasMatch(lastToken)) {
            String cleanNumber = lastToken.replaceAll(',', '');
            tokens[tokens.length - 1] = _addCommas(cleanNumber + text);
            _expression = tokens.join(' ');
          } else if (RegExp(r'^\(.*\)$').hasMatch(lastToken)) {
            String numberPart = lastToken.substring(2, lastToken.length - 1).replaceAll(',', '');
            tokens[tokens.length - 1] = '(-${_addCommas(numberPart + text)})';
            _expression = tokens.join(' ');
          } else {
            _expression += text;
          }
        }
      } else if (text == '.') {
        if (_isResultDisplayed) {
          _history = '';
          _expression = '0.';
          _isResultDisplayed = false;
        } else {
          List<String> tokens = _expression.split(' ');
          String lastToken = tokens.last.replaceAll(',', '');
          if (!lastToken.contains('.')) {
            if (RegExp(r'^\(.*\)$').hasMatch(lastToken)) {
              String numberPart = lastToken.substring(2, lastToken.length - 1);
              tokens[tokens.length - 1] = '(-$numberPart.)';
              _expression = tokens.join(' ');
            } else {
              _expression += '.';
            }
          }
        }
      } else if (text == '⌫') {
        _handleBackspace();
      } else if (text == 'AC') {
        _expression = '0';
        _history = '';
        _isResultDisplayed = false;
        _lastOperator = null;
        _lastOperand = null;
      } else if (text == 'C') {
        List<String> tokens = _expression.trim().split(' ');
        if (tokens.isNotEmpty && 
            (RegExp(r'^[0-9,.]+$').hasMatch(tokens.last.replaceAll('(', '').replaceAll(')', '').replaceAll('%', ''))) && 
            tokens.last != '0') {
          // 마지막이 숫자면 숫자만 제거
          tokens.removeLast();
          if (tokens.isEmpty) {
            _expression = '0';
          } else {
            // 연산자 뒤에 공백을 유지하기 위해 마지막에 공백 추가
            _expression = '${tokens.join(' ')} ';
          }
        } else {
          // 숫자가 아니거나 이미 비어있으면 전체 초기화
          _expression = '0';
          _history = '';
          _isResultDisplayed = false;
          _lastOperator = null;
          _lastOperand = null;
        }
      } else if (text == '+/-') {
        _toggleSign();
      } else if (text == '%') {
        if (_isResultDisplayed) {
          _isResultDisplayed = false;
          _history = '';
          _lastOperator = null;
          _lastOperand = null;
        }
        _applyPercent();
      } else if (text == '+' || text == '-' || text == '×' || text == '÷') {
        if (_isResultDisplayed) {
          _isResultDisplayed = false;
          _history = '';
        }
        if (RegExp(r'[+×÷-]$').hasMatch(_expression.trim())) {
          _expression = _expression.trim().substring(0, _expression.trim().length - 1) + text + ' ';
        } else {
          _expression = '${_expression.trim()} $text ';
        }
      } else if (text == '=') {
        List<String> tokens = _expression.trim().split(' ');
        if (tokens.length == 1 && _lastOperator != null && _lastOperand != null) {
          // 숫자 하나만 입력된 상태에서 =을 누르면 직전 연산 적용
          _repeatLastOperation();
        } else if (_isResultDisplayed && _lastOperator != null && _lastOperand != null) {
          _repeatLastOperation();
        } else {
          _calculate();
        }
      }
    });
  }

  void _repeatLastOperation() {
    double currentVal = _parseToken(_expression);
    double result = 0;
    
    if (_lastOperator == '%') {
      result = currentVal / 100.0;
      _history = "${_addCommas(_formatValue(currentVal))}%";
    } else {
      switch (_lastOperator) {
        case '+': result = currentVal + _lastOperand!; break;
        case '-': result = currentVal - _lastOperand!; break;
        case '×': result = currentVal * _lastOperand!; break;
        case '÷': result = currentVal / _lastOperand!; break;
      }
      _history = "${_addCommas(_formatValue(currentVal))} $_lastOperator ${_addCommas(_formatValue(_lastOperand!))}";
    }
    
    _expression = _formatResult(result);
    _isResultDisplayed = true;
  }

  void _handleBackspace() {
    if (_expression.endsWith(' ')) {
      _expression = _expression.trim();
      _expression = _expression.substring(0, _expression.length - 1).trim();
    } else {
      List<String> tokens = _expression.split(' ');
      String lastToken = tokens.last;
      if (lastToken.length > 1) {
        if (lastToken.endsWith('%')) {
          tokens[tokens.length - 1] = lastToken.substring(0, lastToken.length - 1);
        } else if (RegExp(r'^\(.*\)$').hasMatch(lastToken)) {
          String numberPart = lastToken.substring(2, lastToken.length - 1).replaceAll(',', '');
          if (numberPart.length > 1) {
            tokens[tokens.length - 1] = '(-${_addCommas(numberPart.substring(0, numberPart.length - 1))})';
          } else {
            tokens[tokens.length - 1] = '0';
          }
        } else {
          String cleanNumber = lastToken.replaceAll(',', '');
          tokens[tokens.length - 1] = _addCommas(cleanNumber.substring(0, cleanNumber.length - 1));
        }
        _expression = tokens.join(' ');
      } else {
        if (tokens.length > 1) {
          tokens.removeLast();
          _expression = tokens.join(' ');
        } else {
          _expression = '0';
        }
      }
    }
    if (_expression.isEmpty) _expression = '0';
  }

  String _addCommas(String s) {
    if (s.isEmpty) return '';
    List<String> parts = s.split('.');
    RegExp reg = RegExp(r'\B(?=(\d{3})+(?!\d))');
    parts[0] = parts[0].replaceAll(reg, ',');
    return parts.join('.');
  }

  void _toggleSign() {
    setState(() {
      List<String> tokens = _expression.trim().split(' ');
      if (tokens.isEmpty) return;

      String lastToken = tokens.last;
      
      // 1. 이미 음수(괄호형 또는 단순형)인 경우 양수로 전환
      if (lastToken.startsWith('(-') && lastToken.endsWith(')')) {
        tokens[tokens.length - 1] = lastToken.substring(2, lastToken.length - 1);
      } else if (lastToken.startsWith('-')) {
        tokens[tokens.length - 1] = lastToken.substring(1);
      } 
      // 2. 양수인 경우 음수로 전환 (항상 괄호 사용)
      else {
        String cleanNumber = lastToken.replaceAll(',', '');
        if (double.tryParse(cleanNumber) != null && cleanNumber != '0') {
          tokens[tokens.length - 1] = '(-$lastToken)';
        }
      }
      _expression = tokens.join(' ');
    });
  }

  void _applyPercent() {
    List<String> tokens = _expression.trim().split(' ');
    String lastToken = tokens.last;
    
    if (lastToken.isNotEmpty && !lastToken.endsWith('%') && 
        (RegExp(r'[0-9,.]+$').hasMatch(lastToken) || RegExp(r'^\(.*\)$').hasMatch(lastToken))) {
      tokens[tokens.length - 1] = '$lastToken%';
      _expression = tokens.join(' ');
    }
  }

  double _parseToken(String token) {
    String clean = token.replaceAll('(', '').replaceAll(')', '').replaceAll(',', '');
    bool isPercent = clean.endsWith('%');
    if (isPercent) {
      clean = clean.substring(0, clean.length - 1);
    }
    double val = double.tryParse(clean) ?? 0;
    return isPercent ? val / 100 : val;
  }

  String _formatValue(double result) {
    if (result == 0) return '0';
    
    String s;
    double absRes = result.abs();
    
    // 매우 작거나 매우 큰 숫자는 지수 표기법 사용
    if (absRes < 0.000001 || absRes > 999999999) {
      s = result.toStringAsPrecision(7);
    } else {
      // 일반적인 숫자는 최대한 정밀하게 출력 후 불필요한 0 제거
      s = result.toStringAsFixed(10);
      if (s.contains('.')) {
        s = s.replaceAll(RegExp(r'0+$'), '');
        if (s.endsWith('.')) s = s.substring(0, s.length - 1);
      }
    }

    // 지수 표기법 결과에서도 불필요한 0 제거 (예: 1.000e-10 -> 1e-10)
    if (s.contains('e')) {
      List<String> parts = s.split('e');
      if (parts[0].contains('.')) {
        parts[0] = parts[0].replaceAll(RegExp(r'0+$'), '');
        if (parts[0].endsWith('.')) parts[0] = parts[0].substring(0, parts[0].length - 1);
      }
      s = parts.join('e');
    }
    
    return _addCommas(s);
  }

  void _calculate() {
    try {
      String trimmed = _expression.trim();
      if (RegExp(r'[+×÷-]$').hasMatch(trimmed)) return;

      List<String> tokens = trimmed.split(' ');
      if (tokens.isEmpty) return;

      if (tokens.length == 1) {
        double val = _parseToken(tokens[0]);
        setState(() {
          _history = _expression;
          if (tokens[0].endsWith('%')) {
            _lastOperator = '%';
            _lastOperand = 100.0;
          }
          _expression = _formatResult(val);
          _isResultDisplayed = true;
        });
        return;
      }

      _history = _expression;
      List<dynamic> values = [];
      for (var t in tokens) {
        if (RegExp(r'[+×÷-]').hasMatch(t) && t.length == 1) {
          values.add(t);
        } else {
          values.add(_parseToken(t));
        }
      }

      if (tokens.length >= 3) {
        _lastOperator = tokens[tokens.length - 2];
        _lastOperand = _parseToken(tokens.last);
      }

      for (int i = 0; i < values.length; i++) {
        if (values[i] == '×' || values[i] == '÷') {
          double left = values[i - 1];
          double right = values[i + 1];
          double res = (values[i] == '×') ? left * right : left / right;
          values.removeAt(i - 1);
          values.removeAt(i - 1);
          values.removeAt(i - 1);
          values.insert(i - 1, res);
          i--;
        }
      }

      double finalRes = values[0];
      for (int i = 1; i < values.length; i += 2) {
        String op = values[i];
        double nextVal = values[i + 1];
        if (op == '+') finalRes += nextVal;
        if (op == '-') finalRes -= nextVal;
      }

      setState(() {
        _expression = _formatResult(finalRes);
        _isResultDisplayed = true;
      });
    } catch (e) {
      setState(() {
        _expression = 'Error';
        _isResultDisplayed = true;
      });
    }
  }

  String _formatResult(double result) {
    if (result.isInfinite || result.isNaN) return 'Error';
    return _formatValue(result);
  }

  Widget _buildButton(String text, Color bgColor, Color textColor) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.all(6.0),
        child: InkWell(
          onTap: () => _onButtonPressed(text),
          borderRadius: BorderRadius.circular(50),
          child: Container(
            height: 70,
            decoration: BoxDecoration(color: bgColor, shape: BoxShape.circle),
            child: Center(
              child: Text(
                text,
                style: TextStyle(
                  fontSize: text == '⌫' ? 24 : 28,
                  fontWeight: FontWeight.w500,
                  color: textColor,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: Container(
                alignment: Alignment.bottomRight,
                padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.end,
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    if (_history.isNotEmpty)
                      SingleChildScrollView(
                        reverse: true,
                        scrollDirection: Axis.horizontal,
                        child: Text(
                          _history,
                          style: const TextStyle(
                            fontSize: 24,
                            color: Colors.grey,
                            fontWeight: FontWeight.w400,
                          ),
                        ),
                      ),
                    const SizedBox(height: 8),
                    SingleChildScrollView(
                      reverse: true,
                      scrollDirection: Axis.horizontal,
                      child: Text(
                        _expression,
                        style: TextStyle(
                          fontSize: _expression.length > 10 ? 40 : 60,
                          fontWeight: FontWeight.w300,
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            Column(
              children: [
                Row(
                  children: [
                    _buildButton('⌫', Colors.grey[600]!, Colors.white),
                    _buildButton(
                      (_expression == '0' || _isResultDisplayed) ? 'AC' : 'C',
                      Colors.grey[400]!,
                      Colors.black,
                    ),
                    _buildButton('%', Colors.grey[400]!, Colors.black),
                    _buildButton('÷', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('7', Colors.grey[850]!, Colors.white),
                    _buildButton('8', Colors.grey[850]!, Colors.white),
                    _buildButton('9', Colors.grey[850]!, Colors.white),
                    _buildButton('×', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('4', Colors.grey[850]!, Colors.white),
                    _buildButton('5', Colors.grey[850]!, Colors.white),
                    _buildButton('6', Colors.grey[850]!, Colors.white),
                    _buildButton('-', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('1', Colors.grey[850]!, Colors.white),
                    _buildButton('2', Colors.grey[850]!, Colors.white),
                    _buildButton('3', Colors.grey[850]!, Colors.white),
                    _buildButton('+', Colors.orange, Colors.white),
                  ],
                ),
                Row(
                  children: [
                    _buildButton('+/-', Colors.grey[850]!, Colors.white),
                    _buildButton('0', Colors.grey[850]!, Colors.white),
                    _buildButton('.', Colors.grey[850]!, Colors.white),
                    _buildButton('=', Colors.orange, Colors.white),
                  ],
                ),
                const SizedBox(height: 20),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

이후 공학 계산기를 구현할 계획이며, 이를 완료할 경우 수학 메모 및 변환 기능을 데이터 베이스와 연결시켜 추가할 계획입니다. 현재 화면상 출력은 아래와 같습니다.

 

관련 과정에서 UI 구축과 UX에 대한 고민을 진행할 수 있었으며, 초기 기획 단계에서 정확한 정책이 구축되어야 로직이 꼬여 관련 버그가 대량발생하게되는 위험을 미연에 방지할 수 있음을 느꼈습니다. 체계성에 대해 다시한번 고민할 수 있는 뜻깊은 시간이었으며 로직의 논리적 구현의 중요성을 체감할 수 있었습니다. 그리고 역설계를 통해 기존 계산기 프로그램의 기능들과 이들 구현을 위한 함수 구축을 깊게 고민해볼 수 있는 뜻깊은 시간이었습니다.