티스토리 뷰
Flutter 개발자 계산기 만들기 - Part 4: 진수 변환 (DEC, BIN, HEX) 및 기본 비트 연산
부캐: 개발하는 조대리 2025. 5. 27. 08:24안녕하세요! 지난 파트에서는 Shunting-yard 알고리즘을 사용하여 괄호와 연산자 우선순위를 처리하는 고급 연산 기능을 구현했습니다. 이번 파트에서는 개발자 계산기의 핵심 기능 중 하나인 다양한 진수(Number Base) 간의 변환 기능과 비트(Bitwise) 연산 기능을 추가하여 계산기를 더욱 강력하게 만들겠습니다.
1. 이론: 진수 변환과 비트 연산
1.1. 숫자 시스템 (Number Systems)
- 10진수 (Decimal, DEC): 우리가 일상에서 사용하는 0-9 숫자로 구성된 시스템입니다.
- 2진수 (Binary, BIN): 0과 1로만 구성된 시스템으로, 컴퓨터 내부에서 데이터를 표현하는 기본 방식입니다.
- 16진수 (Hexadecimal, HEX): 0-9와 A-F(10-15를 의미)로 구성된 시스템입니다. 2진수 4자리를 16진수 1자리로 간결하게 표현할 수 있어 메모리 주소나 색상 코드 등에 많이 사용됩니다.
1.2. Dart에서의 진수 변환
Dart의 int 타입은 다른 진수로의 변환을 위한 유용한 메소드를 제공합니다:
- int.toRadixString(int radix): 정수를 주어진 radix (진수)의 문자열 표현으로 변환합니다.
- 예: 10.toRadixString(2) 결과는 "1010" (2진수), 255.toRadixString(16) 결과는 "ff" (16진수).
- int.parse(String source, {int? radix}): 주어진 radix로 표현된 문자열 source를 정수로 변환합니다.
- 예: int.parse("1010", radix: 2) 결과는 10, int.parse("ff", radix: 16) 결과는 255.
1.3. 비트 연산 (Bitwise Operations)
비트 연산은 숫자의 2진수 표현에서 각 비트 단위로 수행되는 연산입니다.
- AND (&): 두 비트가 모두 1일 때만 결과가 1입니다.
- OR (|): 두 비트 중 하나라도 1이면 결과가 1입니다.
- XOR (^): 두 비트가 서로 다를 때만 결과가 1입니다.
- NOT (~): 각 비트를 반전시킵니다 (0은 1로, 1은 0으로). (Dart에서는 ~ 연산자가 2의 보수를 사용하므로, 특정 비트 길이에 대한 NOT을 구현하려면 추가 작업이 필요할 수 있습니다. 이번 파트에서는 AND, OR, XOR에 집중합니다.)
1.4. UI/UX 고려사항
- 모드 전환: 사용자가 DEC, BIN, HEX 모드를 쉽게 전환할 수 있는 버튼이 필요합니다.
- 입력 제한: 현재 선택된 진수 모드에 따라 입력 가능한 숫자/문자가 제한되어야 합니다 (예: BIN 모드에서는 0, 1만 입력 가능).
- 표시 방식: 현재 입력/결과 값이 어떤 진수로 표시되고 있는지 명확히 알려줘야 합니다.
- 비트 연산자 통합: 비트 연산자(&, |, ^)도 기존 수식 계산 로직에 통합되어야 합니다.
2. 코딩: 진수 변환 및 비트 연산 기능 추가
_CalculatorHomePageState 클래스를 수정하여 새로운 상태 변수와 로직을 추가합니다.
Dart
import 'package:flutter/material.dart';
import 'dart:math' as math;
// 이전 파트의 DeveloperCalculatorApp, CalculatorHomePage 클래스 정의는 동일하므로 생략합니다.
// 여기서는 _CalculatorHomePageState의 변경 사항에 집중합니다.
void main() {
runApp(const DeveloperCalculatorApp());
}
class DeveloperCalculatorApp extends StatelessWidget {
const DeveloperCalculatorApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Developer Calculator',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal, // 테마 색상 변경
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const CalculatorHomePage(),
);
}
}
class CalculatorHomePage extends StatefulWidget {
const CalculatorHomePage({super.key});
@override
State<CalculatorHomePage> createState() => _CalculatorHomePageState();
}
// 진수 모드를 위한 Enum
enum RadixMode { DEC, BIN, HEX }
class _CalculatorHomePageState extends State<CalculatorHomePage> {
String _expression = "";
String _output = "0";
bool _evaluated = false;
RadixMode _currentRadixMode = RadixMode.DEC;
int _currentValueForBaseConversion = 0; // 진수 변환을 위한 내부 정수 값
// 연산자 우선순위 (비트 연산자 추가)
final Map<String, int> _precedence = {
'+': 1, '-': 1,
'&': 2, '^': 2, '|': 2, // 비트 연산자 우선순위 (덧셈/뺄셈보다 높게 설정)
'*': 3, '/': 3,
// '%' (모듈로) 등도 추가 가능
};
// 허용되는 문자 (16진수용)
final List<String> _hexChars = ['A', 'B', 'C', 'D', 'E', 'F'];
void _onButtonPressed(String buttonText) {
setState(() {
// 모드 변경 버튼 처리
if (buttonText == "DEC" || buttonText == "BIN" || buttonText == "HEX") {
_changeRadixMode(buttonText);
return;
}
// 이전에 계산이 완료되었고, 새로 입력되는 것이 연산자나 소수점이 아니라면 초기화
if (_evaluated && !_isOperator(buttonText) && buttonText != '.' && !_isRadixModeControl(buttonText)) {
if (buttonText != "(" && buttonText != ")") {
_expression = "";
}
_evaluated = false;
} else if (_evaluated && (_isOperator(buttonText) || buttonText == '.')) {
// 이전 결과값을 _expression의 시작으로 사용 (진수 모드 고려)
// _output이 현재 진수로 표현된 결과값이므로, 이를 기반으로 expression을 재구성해야함
// 또는, _currentValueForBaseConversion 을 사용
if (_currentRadixMode == RadixMode.DEC) {
_expression = _output; // DEC 모드에서는 부동소수점 결과일 수 있음
} else {
// BIN/HEX 모드에서는 _currentValueForBaseConversion을 현재 진수로 변환하여 사용
_expression = _currentValueForBaseConversion.toRadixString(_getRadixValue(_currentRadixMode)).toUpperCase();
}
_evaluated = false;
}
if (buttonText == "C") {
_clearAll();
} else if (buttonText == "=") {
_calculateResult();
} else if (buttonText == "⌫" || buttonText == "Del") {
if (_expression.isNotEmpty) {
_expression = _expression.substring(0, _expression.length - 1);
if (_expression.isEmpty) _output = "0"; else _output = _expression;
}
} else if (_isValidInputForCurrentMode(buttonText)) {
if (_expression.isNotEmpty && _isOperator(_expression[_expression.length - 1]) && _isOperator(buttonText)) {
_expression = _expression.substring(0, _expression.length - 1) + buttonText;
} else {
_expression += buttonText;
}
_output = _expression;
}
});
}
bool _isRadixModeControl(String buttonText) {
return ["DEC", "BIN", "HEX"].contains(buttonText);
}
// 현재 진수 모드에 유효한 입력인지 확인
bool _isValidInputForCurrentMode(String buttonText) {
if (_isOperator(buttonText) || buttonText == '(' || buttonText == ')' || buttonText == '.') {
// . 은 DEC 모드에서만 유효하게 처리 (BIN/HEX는 정수 기반)
if (buttonText == '.' && _currentRadixMode != RadixMode.DEC) return false;
return true;
}
if (RegExp(r'[0-9]').hasMatch(buttonText)) { // 숫자 0-9
if (_currentRadixMode == RadixMode.BIN && (buttonText != '0' && buttonText != '1')) {
return false; // BIN 모드에서는 0, 1만 허용
}
return true;
}
if (_hexChars.contains(buttonText.toUpperCase())) { // A-F
return _currentRadixMode == RadixMode.HEX; // HEX 모드에서만 허용
}
return false; // 그 외의 경우
}
void _changeRadixMode(String mode) {
RadixMode newMode;
switch (mode) {
case "BIN": newMode = RadixMode.BIN; break;
case "HEX": newMode = RadixMode.HEX; break;
case "DEC":
default: newMode = RadixMode.DEC; break;
}
if (_currentRadixMode == newMode) return; // 이미 해당 모드면 변경 안함
// 현재 _output 값을 기준으로 변환 시도
// _output이 숫자 표현이 아니거나 에러면 0으로 초기화
try {
if (_output == "Error" || _output.isEmpty) {
_currentValueForBaseConversion = 0;
} else {
if (_currentRadixMode == RadixMode.DEC) {
// DEC에서 다른 모드로: _output을 double로 파싱 후 int로 변환
_currentValueForBaseConversion = double.parse(_output).toInt();
} else {
// BIN/HEX에서 다른 모드로: _output을 현재 진수의 int로 파싱
_currentValueForBaseConversion = int.parse(_output, radix: _getRadixValue(_currentRadixMode));
}
}
} catch (e) {
_currentValueForBaseConversion = 0; // 파싱 실패 시 0으로
}
_currentRadixMode = newMode;
_output = _currentValueForBaseConversion.toRadixString(_getRadixValue(newMode)).toUpperCase();
_expression = _output; // 모드 변경 시 expression도 현재 값으로 초기화 (UX 고려)
_evaluated = true; // 모드 변경은 일종의 결과 표시로 간주
}
int _getRadixValue(RadixMode mode) {
switch (mode) {
case RadixMode.BIN: return 2;
case RadixMode.HEX: return 16;
case RadixMode.DEC:
default: return 10;
}
}
void _clearAll() {
_expression = "";
_output = "0";
_evaluated = false;
_currentValueForBaseConversion = 0;
// _currentRadixMode = RadixMode.DEC; // 기본값으로 돌릴 수도 있음
}
bool _isOperator(String s) {
return ['+', '-', '*', '/', '&', '|', '^'].contains(s);
}
// 숫자 및 현재 모드에 따른 유효 문자(A-F) 확인
bool _isOperandChar(String char) {
if (RegExp(r'[0-9]').hasMatch(char)) return true;
if (_currentRadixMode == RadixMode.HEX && _hexChars.contains(char.toUpperCase())) return true;
if (_currentRadixMode == RadixMode.DEC && char == '.') return true;
return false;
}
List<String> _tokenizeExpression(String infix) {
List<String> tokens = [];
String currentNumber = "";
for (int i = 0; i < infix.length; i++) {
String char = infix[i];
if (_isOperandChar(char)) {
currentNumber += char;
} else { // 연산자 또는 괄호
if (currentNumber.isNotEmpty) {
tokens.add(currentNumber);
currentNumber = "";
}
tokens.add(char);
}
}
if (currentNumber.isNotEmpty) {
tokens.add(currentNumber);
}
return tokens;
}
List<String> _convertToPostfix(String infix) {
List<String> postfix = [];
List<String> operatorStack = [];
List<String> tokens = _tokenizeExpression(infix);
for (String token in tokens) {
// 현재 진수 모드에 따라 숫자 토큰을 10진수 int 값으로 변환하여 처리 준비
// 단, 부동소수점 DEC는 double로 처리. BIN/HEX 및 비트연산은 int로.
// 이 부분은 evaluatePostfix에서 실제 값을 파싱할 때 진수를 고려하도록 수정.
// 여기서는 토큰 자체를 그대로 사용.
if (!_isOperator(token) && token != '(' && token != ')') { // 피연산자로 간주
postfix.add(token);
} else if (token == '(') {
operatorStack.add(token);
} else if (token == ')') {
while (operatorStack.isNotEmpty && operatorStack.last != '(') {
postfix.add(operatorStack.removeLast());
}
if (operatorStack.isNotEmpty) operatorStack.removeLast(); // '(' 제거
else throw Exception("Mismatched parentheses");
} else if (_isOperator(token)) {
while (operatorStack.isNotEmpty &&
operatorStack.last != '(' &&
(_precedence[operatorStack.last] ?? 0) >= (_precedence[token] ?? 0)) {
postfix.add(operatorStack.removeLast());
}
operatorStack.add(token);
}
}
while (operatorStack.isNotEmpty) {
if (operatorStack.last == '(') throw Exception("Mismatched parentheses");
postfix.add(operatorStack.removeLast());
}
return postfix;
}
// 피연산자 토큰을 현재 진수에 맞게 double 또는 int로 파싱
num _parseOperand(String operandToken) {
if (_currentRadixMode == RadixMode.DEC) {
// DEC 모드에서는 부동소수점 연산도 가능
return double.tryParse(operandToken) ?? 0;
} else {
// BIN/HEX 모드에서는 정수 연산만
return int.tryParse(operandToken, radix: _getRadixValue(_currentRadixMode)) ?? 0;
}
}
double _evaluatePostfix(List<String> postfix) {
List<num> evalStack = []; // double 대신 num 사용 (정수/실수 모두 처리)
for (String token in postfix) {
if (!_isOperator(token)) { // 피연산자
evalStack.add(_parseOperand(token));
} else { // 연산자
if (evalStack.length < 2) throw Exception("Invalid expression");
num val2 = evalStack.removeLast();
num val1 = evalStack.removeLast();
num result;
// 비트 연산자는 정수끼리만 연산
if (['&', '|', '^'].contains(token)) {
if (val1 is! int || val2 is! int) throw Exception("Bitwise ops require integers");
int intVal1 = val1.toInt();
int intVal2 = val2.toInt();
switch (token) {
case '&': result = intVal1 & intVal2; break;
case '|': result = intVal1 | intVal2; break;
case '^': result = intVal1 ^ intVal2; break;
default: throw Exception("Unknown bitwise operator");
}
} else { // 산술 연산자
double doubleVal1 = val1.toDouble();
double doubleVal2 = val2.toDouble();
switch (token) {
case '+': result = doubleVal1 + doubleVal2; break;
case '-': result = doubleVal1 - doubleVal2; break;
case '*': result = doubleVal1 * doubleVal2; break;
case '/':
if (doubleVal2 == 0) throw Exception("Division by zero");
result = doubleVal1 / doubleVal2;
break;
default: throw Exception("Unknown arithmetic operator");
}
}
evalStack.add(result);
}
}
if (evalStack.length == 1) return evalStack.first.toDouble(); // 최종 결과는 double로
throw Exception("Invalid expression - stack not empty");
}
void _calculateResult() {
if (_expression.isEmpty) return;
try {
if (_isOperator(_expression[_expression.length - 1])) {
_expression = _expression.substring(0, _expression.length - 1);
}
List<String> postfixTokens = _convertToPostfix(_expression);
double decResult = _evaluatePostfix(postfixTokens);
_currentValueForBaseConversion = decResult.toInt(); // 다음 진수 변환을 위해 저장
if (_currentRadixMode == RadixMode.DEC) {
if (decResult == decResult.toInt()) {
_output = decResult.toInt().toString();
} else {
String tempResult = decResult.toStringAsFixed(8);
tempResult = tempResult.replaceAll(RegExp(r'0+$'), '');
tempResult = tempResult.replaceAll(RegExp(r'\.$'), '');
_output = tempResult;
}
} else { // BIN, HEX
_output = _currentValueForBaseConversion.toRadixString(_getRadixValue(_currentRadixMode)).toUpperCase();
}
_evaluated = true;
} catch (e) {
_output = "Error"; // e.toString()으로 더 자세한 오류 가능
_expression = "";
_evaluated = true;
}
}
// 버튼 위젯 생성 (진수 모드 버튼, A-F 버튼 추가)
Widget _buildButton(String buttonText, {int flex = 1, bool highlightMode = false}) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
Color fgColor = colorScheme.onSurfaceVariant; // 기본 전경색
Color bgColor = colorScheme.surfaceVariant; // 기본 배경색
// 현재 모드와 일치하는 모드 버튼 강조
if (highlightMode) {
bgColor = colorScheme.primary;
fgColor = colorScheme.onPrimary;
} else if (buttonText == "C" || buttonText == "Del" || buttonText == "⌫") {
bgColor = colorScheme.errorContainer;
fgColor = colorScheme.onErrorContainer;
} else if (_isOperator(buttonText) || buttonText == "(" || buttonText == ")") {
bgColor = colorScheme.secondaryContainer;
fgColor = colorScheme.onSecondaryContainer;
} else if (buttonText == "=") {
bgColor = colorScheme.primaryContainer; // PrimaryContainer로 변경
fgColor = colorScheme.onPrimaryContainer;
} else if (_hexChars.contains(buttonText.toUpperCase()) || RegExp(r'[0-9]').hasMatch(buttonText) || buttonText == '.') {
// 숫자 및 HEX 문자 버튼 (일반 버튼 색상 사용)
bgColor = colorScheme.tertiaryContainer;
fgColor = colorScheme.onTertiaryContainer;
}
// 현재 모드에 따라 버튼 활성화/비활성화 (스타일로 표현)
bool isEnabled = _isValidInputForCurrentMode(buttonText) ||
_isOperator(buttonText) ||
["C", "⌫", "Del", "=", "(", ")"].contains(buttonText) ||
_isRadixModeControl(buttonText); // 모드 버튼은 항상 활성화
if (!isEnabled && !_isRadixModeControl(buttonText)) { // 모드 버튼이 아닌데 비활성화된 경우
fgColor = colorScheme.onSurface.withOpacity(0.38); // 비활성화된 텍스트 색
// bgColor = colorScheme.onSurface.withOpacity(0.12); // 비활성화된 배경 색 (선택사항)
}
return Expanded(
flex: flex,
child: Container(
margin: const EdgeInsets.all(4.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: fgColor,
backgroundColor: bgColor,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
textStyle: TextStyle(fontSize: buttonText.length > 1 ? 14 : 18, fontWeight: FontWeight.w500),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
// 비활성화된 버튼 클릭 효과 없애기 (선택적)
// disabledForegroundColor: fgColor.withOpacity(0.5),
// disabledBackgroundColor: bgColor.withOpacity(0.5),
),
onPressed: isEnabled ? () => _onButtonPressed(buttonText) : null, // 비활성화 시 onPressed null
child: Text(buttonText),
),
),
);
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
// 버튼 레이아웃 정의
// List<List<String>> buttonRows = [ // 이 변수는 현재 코드에서 사용되지 않으므로 주석 처리 또는 삭제 가능
// ["DEC", "BIN", "HEX", "C"],
// ["A", "B", "(", ")", "/"],
// ["C", "D", "7", "8", "9", "*"],
// ["E", "F", "4", "5", "6", "-"],
// ["&", "^", "1", "2", "3", "+"],
// ["|", "⌫", "0", ".", "="],
// ];
return Scaffold(
appBar: AppBar(
title: Text('개발자 계산기 (Mode: ${_currentRadixMode.toString().split('.').last})'),
backgroundColor: colorScheme.inversePrimary,
),
backgroundColor: colorScheme.background,
body: Column(
children: <Widget>[
Expanded(
flex: 2,
child: Container(
alignment: Alignment.bottomRight,
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
child: Text(
_output,
style: TextStyle(
fontSize: _output.length > 12 ? 30 : 36, // 글자 수 따라 폰트 크기 조정
fontWeight: FontWeight.bold,
color: colorScheme.onBackground,
),
textAlign: TextAlign.right,
),
),
),
),
const Divider(height: 1, thickness: 1),
// 모드 버튼 행
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
child: Row(
children: <Widget>[
_buildButton("DEC", highlightMode: _currentRadixMode == RadixMode.DEC),
_buildButton("BIN", highlightMode: _currentRadixMode == RadixMode.BIN),
_buildButton("HEX", highlightMode: _currentRadixMode == RadixMode.HEX),
_buildButton("C"), // Clear는 여기에 두거나 다른 위치로.
],
),
),
// 구분선 추가
const Divider(height: 1, thickness: 1, indent: 4, endIndent: 4),
// 숫자 및 연산자 버튼
Expanded(
flex: _currentRadixMode == RadixMode.HEX ? 4 : 3, // HEX 모드 시 버튼 영역 확장
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
children: [
// HEX 모드일 때만 A-F 버튼 행 추가
if (_currentRadixMode == RadixMode.HEX)
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_buildButton("A"),_buildButton("B"),_buildButton("C"),
_buildButton("D"),_buildButton("E"),_buildButton("F"),
],
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_buildButton("("), _buildButton(")"),
_buildButton("&"), _buildButton("^"), _buildButton("|"), // 비트 연산자
],
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_buildButton("7"), _buildButton("8"), _buildButton("9"), _buildButton("/"),
],
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_buildButton("4"), _buildButton("5"), _buildButton("6"), _buildButton("*"),
],
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_buildButton("1"), _buildButton("2"), _buildButton("3"), _buildButton("-"),
],
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_buildButton("0", flex: 1), // 이전 코드와 동일하게 1로 유지 (0 버튼은 flex 1, .과 ⌫는 1, +와 =도 1)
_buildButton("."),
_buildButton("⌫"),
_buildButton("+"),
_buildButton("=", flex: 1),
],
),
),
],
),
),
),
],
),
);
}
}
2.1. 코드 변경점 설명
- RadixMode Enum: 현재 계산기 진수 모드(DEC, BIN, HEX)를 나타내는 enum을 정의했습니다.
- 상태 변수 추가:
- _currentRadixMode: 현재 진수 모드를 저장합니다 (기본값 RadixMode.DEC).
- _currentValueForBaseConversion: 진수 변환 시 사용될 내부 정수 값을 저장합니다. 일반 산술 계산 결과의 정수 부분을 여기에 저장했다가 모드 변경 시 사용합니다.
- 연산자 우선순위 업데이트: _precedence 맵에 비트 연산자(&, |, ^)와 그 우선순위를 추가했습니다.
- UI 변경:
- DEC, BIN, HEX 모드 전환 버튼을 추가하고, 현재 활성화된 모드 버튼은 강조 표시합니다.
- AppBar에 현재 모드를 표시합니다.
- HEX 모드일 때 A-F 버튼 행이 나타나도록 UI를 동적으로 변경했습니다.
- 버튼 레이아웃을 재구성하여 비트 연산자 등을 포함시켰습니다.
- 입력 유효성 검사 (_isValidInputForCurrentMode): 현재 진수 모드에 따라 입력 가능한 버튼(숫자, A-F)을 제한합니다. 예를 들어 BIN 모드에서는 '0', '1'만, HEX 모드에서는 '0'-'9', 'A'-'F'만 실제 입력으로 처리됩니다. 비활성화된 버튼은 onPressed: null로 설정하여 클릭되지 않도록 하고, 시각적으로도 구분되도록 스타일을 조정했습니다.
- 모드 변경 로직 (_changeRadixMode):
- 모드 변경 시, 현재 _output에 표시된 값을 파싱하여 _currentValueForBaseConversion에 저장합니다. 파싱은 현재 모드를 기준으로 이루어집니다.
- _currentValueForBaseConversion 값을 새로운 진수로 변환하여 _output에 표시합니다.
- 토큰화 로직 수정 (_tokenizeExpression): 16진수(예: "FF", "A0")를 하나의 숫자로 인식하도록 토큰화 로직을 개선해야 합니다. 기존에는 단일 문자 기반이었으나, 이제 연속된 숫자 및 유효한 HEX 문자를 하나의 토큰으로 묶습니다. (_isOperandChar 함수 추가)
- 피연산자 파싱 로직 (_parseOperand): _evaluatePostfix 내부에서 피연산자 토큰을 실제 숫자로 변환할 때, _currentRadixMode를 고려하여 int.parse(token, radix: ...) 또는 double.parse(token)을 사용합니다.
- 계산 로직 수정 (_evaluatePostfix):
- 비트 연산(&, |, ^)을 처리하는 로직을 추가했습니다. 비트 연산은 정수(int) 피연산자에 대해서만 수행됩니다.
- 산술 연산은 이전과 같이 double로 처리될 수 있습니다.
- 스택은 num 타입을 사용하여 정수와 실수를 모두 다룰 수 있도록 했습니다.
- 결과 표시 (_calculateResult): 계산 결과(decResult)를 _currentValueForBaseConversion에 저장하고, 현재 모드에 맞춰 _output을 포맷팅합니다. DEC 모드에서는 소수점까지, BIN/HEX 모드에서는 정수 변환 결과를 해당 진수로 표시합니다.
3. SOLID 원칙: 역할 분담
- SRP (단일 책임 원칙):
- _changeRadixMode: 진수 모드 변경 및 관련 값 변환 책임.
- _isValidInputForCurrentMode: 현재 모드에 따른 입력 유효성 검사 책임.
- _parseOperand: 토큰을 현재 진수에 맞는 숫자 타입으로 변환하는 책임.
- _tokenizeExpression: 수식을 토큰 단위로 분리하는 책임.
- 각 함수가 좀 더 명확한 책임을 갖도록 분리했습니다. 향후 BaseConverter나 ExpressionTokenizer 같은 별도 클래스로 추출하여 더욱 명확한 SRP를 적용할 수 있습니다.
4. 디버깅 팁 💡
- 진수 변환 값 확인: _changeRadixMode 함수 내에서 _currentValueForBaseConversion 값과 변환된 _output 값을 print로 확인하여 진수 변환이 정확한지 검증합니다.
- 토큰화 결과 검증: _tokenizeExpression 함수에서 반환된 tokens 리스트를 print하여 16진수 숫자나 일반 숫자가 올바르게 하나의 토큰으로 묶이는지 확인합니다. (예: "FF+10" -> ["FF", "+", "10"])
- 피연산자 파싱 확인: _parseOperand 함수에서 각 토큰이 현재 진수에 맞게 int 또는 double로 잘 파싱되는지 print로 확인합니다.
- 모드별 버튼 활성화 상태: 각 모드(DEC, BIN, HEX)로 전환했을 때, 해당 모드에서 입력 불가능한 버튼들(예: BIN 모드에서 '2'~'9', 'A'~'F', '.')이 실제로 비활성화(클릭 안됨, 흐리게 표시)되는지 UI를 통해 꼼꼼히 확인합니다.
- 비트 연산 테스트: 간단한 숫자들로 비트 연산(예: 1010 & 1100 in BIN mode)이 예상대로 동작하는지, 특히 정수 간 연산으로 잘 처리되는지 확인합니다.
5. Q&A ❓
Q1: Dart에서 숫자를 다른 진수로 어떻게 변환하나요? (예: 10진수 정수 -> 2진수 문자열) A1: int 타입의 toRadixString(int radix) 메소드를 사용합니다. 예를 들어, 10진수 정수 13을 2진수 문자열로 변환하려면 13.toRadixString(2)를 호출하며, 결과는 "1101"이 됩니다. 16진수로 변환하려면 13.toRadixString(16)이고 결과는 "d"입니다.
Q2: 2진수 또는 16진수 문자열을 다시 10진수 정수로 어떻게 변환하나요? A2: int.parse(String source, {int? radix}) 함수를 사용합니다. 예를 들어, 2진수 문자열 "1101"을 10진수 정수로 변환하려면 int.parse("1101", radix: 2)를 호출하며, 결과는 13이 됩니다. 16진수 문자열 "D"는 int.parse("D", radix: 16)으로 변환합니다.
Q3: 비트 연산은 주로 어떤 상황에서 사용되나요? A3: 비트 연산은 하드웨어 제어, 데이터 압축 및 암호화, 그래픽 처리, 네트워크 프로토콜, 특정 플래그(flag) 관리 등 시스템 프로그래밍이나 저수준(low-level) 데이터 조작이 필요한 다양한 상황에서 사용됩니다. 예를 들어, 특정 권한 설정을 하나의 정수 값에 여러 플래그의 조합으로 저장하고 확인할 때 유용합니다.
Q4: 현재 계산기 모드(DEC, BIN, HEX)에 따라 입력 가능한 버튼을 어떻게 제한하나요? A4: _isValidInputForCurrentMode(String buttonText)라는 함수를 만들어 해결했습니다. 이 함수는 현재 _currentRadixMode 상태를 확인하고, 입력된 buttonText가 해당 모드에서 유효한지 (예: BIN 모드에서는 '0'과 '1'만, HEX 모드에서는 '0'-'9'와 'A'-'F'만 유효) 판단합니다. _buildButton 위젯에서는 이 함수의 결과에 따라 ElevatedButton의 onPressed 콜백을 null로 설정하여 버튼을 비활성화하고, 시각적으로도 흐리게 표시하여 사용자에게 피드백을 줍니다.
이번 파트에서는 진수 변환과 비트 연산 기능을 추가하여 개발자 계산기의 면모를 갖추기 시작했습니다. 다음 파트에서는 사용 편의성을 높이기 위한 추가 기능들(예: 메모리 기능, 상수 입력, 더 많은 수학 함수)과 코드 리팩토링을 통한 구조 개선을 다뤄보겠습니다.
'개발 > Flutter' 카테고리의 다른 글
Flutter 개발자 계산기 만들기 - Part 6: 코드 리팩토링 (로직 분리 및 가독성 향상) (0) | 2025.05.28 |
---|---|
Flutter 개발자 계산기 만들기 - Part 5: 메모리 기능 및 UI/UX 개선 (0) | 2025.05.28 |
Flutter 개발자 계산기 만들기 - Part 3: 고급 연산 기능 (괄호 및 연산 순서 처리) (0) | 2025.05.26 |
Flutter 개발자 계산기 만들기 - Part 2: 기본 사칙연산 로직 구현 (0) | 2025.05.23 |
Flutter 개발자 계산기 만들기 - Part 1: 프로젝트 설정 및 기본 UI 레이아웃 (0) | 2025.05.22 |