Flutter 중간시험 - 계산기 프로젝트

2024. 10. 23. 10:11flutter

https://youtu.be/Pj5whnY7ynI

 

 

소스코드

 

main.dart

import 'package:flutter/material.dart';
import 'calculator_button.dart';  // 버튼 위젯을 정의한 파일 import
import 'calculator_logic.dart';  // 계산 로직을 정의한 파일 import

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

// MyApp 클래스는 앱의 최상위 위젯으로, 전체 앱 구조를 정의
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      // 앱의 첫 화면으로 CalculatorApp을 설정
      home: CalculatorApp(),
    );
  }
}

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

  @override
  _CalculatorAppState createState() => _CalculatorAppState();
}

class _CalculatorAppState extends State<CalculatorApp> {
  // 계산기의 현재 디스플레이에 표시될 문자열
  String displayText = '0';

  // CalculatorLogic 클래스의 인스턴스를 생성해 계산 로직을 처리
  CalculatorLogic logic = CalculatorLogic();

  // 버튼이 눌렸을 때 호출되는 함수, 계산 로직을 처리하고 화면에 출력
  void onButtonPressed(String buttonText) {
    setState(() {
      // 계산 로직에서 입력된 값을 처리한 결과를 displayText에 반영
      displayText = logic.handleInput(buttonText);
    });
  }

  // 앱의 UI를 정의하는 build 메서드
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 앱의 상단 바에 표시되는 타이틀
        title: const Text('202316035 박준혁 계산기'),
      ),
      // 앱 배경색 설정
      backgroundColor: Colors.black12,
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          // 계산기 디스플레이 영역, 사용자가 입력한 값과 결과를 표시
          Expanded(
            flex: 1,  // 디스플레이 영역의 크기를 전체 화면의 1/3로 설정
            child: Container(
              alignment: Alignment.centerRight,  // 텍스트를 오른쪽으로 정렬
              padding: const EdgeInsets.all(20), // 디스플레이 영역의 여백 설정
              child: Text(
                displayText,  // 현재 표시할 텍스트 (계산 결과 또는 입력된 값)
                style: const TextStyle(
                    fontSize: 48,  // 텍스트 크기
                    color: Colors.white  // 텍스트 색상 (흰색)
                ),
              ),
            ),
          ),
          // 계산기 버튼 영역
          Expanded(
            flex: 2,  // 버튼 영역의 크기를 전체 화면의 2/3로 설정
            child: CalculatorButtons(onButtonPressed: onButtonPressed), // CalculatorButtons 위젯에 버튼 클릭 시 동작할 콜백 전달
          ),
        ],
      ),
    );
  }
}

 

 

calculator_button.dart

import 'package:flutter/material.dart';

// 계산기 버튼들을 묶어서 관리하는 CalculatorButtons 클래스
class CalculatorButtons extends StatelessWidget {
  // 버튼이 눌렸을 때 호출될 콜백 함수를 받아옴
  final Function(String) onButtonPressed;

  // 생성자에서 onButtonPressed 콜백 함수를 받아옴
  const CalculatorButtons({super.key, required this.onButtonPressed});

  // 각 버튼들을 배치하는 메서드인 buildButtonRow를 이용해 UI 구성
  @override
  Widget build(BuildContext context) {
    return Column(
      // 버튼 배열을 행으로 나누어 배치
      children: [
        buildButtonRow(['C', '⌫', '%', '÷']),  // 첫 번째 행 (기능 버튼들)
        buildButtonRow(['7', '8', '9', '×']),  // 두 번째 행 (숫자 및 연산 버튼들)
        buildButtonRow(['4', '5', '6', '-']),  // 세 번째 행
        buildButtonRow(['1', '2', '3', '+']),  // 네 번째 행
        buildButtonRow(['0', '.', '='], isLastRow: true),  // 마지막 행
      ],
    );
  }

  // 버튼을 한 행씩 나누어 생성하는 메서드
  Widget buildButtonRow(List<String> buttons, {bool isLastRow = false}) {
    return Expanded(
      // 버튼 행을 채우도록 확장
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,  // 버튼들이 세로로 늘어나도록 설정
        children: buttons.map((buttonText) {
          if (buttonText == '0' && isLastRow) {
            // 0 버튼이 마지막 행일 경우 2칸 크기로 확장
            return Expanded(
              flex: 2,  // 0 버튼은 두 칸 크기 차지
              child: CalculatorButton(
                text: buttonText,
                onTap: () => onButtonPressed(buttonText),  // 버튼 눌렸을 때 콜백 호출
              ),
            );
          } else {
            return Expanded(
              flex: 1,  // 나머지 버튼은 한 칸 크기 차지
              child: CalculatorButton(
                text: buttonText,
                onTap: () => onButtonPressed(buttonText),  // 버튼 눌렸을 때 콜백 호출
              ),
            );
          }
        }).toList(),  // 버튼 리스트를 Row로 변환
      ),
    );
  }
}

// 각 계산기 버튼을 정의하는 CalculatorButton 클래스
class CalculatorButton extends StatelessWidget {
  // 버튼 텍스트와 버튼이 눌렸을 때 실행할 콜백을 받아옴
  final String text;
  final VoidCallback onTap;

  // 생성자에서 버튼 텍스트와 콜백을 받아옴
  const CalculatorButton({super.key, required this.text, required this.onTap});

  // 버튼에 따라 색상을 결정하는 함수
  @override
  Widget build(BuildContext context) {
    // 입력된 버튼 텍스트에 따라 색상을 반환
    Color? getColor(String value) {
      if (['+', '-', '×', '÷', '='].contains(value)) {
        return Colors.yellow[800];  // 연산자 버튼 색상
      } else if (['C', '⌫', '%'].contains(value)) {
        return Colors.grey[600];  // 기능 버튼 색상
      } else {
        return Colors.grey[800];  // 숫자 버튼 색상
      }
    }

    // 버튼의 UI를 정의
    return Padding(
      // 버튼 간의 간격을 위한 패딩 설정
      padding: const EdgeInsets.all(3.0),
      child: ElevatedButton(
        // 버튼 스타일 설정
        style: ElevatedButton.styleFrom(
          backgroundColor: getColor(text),  // 버튼 색상 설정
          padding: const EdgeInsets.all(16.0),  // 버튼 내부 여백 설정
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(40),  // 버튼 모서리를 둥글게 설정
          ),
        ),
        onPressed: onTap,  // 버튼이 눌리면 콜백 함수 실행
        child: Text(
          text,  // 버튼에 표시될 텍스트
          style: const TextStyle(
            fontSize: 30,  // 버튼 텍스트 크기
            color: Colors.white,  // 버튼 텍스트 색상 (흰색)
          ),
        ),
      ),
    );
  }
}

 

 

calculator_logic.dart

class CalculatorLogic {
  // 화면에 표시되는 텍스트를 저장하는 변수
  String _displayText = '';

  // 입력된 버튼 값에 따라 동작을 처리하는 함수
  String handleInput(String input) {
    if (input == '=') {
      // '=' 버튼이 눌리면 수식을 계산하여 결과 표시
      _displayText = _calculate();
    } else if (input == 'C') {
      // 'C' 버튼이 눌리면 계산기 초기화
      _clear();
    } else if (input == '⌫') {
      // '⌫' 버튼이 눌리면 마지막 입력 문자 삭제
      _displayText = _displayText.isNotEmpty
          ? _displayText.substring(0, _displayText.length - 1)
          : '';
    } else {
      // 숫자 및 연산자가 입력될 경우 화면에 추가
      _displayText += input;
    }

    // 화면이 비었으면 0을 반환, 아니면 현재 화면에 표시될 텍스트 반환
    return _displayText.isEmpty ? '0' : _displayText;
  }

  // 입력된 수식을 계산하는 함수
  String _calculate() {
    try {
      // 수식을 숫자와 연산자로 분리
      List<String> tokens = _parseExpression(_displayText);
      // 분리된 수식을 계산
      double result = _evaluateExpression(tokens);
      // 계산 결과가 정수이면 정수형으로 반환, 아니면 소수점 포함 문자열로 반환
      return result == result.toInt() ? result.toInt().toString() : result.toString();
    } catch (e) {
      // 계산 중 에러가 발생하면 'Error' 반환
      return 'Error';
    }
  }

  // 입력된 수식을 숫자와 연산자로 분리하는 함수
  List<String> _parseExpression(String expression) {
    List<String> tokens = []; // 토큰들을 저장할 리스트
    String number = ''; // 숫자를 저장할 변수

    // 수식의 각 문자들을 순회하며 파싱
    for (int i = 0; i < expression.length; i++) {
      String char = expression[i];

      // 현재 문자가 숫자 또는 소수점인 경우
      if ('0123456789.'.contains(char)) {
        number += char; // 숫자에 추가
      }
      // 현재 문자가 연산자인 경우
      else if ('+-×÷'.contains(char)) {
        if (number.isNotEmpty) {
          tokens.add(number); // 숫자를 토큰 리스트에 추가
          number = ''; // 숫자 초기화
        }
        tokens.add(char); // 연산자를 토큰 리스트에 추가
      }
    }

    // 마지막 남은 숫자가 있으면 토큰 리스트에 추가
    if (number.isNotEmpty) {
      tokens.add(number);
    }

    return tokens; // 숫자와 연산자가 분리된 토큰 리스트 반환
  }

  // 분리된 수식을 계산하는 함수
  double _evaluateExpression(List<String> tokens) {
    List<double> numbers = []; // 숫자를 저장할 리스트
    List<String> operators = []; // 연산자를 저장할 리스트

    // 토큰 리스트를 순회하면서 숫자와 연산자 분리
    for (int i = 0; i < tokens.length; i++) {
      String token = tokens[i];

      // 토큰이 연산자인 경우
      if ('+-×÷'.contains(token)) {
        operators.add(token); // 연산자를 저장
      } else {
        // 토큰이 숫자인 경우 double로 변환하여 저장
        numbers.add(double.parse(token));
      }
    }

    // ×, ÷ 연산 우선 처리 (연산자 우선순위 고려)
    for (int i = 0; i < operators.length; i++) {
      if (operators[i] == '×' || operators[i] == '÷') {
        double result;
        if (operators[i] == '×') {
          result = numbers[i] * numbers[i + 1]; // 곱셈 계산
        } else {
          if (numbers[i + 1] == 0) {
            throw Exception('Division by zero'); // 0으로 나누는 경우 예외 처리
          }
          result = numbers[i] / numbers[i + 1]; // 나눗셈 계산
        }

        numbers[i] = result; // 계산 결과를 현재 위치에 덮어씀
        numbers.removeAt(i + 1); // 계산에 사용된 다음 숫자를 리스트에서 제거
        operators.removeAt(i); // 연산자를 리스트에서 제거
        i--; // 리스트가 줄어들었으므로 인덱스를 조정
      }
    }

    // +, - 연산 처리
    double result = numbers[0]; // 첫 번째 숫자부터 시작
    for (int i = 0; i < operators.length; i++) {
      if (operators[i] == '+') {
        result += numbers[i + 1]; // 덧셈
      } else if (operators[i] == '-') {
        result -= numbers[i + 1]; // 뺄셈
      }
    }

    return result; // 최종 결과 반환
  }

  // 계산기 초기화 함수
  void _clear() {
    _displayText = ''; // 화면 텍스트 초기화
  }
}

 


 

안녕하세요. 202316035학번 박준혁입니다.

 

1. main.dart 설명 스크립트

계산기 앱의 메인 파일인 main.dart 파일부터 설명드리겠습니다.

먼저, main() 함수에서 runApp() 메서드를 통해 StatelessWidget을 상속받은 MyApp을 호출했습니다.

MyApp 클래스는 MaterialApp 위젯을 반환합니다. MaterialApp은 Flutter 앱의 전반적인 UI 스타일을 정의하며, 여기서 홈 화면으로 CalculatorApp을 지정했습니다. CalculatorApp은 계산기를 작동할때마다 상태가 변하므로 StatefulWidget을 상속받았습니다.

 

이제 CalculatorAppState 클래스를 살펴보겠습니다. 이 클래스는 계산기의 상태를 관리하는 곳입니다. displayText라는 변수를 통해 현재 화면에 표시되는 값을 저장하고 있습니다. 초기 값은 "0"으로 설정됩니다.

CalculatorLogic 클래스의 인스턴스인 logic을 생성하여 계산 로직을 처리하고 있습니다.

여기서 중요한 부분은 onButtonPressed라는 함수입니다. 이 함수는 사용자가 버튼을 눌렀을 때 호출되며, 버튼의 텍스트 값을 인자로 받아 계산 로직을 처리하고 화면에 업데이트합니다.

build() 함수에서는 Scaffold 위젯을 사용하여 기본 레이아웃을 정의하고 있습니다. 화면 상단 AppBar에는 앱 제목을, 하단에는 계산기 버튼들을 배치하고 있습니다. 이 CalculatorButtons는 별도의 calculator_button 파일에 정의된 위젯으로, 버튼을 그리는 역할을 합니다.

 

 

2. calculator_button.dart 설명 스크립트

다음으로는 계산기 버튼을 정의하는 calculator_button.dart 파일을 설명드리겠습니다.

이 파일은 두 가지 주요 클래스로 구성됩니다. 먼저 CalculatorButtons 클래스입니다. 이 클래스는 모든 계산기 버튼을 배치하는 역할을 합니다. onButtonPressed라는 콜백 함수를 인자로 받아, 버튼이 눌렸을 때 해당 버튼의 텍스트 값을 전달할 수 있도록 했습니다.

build() 함수에서 Column 위젯을 사용하여 버튼들을 행 단위로 배치합니다. 각각의 행은 buildButtonRow() 함수를 통해 생성됩니다. 각 버튼의 텍스트는 리스트로 전달되며, 이를 map() 메서드를 사용하여 반복문 형태로 각 버튼을 생성합니다.

그리고 마지막 줄의 0 버튼은 Expanded 위젯의 flex 값을 2로 설정하여 다른 버튼과 달리 두 칸 크기를 차지하도록 했습니다.

 

다음으로 CalculatorButton 클래스를 살펴보겠습니다. 이 클래스는 각 버튼의 스타일과 동작을 정의합니다. text는 버튼에 표시되는 값이고, onTap은 버튼이 눌렸을 때 호출되는 콜백 함수입니다.

getColor() 함수는 버튼의 종류에 따라 색상을 다르게 지정합니다. +, -, ×, ÷, = 같은 연산자 버튼은 노란색, C, ⌫, % 같은 기능 버튼은 회색, 나머지 숫자 버튼은 짙은 회색으로 표시됩니다.

이렇게 ElevatedButton 위젯을 사용해 스타일과 동작이 지정된 버튼이 생성되며, 사용자가 해당 버튼을 누르면 콜백 함수를 통해 계산 로직으로 전달됩니다.

 

 

3. calculator_logic.dart 설명 스크립트

마지막으로, 계산기의 실제 동작을 처리하는 로직 파일인 calculator_logic.dart 파일을 설명드리겠습니다.

CalculatorLogic 클래스는 계산기의 상태와 입력을 처리하고, 계산 결과를 반환하는 역할을 합니다.

먼저, 가장 중요한 함수는 handleInput()입니다. 이 함수는 입력된 값을 받아 계산기 화면에 출력할 값을 결정합니다. = 버튼이 눌렸을 때는 _calculate() 함수를 호출하여 수식을 계산하고, C 버튼이 눌리면 _clear() 함수로 화면을 초기화합니다. ⌫ 버튼을 누르면 마지막 입력을 삭제합니다. 그 외의 숫자와 연산자는 _displayText에 추가됩니다.

이제 _calculate() 함수로 넘어가보겠습니다. 이 함수는 현재 입력된 수식을 계산하는 역할을 합니다. 먼저 _parseExpression() 함수를 사용하여 입력된 수식을 숫자와 연산자로 나누고, _evaluateExpression() 함수를 사용해 실제로 수식을 계산합니다. 계산 중에 오류가 발생하면 "Error"라는 메시지를 반환합니다.

_parseExpression() 함수는 입력된 수식을 숫자와 연산자로 나누는 역할을 합니다. 예를 들어, "12+3×4"라는 수식을 입력하면, 이 함수는 숫자와 연산자를 각각의 토큰으로 나누어 리스트로 반환합니다.

_evaluateExpression() 함수는 토큰화된 수식을 실제로 계산하는 역할을 합니다. 여기서는 ×, ÷ 연산을 우선 처리한 , +, - 연산을 처리합니다. 이렇게 연산 우선순위를 고려한 , 최종 결과를 반환하게 됩니다.