├── src ├── vite-env.d.ts ├── setupTests.ts ├── main.tsx ├── App.tsx ├── frameworks │ └── ui │ │ ├── theme.ts │ │ └── components │ │ └── Calculator.tsx ├── entities │ ├── __tests__ │ │ └── Expression.test.ts │ └── Expression │ │ └── Expression.ts ├── index.css ├── assets │ └── react.svg └── App.css ├── .windsurfrules ├── tsconfig.json ├── .prettierrc ├── .gitignore ├── vite.config.ts ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── index.html ├── .github └── workflows │ └── deploy.yml ├── package.json ├── public └── vite.svg ├── README.md └── docs ├── demo-scenario.md ├── requirements.md ├── checklist.md ├── assets └── calculator-wireframe.svg └── design.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | // 여기에 테스트 설정을 추가할 수 있습니다. 4 | -------------------------------------------------------------------------------- /.windsurfrules: -------------------------------------------------------------------------------- 1 | - UI 작업시에는 구현을 다 끝낸 다음 테스트 코드를 만들 것 2 | - UI 이외의 코어 로직은 TDD로 구현할 것 3 | - 작업이 끝나면 docs/checklist.md에 작업 내용을 업데이트 할 것 4 | - 체크리스트 업데이트 후에 커밋을 할 것 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 100, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | coverage -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import Calculator from './frameworks/ui/components/Calculator'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 |

공학용 계산기

9 |

Clean Architecture와 SOLID 원칙을 적용한 계산기 웹앱

10 |
11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { configDefaults } from 'vitest/config' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | base: '/vibecoding-demo/', 9 | test: { 10 | globals: true, 11 | environment: 'jsdom', 12 | setupFiles: ['./src/setupTests.ts'], 13 | exclude: [...configDefaults.exclude, 'e2e/*'], 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 공학용 계산기 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow only one concurrent deployment 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | # Build job 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: '22' 31 | 32 | - name: Install dependencies 33 | run: npm install 34 | 35 | - name: Build 36 | run: npm run build 37 | 38 | - name: Setup Pages 39 | uses: actions/configure-pages@v4 40 | 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: './dist' 45 | 46 | # Deployment job 47 | deploy: 48 | permissions: 49 | contents: read 50 | pages: write 51 | id-token: write 52 | runs-on: ubuntu-latest 53 | needs: build 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "test:coverage": "vitest run --coverage", 13 | "format": "prettier --write \"src/**/*.{ts,tsx}\"" 14 | }, 15 | "dependencies": { 16 | "@chakra-ui/react": "^3.16.1", 17 | "@emotion/react": "^11.14.0", 18 | "@emotion/styled": "^11.14.0", 19 | "framer-motion": "^12.7.4", 20 | "mathjs": "^14.4.0", 21 | "react": "^19.1.0", 22 | "react-dom": "^19.1.0" 23 | }, 24 | "devDependencies": { 25 | "@eslint/js": "^9.25.1", 26 | "@testing-library/jest-dom": "^6.6.3", 27 | "@testing-library/react": "^16.3.0", 28 | "@types/react": "^19.1.2", 29 | "@types/react-dom": "^19.1.2", 30 | "@vitejs/plugin-react": "^4.4.1", 31 | "@vitest/coverage-v8": "^3.1.2", 32 | "eslint": "^9.25.1", 33 | "eslint-config-prettier": "^10.1.2", 34 | "eslint-plugin-react-hooks": "^5.2.0", 35 | "eslint-plugin-react-refresh": "^0.4.20", 36 | "globals": "^16.0.0", 37 | "jsdom": "^26.1.0", 38 | "prettier": "^3.5.3", 39 | "typescript": "~5.8.3", 40 | "typescript-eslint": "^8.31.0", 41 | "vite": "^6.3.2", 42 | "vitest": "^3.1.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 공학용 계산기 웹앱 2 | 3 | [![Deploy](https://github.com/roboco-io/vibecoding-demo/actions/workflows/deploy.yml/badge.svg)](https://github.com/roboco-io/vibecoding-demo/actions/workflows/deploy.yml) 4 | 5 | 이 프로젝트는 티타임즈TV의 **[AX에 코리아 운명이 달렸다](https://www.youtube.com/watch?v=tTeCnBi6GPU&list=PL7d4-rFjtYdJ92huzIuksMynSBhA_FfMH)** 시리즈의 인터뷰에서 라이브 코딩으로 만든 공학용 계산기 웹 애플리케이션입니다. 6 | 7 | [계산기 앱](https://roboco.io/vibecoding-demo/) 8 | 9 | ## 문서 10 | 11 | - [프롬프트 시나리오](docs/demo-scenario.md) 12 | - [설계 문서](docs/design.md) 13 | - [요구사항 문서](docs/requirements.md) 14 | - [개발 체크리스트](docs/checklist.md) 15 | 16 | ## 유튜브 영상 17 | - [1편 아! 바이브코딩이 이런 거였구나!!](https://www.youtube.com/watch?v=tTeCnBi6GPU) 18 | - [2편 바이브코딩 잘 하는 법 보여드립니다](https://www.youtube.com/watch?v=Ak2SiHYekdA) 19 | 20 | ## 기능 21 | 22 | - 기본 수학 연산 (덧셈, 뺄셈, 곱셈, 나눗셈, 제곱, 제곱근) 23 | - 공학용 함수 (삼각함수, 로그함수, 지수함수) 24 | - 괄호를 사용한 복잡한 수식 계산 25 | - 계산 기록 저장 및 조회 26 | - 일반 계산기와 공학용 계산기 모드 전환 27 | - 다크/라이트 모드 지원 28 | - 반응형 디자인 29 | 30 | ## 기술 스택 31 | 32 | - **프론트엔드**: React 19.1 + TypeScript 33 | - **UI 라이브러리**: Chakra UI 34 | - **상태 관리**: React Context API + useReducer 35 | - **수학 계산 엔진**: Math.js 36 | - **빌드 시스템**: Vite 37 | - **테스트**: Vitest + React Testing Library 38 | 39 | ## 프로젝트 구조 40 | 41 | 프로젝트는 Clean Architecture 원칙에 따라 구성되어 있습니다: 42 | 43 | ``` 44 | src/ 45 | ├── entities/ # 핵심 비즈니스 로직과 데이터 모델 46 | ├── usecases/ # 애플리케이션 특화 비즈니스 규칙 47 | ├── adapters/ # 외부 라이브러리와의 인터페이스 48 | └── frameworks/ # UI 컴포넌트, 상태 관리 등 49 | ├── ui/ # UI 컴포넌트 50 | └── state/ # 상태 관리 51 | ``` 52 | 53 | ## 개발 환경 설정 54 | 55 | ### 필수 조건 56 | 57 | - Node.js 18.0.0 이상 58 | - npm 9.0.0 이상 59 | 60 | ### 설치 61 | 62 | ```bash 63 | # 의존성 설치 64 | npm install 65 | 66 | # 개발 서버 실행 67 | npm run dev 68 | 69 | # 테스트 실행 70 | npm test 71 | 72 | # 프로덕션 빌드 73 | npm run build 74 | ``` 75 | 76 | ## 테스트 77 | 78 | 이 프로젝트는 TDD(Test-Driven Development) 방식으로 개발되었습니다: 79 | 80 | - 코어 로직(엔티티, 유스케이스, 어댑터)은 TDD로 구현 81 | - UI 컴포넌트는 구현 후 테스트 작성 82 | 83 | ```bash 84 | # 모든 테스트 실행 85 | npm test 86 | 87 | # 테스트 커버리지 확인 88 | npm run test:coverage 89 | ``` 90 | 91 | ## 라이선스 92 | 93 | MIT 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/frameworks/ui/theme.ts: -------------------------------------------------------------------------------- 1 | import { createSystem, defaultConfig } from '@chakra-ui/react'; 2 | 3 | // 테마 확장 4 | export const system = createSystem(defaultConfig, { 5 | theme: { 6 | tokens: { 7 | colors: { 8 | brand: { 9 | 50: { value: '#e3f2fd' }, 10 | 100: { value: '#bbdefb' }, 11 | 200: { value: '#90caf9' }, 12 | 300: { value: '#64b5f6' }, 13 | 400: { value: '#42a5f5' }, 14 | 500: { value: '#2196f3' }, // 주 색상 15 | 600: { value: '#1e88e5' }, 16 | 700: { value: '#1976d2' }, 17 | 800: { value: '#1565c0' }, 18 | 900: { value: '#0d47a1' }, 19 | }, 20 | success: { 21 | 500: { value: '#4caf50' }, 22 | }, 23 | warning: { 24 | 500: { value: '#ff9800' }, 25 | }, 26 | }, 27 | }, 28 | config: { 29 | initialColorMode: 'light', 30 | useSystemColorMode: true, 31 | }, 32 | components: { 33 | Button: { 34 | baseStyle: { 35 | borderRadius: 'md', 36 | fontWeight: 'normal', 37 | }, 38 | variants: { 39 | solid: { 40 | bg: 'brand.500', 41 | color: 'white', 42 | _hover: { 43 | bg: 'brand.600', 44 | }, 45 | _dark: { 46 | bg: 'brand.600', 47 | _hover: { 48 | bg: 'brand.700', 49 | }, 50 | }, 51 | }, 52 | outline: { 53 | border: '1px solid', 54 | borderColor: 'brand.500', 55 | color: 'brand.500', 56 | _dark: { 57 | borderColor: 'brand.600', 58 | color: 'brand.600', 59 | }, 60 | }, 61 | ghost: { 62 | color: 'brand.500', 63 | _dark: { 64 | color: 'brand.600', 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | styles: { 71 | global: { 72 | body: { 73 | bg: 'white', 74 | color: 'gray.800', 75 | _dark: { 76 | bg: 'gray.800', 77 | color: 'white', 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }); 84 | 85 | export default system; 86 | -------------------------------------------------------------------------------- /src/entities/__tests__/Expression.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Expression } from '../Expression/Expression'; 3 | 4 | describe('Expression', () => { 5 | it('should create an expression with a valid string', () => { 6 | const expr = new Expression('2+2'); 7 | expect(expr.getValue()).toBe('2+2'); 8 | }); 9 | 10 | it('should throw an error when created with an empty string', () => { 11 | expect(() => new Expression('')).toThrow('Expression cannot be empty'); 12 | }); 13 | 14 | it('should validate basic arithmetic expressions', () => { 15 | expect(new Expression('2+2').isValid()).toBe(true); 16 | expect(new Expression('2*3').isValid()).toBe(true); 17 | expect(new Expression('10-5').isValid()).toBe(true); 18 | expect(new Expression('8/4').isValid()).toBe(true); 19 | }); 20 | 21 | it('should validate expressions with parentheses', () => { 22 | expect(new Expression('(2+2)*3').isValid()).toBe(true); 23 | expect(new Expression('2*(3+4)').isValid()).toBe(true); 24 | expect(new Expression('(2+3)*(4-1)').isValid()).toBe(true); 25 | }); 26 | 27 | it('should invalidate expressions with unbalanced parentheses', () => { 28 | expect(new Expression('(2+2').isValid()).toBe(false); 29 | expect(new Expression('2+2)').isValid()).toBe(false); 30 | expect(new Expression('(2+2))').isValid()).toBe(false); 31 | }); 32 | 33 | it('should validate expressions with scientific functions', () => { 34 | expect(new Expression('sin(30)').isValid()).toBe(true); 35 | expect(new Expression('cos(45)').isValid()).toBe(true); 36 | expect(new Expression('tan(60)').isValid()).toBe(true); 37 | expect(new Expression('log(100)').isValid()).toBe(true); 38 | expect(new Expression('sqrt(16)').isValid()).toBe(true); 39 | }); 40 | 41 | it('should validate complex expressions', () => { 42 | expect(new Expression('2+3*4').isValid()).toBe(true); 43 | expect(new Expression('sin(30)+cos(60)').isValid()).toBe(true); 44 | expect(new Expression('(2+3)*(sin(45)+1)').isValid()).toBe(true); 45 | expect(new Expression('sqrt(16)+log(100)/2').isValid()).toBe(true); 46 | }); 47 | 48 | it('should invalidate expressions with invalid characters', () => { 49 | expect(new Expression('2+@').isValid()).toBe(false); 50 | expect(new Expression('hello').isValid()).toBe(false); 51 | expect(new Expression('2++2').isValid()).toBe(false); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/entities/Expression/Expression.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Expression 엔티티 3 | * 4 | * 수학적 표현식을 나타내는 엔티티 클래스입니다. 5 | * 표현식의 유효성을 검증하고 값을 관리합니다. 6 | */ 7 | export class Expression { 8 | private readonly value: string; 9 | 10 | /** 11 | * 표현식 객체를 생성합니다. 12 | * @param expression 수학적 표현식 문자열 13 | * @throws 빈 문자열이 전달될 경우 에러를 발생시킵니다. 14 | */ 15 | constructor(expression: string) { 16 | if (!expression || expression.trim() === '') { 17 | throw new Error('Expression cannot be empty'); 18 | } 19 | this.value = expression.trim(); 20 | } 21 | 22 | /** 23 | * 표현식의 값을 반환합니다. 24 | * @returns 표현식 문자열 25 | */ 26 | getValue(): string { 27 | return this.value; 28 | } 29 | 30 | /** 31 | * 표현식의 유효성을 검사합니다. 32 | * @returns 유효한 표현식이면 true, 그렇지 않으면 false 33 | */ 34 | isValid(): boolean { 35 | // 괄호 균형 검사 36 | if (!this.hasBalancedParentheses()) { 37 | return false; 38 | } 39 | 40 | // 함수 호출이 포함된 경우 41 | if (this.value.match(/(sin|cos|tan|log|sqrt)\(/)) { 42 | // 함수 호출 패턴 검사 43 | const functionMatches = this.value.match(/(sin|cos|tan|log|sqrt)\([^)]*\)/g); 44 | if (!functionMatches) { 45 | return false; 46 | } 47 | 48 | // 함수 호출을 제외한 나머지 부분이 유효한지 검사 49 | let remainingExpression = this.value; 50 | for (const match of functionMatches) { 51 | remainingExpression = remainingExpression.replace(match, '0'); // 함수 호출을 숫자로 대체 52 | } 53 | 54 | // 남은 표현식에 유효하지 않은 문자가 있는지 검사 55 | if (!/^[\d+\-*/().^,\s]+$/.test(remainingExpression)) { 56 | return false; 57 | } 58 | } else { 59 | // 함수 호출이 없는 경우, 전체 표현식이 유효한지 검사 60 | if (!/^[\d+\-*/().^,\s]+$/.test(this.value)) { 61 | return false; 62 | } 63 | } 64 | 65 | // 연속된 연산자 검사 (++, --, +*, 등) 66 | if (/[+\-*/]{2,}/.test(this.value)) { 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | /** 74 | * 괄호의 균형이 맞는지 검사합니다. 75 | * @returns 괄호 균형이 맞으면 true, 그렇지 않으면 false 76 | */ 77 | private hasBalancedParentheses(): boolean { 78 | const stack: string[] = []; 79 | 80 | for (const char of this.value) { 81 | if (char === '(') { 82 | stack.push(char); 83 | } else if (char === ')') { 84 | if (stack.length === 0) { 85 | return false; 86 | } 87 | stack.pop(); 88 | } 89 | } 90 | 91 | return stack.length === 0; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/demo-scenario.md: -------------------------------------------------------------------------------- 1 | # 프롬프트를 중심으로한 공학용 계산기 웹앱 개발 데모 시나리오 2 | 3 | 이 문서는 프로젝트의 커밋 이력과 문서를 분석하여 예상되는 프롬프트를 시간 순서대로 정리한 것입니다. 4 | 5 | ## 1. 초기 요구사항 정의 및 설계 (2025-04-22) 6 | 7 | > **프롬프트**: "공학용 계산기 웹앱을 개발하려고 합니다. React와 TypeScript를 사용하고, Clean Architecture와 SOLID 원칙을 적용하려고 합니다. 어떤 기술 스택과 아키텍처를 추천하시겠습니까?" 8 | 9 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 10 | - 요구사항 분석 문서 (`requirements.md`) 11 | - 설계 문서 (`design.md`) 12 | - 기술 스택 선정 (React 19.1, TypeScript, Chakra UI, Math.js, Vite 등) 13 | - Clean Architecture 기반 프로젝트 구조 설계 14 | 15 | ## 2. 프로젝트 초기 설정 (2025-04-22) 16 | 17 | > **프롬프트**: "선택한 기술 스택을 바탕으로 프로젝트 초기 설정을 도와주세요. Clean Architecture에 맞는 디렉토리 구조를 만들고 필요한 패키지를 설치해주세요." 18 | 19 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 20 | - 프로젝트 기본 구조 생성 21 | - 필요한 패키지 설치 및 설정 22 | - 기본 설정 파일 생성 (tsconfig.json, .gitignore, eslint.config.js 등) 23 | - README.md 작성 24 | 25 | ## 3. 기본 UI 컴포넌트 구현 (2025-04-22) 26 | 27 | > **프롬프트**: "TDD 방식으로 Expression 엔티티와 기본 계산기 UI 컴포넌트를 구현해주세요. 계산기는 기본적인 수학 연산을 지원해야 합니다." 28 | 29 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 30 | - Expression 엔티티 및 테스트 구현 31 | - 기본 UI 컴포넌트 구현 (Calculator, Display, Keypad, Button 등) 32 | - 계산기 기본 기능 구현 33 | 34 | ## 4. 계산기 UI 레이아웃 수정 (2025-04-22) 35 | 36 | > **프롬프트**: "와이어프레임에 맞게 계산기 UI 레이아웃을 수정해주세요. 디자인은 깔끔하고 모던하게 만들어주세요." 37 | 38 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 39 | - 계산기 UI 레이아웃 개선 40 | - 스타일링 및 디자인 적용 41 | - 반응형 디자인 구현 42 | 43 | ## 5. GitHub Pages 배포 설정 (2025-04-22) 44 | 45 | > **프롬프트**: "GitHub Pages를 사용하여 웹앱을 배포하고 싶습니다. Vite 설정과 GitHub Actions 워크플로우를 설정해주세요." 46 | 47 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 48 | - Vite 설정 수정 (base 경로 설정) 49 | - GitHub Actions 워크플로우 설정 파일 생성 50 | - GitHub Pages 배포 설정 51 | 52 | ## 6. GitHub Actions 워크플로우 개선 (2025-04-22) 53 | 54 | > **프롬프트**: "GitHub Actions 워크플로우에서 yarn이 아닌 npm을 사용하도록 수정해주세요. 그리고 deploy 작업의 환경 설정 오류를 해결해주세요." 55 | 56 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 57 | - GitHub Actions 워크플로우 수정 58 | - 배포 설정 오류 해결 59 | 60 | ## 7. 문서 업데이트 (2025-04-22) 61 | 62 | > **프롬프트**: "프로젝트 문서를 업데이트해주세요. README.md에 배지를 추가하고, 데모 시나리오를 작성하고, 체크리스트를 갱신해주세요." 63 | 64 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 65 | - README.md 업데이트 (배지 추가) 66 | - 데모 시나리오 문서 작성 67 | - 체크리스트 갱신 68 | 69 | ## 8. 배포된 웹 앱 링크 추가 (2025-04-22) 70 | 71 | > **프롬프트**: "README.md에 배포된 웹 앱 링크를 추가해주세요." 72 | 73 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 74 | - README.md에 배포된 웹 앱 링크 추가 75 | 76 | ## 9. 문서 업데이트: 티타임즈TV 유튜브 영상 링크 (2025-05-31) 77 | 78 | > **프롬프트**: "README.md에 티타임즈TV 유튜브 영상 링크와 프로젝트 설명을 업데이트해주세요." 79 | 80 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 81 | - README.md에 티타임즈TV 유튜브 영상 링크 추가 82 | - 프로젝트 설명 업데이트 83 | 84 | ## 10. 의존성 업데이트 (2025-05-31) 85 | 86 | > **프롬프트**: "프로젝트의 의존성을 최신 버전으로 업데이트해주세요." 87 | 88 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 89 | - package.json 및 package-lock.json 업데이트 90 | - 의존성 버전 업그레이드 91 | 92 | ## 11. .gitignore에 coverage 추가 (2025-05-31) 93 | 94 | > **프롬프트**: ".gitignore 파일에 coverage 디렉토리를 추가해주세요." 95 | 96 | 이 프롬프트를 통해 다음과 같은 결과물이 생성되었을 것으로 예상됩니다: 97 | - .gitignore 파일에 coverage 디렉토리 추가 98 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | /* 라이트 모드 변수 */ 7 | --background-color: #f8f9fa; 8 | --text-color: #333; 9 | --header-color: #2196f3; 10 | --calculator-bg: #f5f5f5; 11 | --calculator-shadow: rgba(0, 0, 0, 0.1); 12 | --toggle-bg: #333; 13 | --toggle-border: #ddd; 14 | --mode-toggle-bg: #e0e0e0; 15 | --mode-active-bg: #fff; 16 | --mode-active-color: #333; 17 | --display-bg: #fff; 18 | --display-shadow: rgba(0, 0, 0, 0.1); 19 | --expression-color: #757575; 20 | --result-color: #333; 21 | --key-bg: #fff; 22 | --key-hover-bg: #f0f0f0; 23 | --operator-bg: #2196f3; 24 | --operator-color: #fff; 25 | --equals-bg: #4caf50; 26 | --equals-color: #fff; 27 | --clear-bg: #ff9800; 28 | --clear-color: #fff; 29 | --function-bg: #e3f2fd; 30 | --function-color: #333; 31 | --functions-area-bg: #e3f2fd; 32 | --history-bg: #fff; 33 | --history-border: #e0e0e0; 34 | --history-expression: #757575; 35 | --history-result: #333; 36 | 37 | color-scheme: light dark; 38 | color: var(--text-color); 39 | background-color: var(--background-color); 40 | 41 | font-synthesis: none; 42 | text-rendering: optimizeLegibility; 43 | -webkit-font-smoothing: antialiased; 44 | -moz-osx-font-smoothing: grayscale; 45 | } 46 | 47 | /* 다크 모드 클래스 */ 48 | html.dark-mode { 49 | --background-color: #121212; 50 | --text-color: #e0e0e0; 51 | --header-color: #64b5f6; 52 | --calculator-bg: #1e1e1e; 53 | --calculator-shadow: rgba(0, 0, 0, 0.3); 54 | --toggle-bg: #e0e0e0; 55 | --toggle-border: #333; 56 | --mode-toggle-bg: #333; 57 | --mode-active-bg: #2d2d2d; 58 | --mode-active-color: #e0e0e0; 59 | --display-bg: #2d2d2d; 60 | --display-shadow: rgba(0, 0, 0, 0.3); 61 | --expression-color: #aaa; 62 | --result-color: #e0e0e0; 63 | --key-bg: #2d2d2d; 64 | --key-hover-bg: #3d3d3d; 65 | --operator-bg: #1976d2; 66 | --operator-color: #fff; 67 | --equals-bg: #388e3c; 68 | --equals-color: #fff; 69 | --clear-bg: #f57c00; 70 | --clear-color: #fff; 71 | --function-bg: #0d47a1; 72 | --function-color: #e0e0e0; 73 | --functions-area-bg: #0d47a1; 74 | --history-bg: #2d2d2d; 75 | --history-border: #444; 76 | --history-expression: #aaa; 77 | --history-result: #e0e0e0; 78 | } 79 | 80 | body { 81 | margin: 0; 82 | display: flex; 83 | place-items: center; 84 | min-width: 320px; 85 | min-height: 100vh; 86 | background-color: var(--background-color); 87 | color: var(--text-color); 88 | transition: background-color 0.3s ease, color 0.3s ease; 89 | } 90 | 91 | a { 92 | font-weight: 500; 93 | color: var(--header-color); 94 | text-decoration: inherit; 95 | } 96 | a:hover { 97 | color: var(--equals-bg); 98 | } 99 | 100 | h1 { 101 | font-size: 3.2em; 102 | line-height: 1.1; 103 | } 104 | 105 | button { 106 | border-radius: 8px; 107 | border: 1px solid transparent; 108 | padding: 0.6em 1.2em; 109 | font-size: 1em; 110 | font-weight: 500; 111 | font-family: inherit; 112 | background-color: var(--key-bg); 113 | cursor: pointer; 114 | transition: border-color 0.25s; 115 | } 116 | button:hover { 117 | border-color: var(--header-color); 118 | } 119 | button:focus, 120 | button:focus-visible { 121 | outline: 4px auto -webkit-focus-ring-color; 122 | } 123 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.md: -------------------------------------------------------------------------------- 1 | # 공학용 계산기 웹앱 요건 정의 2 | 3 | ## 요건 정의를 위한 질문 목록 4 | 5 | ### 1. 기본 기능 요구사항 6 | 7 | 1. **기본 연산 기능** 8 | - 어떤 기본 수학 연산이 필요한가요? 9 | - 덧셈, 뺄셈, 곱셈, 나눗셈, 제곱, 제곱근 등 10 | - 괄호를 사용한 복잡한 수식 계산이 필요한가요? 11 | - 예: (3 + 5) * 2 - 4 / 2 12 | - 계산 기록(히스토리)을 저장하고 볼 수 있는 기능이 필요한가요? 13 | - 예: 3 + 5 = 8, 8 * 2 = 16, 16 - 4 / 2 = 15 14 | 15 | 2. **공학용 특수 기능** 16 | - 어떤 공학용 함수가 필요한가요? 17 | - 삼각함수, 로그함수, 지수함수 18 | - 단위 변환 기능이 필요한가요? 19 | - 필요 없음 20 | - 통계 계산 기능이 필요한가요? 21 | - 필요 없음 22 | - 행렬 계산 기능이 필요한가요? 23 | - 필요 없음 24 | - 미분/적분 계산 기능이 필요한가요? 25 | - 필요 없음 26 | - 방정식 풀이 기능이 필요한가요? 27 | - 필요 없음 28 | 29 | 3. **그래프 기능** 30 | - 함수 그래프를 그리는 기능이 필요한가요? 31 | - 필요 없음 32 | - 어떤 종류의 그래프가 필요한가요? (2D, 3D, 극좌표 등) 33 | - 필요 없음 34 | - 그래프의 확대/축소, 이동 등의 상호작용이 필요한가요? 35 | - 필요 없음 36 | 37 | ### 2. 사용자 인터페이스 요구사항 38 | 39 | 1. **레이아웃 및 디자인** 40 | - 계산기의 레이아웃은 어떤 형태가 좋을까요? 41 | - 공학용과 일반용을 스위칭 가능하게 42 | - 다크 모드/라이트 모드 지원이 필요한가요? 43 | - 필요함 44 | - 반응형 디자인이 필요한가요? 45 | - 필요함 46 | 47 | 2. **사용자 경험** 48 | - 키보드 입력 지원이 필요한가요? 49 | - 계산 결과의 복사/붙여넣기 기능이 필요한가요? 50 | - 사용자 설정 저장 기능이 필요한가요? (소수점 자릿수, 각도 단위 등) 51 | 52 | ### 3. 기술적 요구사항 53 | 54 | 1. **웹 기술** 55 | - 어떤 프론트엔드 프레임워크를 사용할 예정인가요? 56 | - React, Vue, Angular 등 57 | - 서버 사이드 기능이 필요한가요? 필요하다면 어떤 기능인가요? 58 | - 오프라인 사용이 가능해야 하나요? 59 | 60 | 2. **성능 및 호환성** 61 | - 지원해야 할 최소 브라우저 버전이 있나요? 62 | - 계산 정밀도에 대한 요구사항이 있나요? 63 | - 대용량 계산 처리 시 성능에 대한 요구사항이 있나요? 64 | 65 | ### 4. 보안 및 데이터 관리 66 | 67 | 1. **데이터 저장** 68 | - 사용자 계산 데이터를 저장해야 하나요? 69 | - 저장한다면 어디에 저장할 예정인가요? (로컬 스토리지, 서버 등) 70 | - 데이터 백업/복원 기능이 필요한가요? 71 | 72 | 2. **보안** 73 | - 사용자 인증이 필요한가요? 74 | - 민감한 계산 데이터에 대한 보안 요구사항이 있나요? 75 | 76 | ### 5. 배포 및 유지보수 77 | 78 | 1. **배포** 79 | - 어떤 방식으로 배포할 예정인가요? (정적 웹 호스팅, 서버 호스팅 등) 80 | - 도메인 및 SSL 인증서가 필요한가요? 81 | 82 | 2. **유지보수** 83 | - 향후 기능 추가 계획이 있나요? 84 | - 버전 관리 및 업데이트 방식은 어떻게 할 예정인가요? 85 | 86 | ### 6. 기타 요구사항 87 | 88 | 1. **접근성** 89 | - 웹 접근성 표준을 준수해야 하나요? 90 | - 특정 장애를 가진 사용자를 위한 기능이 필요한가요? 91 | 92 | 2. **국제화** 93 | - 다국어 지원이 필요한가요? 94 | - 지역에 따른 숫자 형식 지원이 필요한가요? (천 단위 구분자, 소수점 기호 등) 95 | 96 | 3. **법적 요구사항** 97 | - 특정 라이선스 요구사항이 있나요? 98 | - 개인정보 보호 관련 요구사항이 있나요? 99 | 100 | ### 7. 제안된 기술 스택 (데모 구현용) 101 | 102 | 1. **프론트엔드 기술 스택** 103 | - **프레임워크**: React.js 104 | - 이유: 광범위한 커뮤니티 지원, 풍부한 라이브러리 생태계, 컴포넌트 기반 아키텍처 105 | - **UI 라이브러리**: Material-UI 또는 Chakra UI 106 | - 이유: 미리 디자인된 컴포넌트로 빠른 개발 가능, 반응형 디자인 지원, 다크/라이트 모드 기본 지원 107 | - **상태 관리**: React Context API 또는 Redux Toolkit 108 | - 이유: 계산기 상태 및 히스토리 관리에 적합 109 | - **수학 계산 라이브러리**: Math.js 110 | - 이유: 복잡한 수학 표현식 파싱 및 계산, 공학용 함수 지원 111 | - **빌드 도구**: Vite 112 | - 이유: 빠른 개발 서버, 간편한 설정, 빠른 빌드 시간 113 | 114 | 2. **개발 환경** 115 | - **패키지 관리자**: npm 또는 yarn 116 | - **코드 품질 도구**: ESLint, Prettier 117 | - **테스트 도구**: Jest, React Testing Library 118 | - **버전 관리**: Git 119 | 120 | 3. **배포 옵션** 121 | - **정적 호스팅**: Netlify, Vercel, GitHub Pages 122 | - 이유: 무료 호스팅, 쉬운 배포 프로세스, CI/CD 통합 123 | 124 | 이 기술 스택은 다음과 같은 이점을 제공합니다: 125 | - **빠른 개발**: 풍부한 에코시스템과 도구로 신속한 개발 가능 126 | - **확장성**: 필요에 따라 기능 추가 용이 127 | - **유지보수성**: 모듈화된 구조로 코드 관리 용이 128 | - **성능**: 최신 웹 기술로 최적화된 성능 129 | - **접근성**: UI 라이브러리의 접근성 지원 활용 가능 130 | 131 | ### 8. 최종 선택된 기술 스택 132 | 133 | 공학용 계산기 웹앱 개발을 위한 최적의 기술 스택은 다음과 같습니다: 134 | 135 | 1. **프론트엔드** 136 | - **핵심 프레임워크**: React 19.1 (2025년 3월 출시된 최신 버전) 137 | - TypeScript를 사용하여 타입 안정성 확보 138 | - **UI 라이브러리**: Chakra UI 139 | - 이유: 접근성, 다크 모드, 반응형 디자인이 기본 지원되며 커스터마이징이 용이함 140 | - **상태 관리**: React Context API + useReducer 141 | - 이유: Redux보다 설정이 간단하고, 계산기 앱 규모에 적합함 142 | - **수학 계산 엔진**: Math.js 143 | - 이유: 수식 파싱, 계산, 공학용 함수(삼각함수, 로그 등) 지원이 우수함 144 | - **빌드 시스템**: Vite 145 | - 이유: 빠른 개발 서버와 빌드 속도, 간편한 설정 146 | 147 | 2. **프로젝트 구조 (Clean Architecture)** 148 | - **entities**: 핵심 비즈니스 로직과 데이터 모델 (계산 결과, 히스토리 등) 149 | - **usecases**: 애플리케이션 특화 비즈니스 규칙 (계산 수행, 히스토리 관리 등) 150 | - **adapters**: 외부 라이브러리와의 인터페이스 (Math.js 어댑터 등) 151 | - **frameworks**: UI 컴포넌트, 상태 관리 등 152 | 153 | 3. **개발 환경** 154 | - **패키지 관리**: yarn 155 | - **코드 품질**: ESLint + Prettier 156 | - **테스트 프레임워크**: Vitest + React Testing Library 157 | - 이유: Vite와 통합이 잘 되어 있고 빠른 테스트 실행 속도 158 | - **컴포넌트 문서화**: Storybook 159 | - 이유: UI 컴포넌트 개발 및 테스트에 유용 160 | 161 | 4. **배포** 162 | - **호스팅 플랫폼**: Vercel 163 | - 이유: React 앱에 최적화된 배포 환경, 자동 미리보기 기능 164 | - **CI/CD**: GitHub Actions 165 | - 이유: 코드 품질 검사, 테스트 자동화, 배포 자동화 166 | 167 | 이 기술 스택은 SOLID 원칙과 Clean Architecture를 적용하기에 적합하며, 특히: 168 | - **단일 책임 원칙(SRP)**: 각 컴포넌트와 함수가 하나의 책임만 가짐 169 | - **개방-폐쇄 원칙(OCP)**: 새로운 기능 추가가 용이하도록 설계 170 | - **의존성 역전 원칙(DIP)**: 고수준 모듈이 저수준 모듈에 의존하지 않도록 인터페이스 활용 171 | 172 | 이 기술 스택으로 빠르게 개발을 시작하고, 유지보수가 용이한 고품질 웹앱을 구현할 수 있습니다. 173 | 174 | ## 다음 단계 175 | 176 | 위 질문들에 대한 답변을 바탕으로 상세한 요구사항 명세서를 작성하고, 이를 기반으로 웹앱 개발을 진행할 예정입니다. 177 | -------------------------------------------------------------------------------- /docs/checklist.md: -------------------------------------------------------------------------------- 1 | # 공학용 계산기 웹앱 개발 체크리스트 2 | 3 | ## 1. 프로젝트 초기 설정 4 | 5 | - [x] 1.1. 프로젝트 디렉토리 구조 생성 6 | - Clean Architecture에 맞는 폴더 구조 설정 (entities, usecases, adapters, frameworks) 7 | - 커밋: "초기 프로젝트 구조 설정" 8 | 9 | - [x] 1.2. 개발 환경 설정 10 | - [x] Vite + React + TypeScript 프로젝트 생성 11 | - [x] ESLint, Prettier 설정 12 | - [x] Vitest 설정 13 | - [x] Chakra UI 설치 및 설정 14 | - [x] Math.js 설치 15 | - 커밋: "개발 환경 설정 완료" 16 | 17 | - [x] 1.3. 기본 프로젝트 구성 파일 작성 18 | - [x] README.md 작성 19 | - [x] .gitignore 설정 20 | - [x] tsconfig.json 설정 21 | - 커밋: "기본 프로젝트 구성 파일 작성 완료" 22 | 23 | ## 2. 엔티티 계층 구현 24 | 25 | - [x] 2.1. 핵심 엔티티 구현 26 | - [x] Expression 인터페이스 및 구현체 작성 27 | - [ ] CalculationResult 인터페이스 및 구현체 작성 28 | - [ ] CalculationHistory 인터페이스 및 구현체 작성 29 | - 커밋: "핵심 엔티티 구현" 30 | 31 | - [x] 2.2. 엔티티 단위 테스트 작성 32 | - [x] Expression 테스트 33 | - [ ] CalculationResult 테스트 34 | - [ ] CalculationHistory 테스트 35 | - 커밋: "엔티티 단위 테스트 작성 완료" 36 | 37 | ## 3. 유스케이스 계층 구현 38 | 39 | - [ ] 3.1. 유스케이스 인터페이스 정의 40 | - [ ] CalculateExpressionUseCase 인터페이스 정의 41 | - [ ] ManageHistoryUseCase 인터페이스 정의 42 | - 커밋: "유스케이스 인터페이스 정의" 43 | 44 | - [ ] 3.2. 유스케이스 구현체 작성 45 | - [ ] CalculateExpressionImpl 구현 46 | - [ ] ManageHistoryImpl 구현 47 | - 커밋: "유스케이스 구현체 작성 완료" 48 | 49 | - [ ] 3.3. 유스케이스 단위 테스트 작성 50 | - [ ] CalculateExpressionUseCase 테스트 51 | - [ ] ManageHistoryUseCase 테스트 52 | - 커밋: "유스케이스 단위 테스트 작성 완료" 53 | 54 | ## 4. 어댑터 계층 구현 55 | 56 | - [ ] 4.1. Math.js 어댑터 구현 57 | - [ ] MathJsAdapter 클래스 구현 58 | - [ ] MathJsExpression 클래스 구현 59 | - 커밋: "Math.js 어댑터 구현" 60 | 61 | - [ ] 4.2. 스토리지 어댑터 구현 (로컬 스토리지) 62 | - [ ] LocalStorageAdapter 클래스 구현 63 | - 커밋: "스토리지 어댑터 구현" 64 | 65 | - [ ] 4.3. 어댑터 단위 테스트 작성 66 | - [ ] MathJsAdapter 테스트 67 | - [ ] LocalStorageAdapter 테스트 68 | - 커밋: "어댑터 단위 테스트 작성 완료" 69 | 70 | ## 5. 프레임워크 계층 구현 - 상태 관리 71 | 72 | - [ ] 5.1. 상태 관리 구현 73 | - [ ] CalculatorContext 생성 74 | - [ ] 리듀서 함수 구현 75 | - [ ] 액션 타입 정의 76 | - 커밋: "상태 관리 구현" 77 | 78 | - [ ] 5.2. 커스텀 훅 구현 79 | - [ ] useCalculator 훅 구현 80 | - [ ] useTheme 훅 구현 81 | - 커밋: "커스텀 훅 구현 완료" 82 | 83 | - [ ] 5.3. 상태 관리 테스트 작성 84 | - [ ] CalculatorContext 테스트 85 | - [ ] 리듀서 함수 테스트 86 | - [ ] 커스텀 훅 테스트 87 | - 커밋: "상태 관리 테스트 작성 완료" 88 | 89 | ## 6. 프레임워크 계층 구현 - UI 컴포넌트 90 | 91 | - [x] 6.1. 테마 설정 92 | - [x] 기본 스타일 설정 93 | - [x] 다크/라이트 모드 구현 94 | - [x] CSS 변수를 사용한 테마 설정 95 | - [x] 시스템 테마 감지 및 적용 96 | - [x] 테마 토글 버튼 구현 97 | - [x] localStorage를 사용한 테마 설정 저장 98 | - [x] 테마 전환 애니메이션 추가 99 | - 커밋: "테마 설정 완료" 100 | 101 | - [x] 6.2. 기본 UI 컴포넌트 구현 102 | - [x] App 컴포넌트 구현 103 | - [x] Calculator 컴포넌트 구현 104 | - [x] 계산기 디스플레이 구현 105 | - [x] 키패드 구현 106 | - [x] 버튼 구현 107 | - 커밋: "기본 UI 컴포넌트 구현 완료" 108 | 109 | - [ ] 6.3. 공학용 기능 UI 컴포넌트 구현 110 | - [ ] EngineeringKeypad 컴포넌트 구현 111 | - [ ] ModeToggle 컴포넌트 구현 112 | - 커밋: "공학용 기능 UI 컴포넌트 구현 완료" 113 | 114 | - [x] 6.4. 히스토리 UI 컴포넌트 구현 115 | - [x] 계산 기록 표시 기능 구현 116 | - [x] 기록 아이템 구현 117 | - 커밋: "히스토리 UI 컴포넌트 구현 완료" 118 | 119 | - [ ] 6.5. UI 컴포넌트 테스트 작성 120 | - [ ] Calculator 컴포넌트 테스트 121 | - [ ] Display 컴포넌트 테스트 122 | - [ ] Keypad 컴포넌트 테스트 123 | - [ ] Button 컴포넌트 테스트 124 | - [ ] EngineeringKeypad 컴포넌트 테스트 125 | - [ ] ModeToggle 컴포넌트 테스트 126 | - [ ] History 컴포넌트 테스트 127 | - 커밋: "UI 컴포넌트 테스트 작성 완료" 128 | 129 | ## 7. 통합 및 최적화 130 | 131 | - [ ] 7.1. 컴포넌트 통합 132 | - [ ] 모든 컴포넌트 연결 및 통합 133 | - [ ] 전체 앱 동작 확인 134 | - 커밋: "컴포넌트 통합 완료" 135 | 136 | - [ ] 7.2. 반응형 디자인 최적화 137 | - [ ] 모바일 화면 최적화 138 | - [ ] 태블릿 화면 최적화 139 | - [ ] 데스크톱 화면 최적화 140 | - 커밋: "반응형 디자인 최적화 완료" 141 | 142 | - [ ] 7.3. 성능 최적화 143 | - [ ] 불필요한 리렌더링 방지 144 | - [ ] 메모이제이션 적용 145 | - [ ] 번들 크기 최적화 146 | - 커밋: "성능 최적화 완료" 147 | 148 | ## 8. 문서화 및 배포 149 | 150 | - [ ] 8.1. 컴포넌트 문서화 151 | - [ ] Storybook 설정 152 | - [ ] 주요 컴포넌트 스토리 작성 153 | - 커밋: "컴포넌트 문서화 완료" 154 | 155 | - [ ] 8.2. 사용자 문서 작성 156 | - [ ] 사용 설명서 작성 157 | - [x] README.md 업데이트 158 | - [x] 데모 시나리오 작성 159 | - 커밋: "사용자 문서 작성 완료" 160 | 161 | - [x] 8.3. 배포 설정 162 | - [ ] Vercel 배포 설정 163 | - [x] CI/CD 파이프라인 구성 (GitHub Actions) 164 | - 커밋: "배포 설정 완료" 165 | 166 | - [ ] 8.4. 최종 배포 167 | - [ ] 최종 테스트 수행 168 | - [ ] 프로덕션 빌드 생성 169 | - [x] GitHub Pages에 배포 170 | - 커밋: "최종 배포 완료" 171 | 172 | ## 9. 프로젝트 완료 173 | 174 | - [ ] 9.1. 프로젝트 회고 175 | - [ ] 개발 과정 리뷰 176 | - [ ] 개선 사항 식별 177 | - [ ] 향후 기능 확장 계획 수립 178 | - 커밋: "프로젝트 회고 완료" 179 | 180 | - [ ] 9.2. 최종 문서 정리 181 | - [ ] 모든 문서 최종 검토 및 업데이트 182 | - 커밋: "최종 문서 정리 완료" 183 | 184 | ## 작업 진행 시 주의사항 185 | 186 | 1. 각 단계별로 테스트를 먼저 작성한 후 구현을 진행하는 TDD(Test-Driven Development) 방식으로 개발합니다. 187 | 2. SOLID 원칙을 준수하여 코드를 작성합니다. 188 | 3. Clean Architecture의 계층 간 의존성 방향을 준수합니다. 189 | 4. 각 커밋은 명확하고 일관된 메시지를 포함해야 합니다. 190 | 5. 코드 품질 유지를 위해 ESLint와 Prettier를 활용합니다. 191 | 6. 주요 컴포넌트 개발 후에는 /docs 폴더에 관련 문서를 업데이트합니다. 192 | 193 | ## 현재 진행 상황 (2025-04-22) 194 | 195 | - 프로젝트 초기 설정 완료 196 | - Expression 엔티티 및 테스트 구현 완료 197 | - 기본 UI 컴포넌트 및 계산기 화면 구현 완료 198 | - 다크/라이트 모드 지원 구현 완료 199 | - CSS 변수 기반 테마 시스템 구현 200 | - 테마 토글 및 사용자 선호도 저장 기능 구현 201 | - 시스템 테마 감지 및 적용 기능 구현 202 | - 계산 기록 표시 기능 구현 완료 203 | - GitHub Actions를 사용한 GitHub Pages 배포 설정 완료 204 | - npm 기반 워크플로우 구성 205 | - 필요한 권한 설정 206 | - 문서화 작업 진행 중 207 | - README.md 업데이트 완료 208 | - 데모 시나리오 작성 완료 209 | 210 | ### 다음 작업 예정 211 | - CalculationResult 및 CalculationHistory 엔티티 구현 212 | - 유스케이스 계층 구현 213 | - Math.js 어댑터 구현 214 | - 공학용 기능 UI 컴포넌트 구현 215 | -------------------------------------------------------------------------------- /src/frameworks/ui/components/Calculator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | /** 4 | * 계산기 컴포넌트 5 | * 6 | * 일반 모드와 공학용 모드를 지원하는 계산기 UI 컴포넌트입니다. 7 | * 다크 모드와 라이트 모드를 전환할 수 있습니다. 8 | */ 9 | const Calculator: React.FC = () => { 10 | // 상태 관리 11 | const [expression, setExpression] = useState(''); 12 | const [result, setResult] = useState('0'); 13 | const [history, setHistory] = useState>([]); 14 | const [isEngineeringMode, setIsEngineeringMode] = useState(false); 15 | 16 | // 다크 모드 상태 초기화 (localStorage 또는 시스템 설정에서 가져오기) 17 | const [isDarkMode, setIsDarkMode] = useState(() => { 18 | const savedTheme = localStorage.getItem('theme'); 19 | if (savedTheme) { 20 | return savedTheme === 'dark'; 21 | } 22 | return window.matchMedia('(prefers-color-scheme: dark)').matches; 23 | }); 24 | 25 | // 다크 모드 초기화 및 시스템 설정 변경 감지 26 | useEffect(() => { 27 | // 다크 모드 클래스 적용 28 | if (isDarkMode) { 29 | document.documentElement.classList.add('dark-mode'); 30 | localStorage.setItem('theme', 'dark'); 31 | } else { 32 | document.documentElement.classList.remove('dark-mode'); 33 | localStorage.setItem('theme', 'light'); 34 | } 35 | 36 | // 시스템 다크 모드 설정 감지 37 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 38 | 39 | // 시스템 설정 변경 시 다크 모드 업데이트 (localStorage에 저장된 설정이 없을 경우에만) 40 | const handleDarkModeChange = (e: MediaQueryListEvent) => { 41 | const savedTheme = localStorage.getItem('theme'); 42 | if (!savedTheme) { 43 | setIsDarkMode(e.matches); 44 | } 45 | }; 46 | 47 | darkModeMediaQuery.addEventListener('change', handleDarkModeChange); 48 | 49 | return () => { 50 | darkModeMediaQuery.removeEventListener('change', handleDarkModeChange); 51 | }; 52 | }, [isDarkMode]); 53 | 54 | // 키 입력 처리 55 | const handleKeyPress = (key: string) => { 56 | switch (key) { 57 | case 'C': 58 | // 모든 입력 초기화 59 | setExpression(''); 60 | setResult('0'); 61 | break; 62 | case '=': 63 | // 계산 실행 (추후 usecase 연결) 64 | try { 65 | // 임시 계산 로직 (추후 math.js 라이브러리로 대체) 66 | const calculatedResult = eval(expression).toString(); 67 | setResult(calculatedResult); 68 | 69 | // 계산 기록 저장 70 | setHistory([ 71 | { expression, result: calculatedResult }, 72 | ...history.slice(0, 9), // 최근 10개 기록만 유지 73 | ]); 74 | 75 | // 새로운 계산을 위해 표현식 초기화 76 | setExpression(''); 77 | } catch { 78 | setResult('Error'); 79 | } 80 | break; 81 | case '←': 82 | // 마지막 문자 삭제 83 | setExpression(expression.slice(0, -1)); 84 | break; 85 | case '+/-': 86 | // 부호 변경 87 | if (expression.startsWith('-')) { 88 | setExpression(expression.slice(1)); 89 | } else { 90 | setExpression('-' + expression); 91 | } 92 | break; 93 | case 'sin': 94 | case 'cos': 95 | case 'tan': 96 | case 'log': 97 | case 'ln': 98 | case 'π': 99 | case '√': 100 | // 함수 처리 (추후 math.js 라이브러리로 대체) 101 | setExpression(expression + key + '('); 102 | break; 103 | default: 104 | // 표현식에 키 추가 105 | setExpression(expression + key); 106 | break; 107 | } 108 | }; 109 | 110 | // 테마 전환 111 | const toggleTheme = () => { 112 | setIsDarkMode(!isDarkMode); 113 | }; 114 | 115 | // 키패드 배열 정의 (5x5 그리드) 116 | const standardKeypad = [ 117 | ['C', '+/-', '%', '/', '←'], 118 | ['7', '8', '9', '*', '('], 119 | ['4', '5', '6', '-', ')'], 120 | ['1', '2', '3', '+', '^'], 121 | ['0', '.', '=', '√', 'π'], 122 | ]; 123 | 124 | // 공학용 함수 배열 125 | const engineeringFunctions = ['sin', 'cos', 'tan', 'log', 'ln']; 126 | 127 | return ( 128 |
129 |
130 |
공학용 계산기
131 | 138 |
139 | 140 |
141 | 147 | 153 |
154 | 155 |
156 |
{expression}
157 |
{result}
158 |
159 | 160 |
161 | {standardKeypad.flat().map((key) => ( 162 | 173 | ))} 174 |
175 | 176 | {isEngineeringMode && ( 177 |
178 | {engineeringFunctions.map((func) => ( 179 | 186 | ))} 187 |
188 | )} 189 | 190 | {history.length > 0 && ( 191 |
192 |

계산 기록:

193 | {history.map((item, index) => ( 194 |
195 |
{item.expression}
196 |
{item.result}
197 |
198 | ))} 199 |
200 | )} 201 |
202 | ); 203 | }; 204 | 205 | export default Calculator; 206 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* CSS 변수 정의 */ 2 | :root { 3 | --text-color: #333; 4 | --header-color: #2196f3; 5 | --calculator-bg: #f5f5f5; 6 | --calculator-shadow: rgba(0, 0, 0, 0.1); 7 | --toggle-bg: #333; 8 | --toggle-border: #ddd; 9 | --mode-toggle-bg: #e0e0e0; 10 | --mode-active-bg: #fff; 11 | --mode-active-color: #333; 12 | --display-bg: #fff; 13 | --display-shadow: rgba(0, 0, 0, 0.1); 14 | --expression-color: #757575; 15 | --result-color: #333; 16 | --key-bg: #fff; 17 | --key-hover-bg: #f0f0f0; 18 | --operator-bg: #2196f3; 19 | --operator-color: #fff; 20 | --equals-bg: #4caf50; 21 | --equals-color: #fff; 22 | --clear-bg: #ff9800; 23 | --clear-color: #fff; 24 | --function-bg: #e3f2fd; 25 | --function-color: #333; 26 | --functions-area-bg: #e3f2fd; 27 | --history-bg: #fff; 28 | --history-border: #e0e0e0; 29 | --history-expression: #757575; 30 | --history-result: #333; 31 | } 32 | 33 | #root { 34 | max-width: 1280px; 35 | margin: 0 auto; 36 | padding: 2rem; 37 | text-align: center; 38 | } 39 | 40 | .logo { 41 | height: 6em; 42 | padding: 1.5em; 43 | will-change: filter; 44 | transition: filter 300ms; 45 | } 46 | .logo:hover { 47 | filter: drop-shadow(0 0 2em #646cffaa); 48 | } 49 | .logo.react:hover { 50 | filter: drop-shadow(0 0 2em #61dafbaa); 51 | } 52 | 53 | @keyframes logo-spin { 54 | from { 55 | transform: rotate(0deg); 56 | } 57 | to { 58 | transform: rotate(360deg); 59 | } 60 | } 61 | 62 | @media (prefers-reduced-motion: no-preference) { 63 | a:nth-of-type(2) .logo { 64 | animation: logo-spin infinite 20s linear; 65 | } 66 | } 67 | 68 | .card { 69 | padding: 2em; 70 | } 71 | 72 | .read-the-docs { 73 | color: #888; 74 | } 75 | 76 | /* 계산기 스타일 */ 77 | .container { 78 | max-width: 800px; 79 | margin: 0 auto; 80 | padding: 20px; 81 | font-family: 'Arial', sans-serif; 82 | color: var(--text-color); 83 | } 84 | 85 | header { 86 | margin-bottom: 30px; 87 | } 88 | 89 | header h1 { 90 | color: var(--header-color); 91 | margin-bottom: 10px; 92 | } 93 | 94 | .calculator { 95 | display: flex; 96 | flex-direction: column; 97 | background-color: var(--calculator-bg); 98 | border-radius: 24px; 99 | box-shadow: 0 4px 12px var(--calculator-shadow); 100 | overflow: hidden; 101 | max-width: 400px; 102 | margin: 0 auto; 103 | padding: 20px; 104 | transition: background-color 0.3s ease, box-shadow 0.3s ease; 105 | } 106 | 107 | .calculator-header { 108 | display: flex; 109 | justify-content: space-between; 110 | align-items: center; 111 | margin-bottom: 16px; 112 | } 113 | 114 | .calculator-title { 115 | font-size: 20px; 116 | font-weight: bold; 117 | color: var(--text-color); 118 | } 119 | 120 | .theme-toggle { 121 | width: 40px; 122 | height: 40px; 123 | border-radius: 50%; 124 | background-color: var(--toggle-bg); 125 | border: 2px solid var(--toggle-border); 126 | cursor: pointer; 127 | display: flex; 128 | align-items: center; 129 | justify-content: center; 130 | transition: background-color 0.3s ease, border-color 0.3s ease; 131 | } 132 | 133 | .mode-toggle { 134 | display: flex; 135 | margin-bottom: 16px; 136 | border-radius: 16px; 137 | overflow: hidden; 138 | background-color: var(--mode-toggle-bg); 139 | transition: background-color 0.3s ease; 140 | } 141 | 142 | .mode-toggle button { 143 | flex: 1; 144 | padding: 10px; 145 | border: none; 146 | background-color: transparent; 147 | cursor: pointer; 148 | font-size: 16px; 149 | transition: background-color 0.2s; 150 | color: var(--text-color); 151 | } 152 | 153 | .mode-toggle button.active { 154 | background-color: var(--mode-active-bg); 155 | color: var(--mode-active-color); 156 | font-weight: bold; 157 | } 158 | 159 | .calculator-display { 160 | background-color: var(--display-bg); 161 | padding: 16px; 162 | border-radius: 12px; 163 | margin-bottom: 16px; 164 | box-shadow: inset 0 1px 3px var(--display-shadow); 165 | transition: background-color 0.3s ease, box-shadow 0.3s ease; 166 | } 167 | 168 | .calculator-expression { 169 | font-size: 16px; 170 | color: var(--expression-color); 171 | min-height: 24px; 172 | margin-bottom: 8px; 173 | word-break: break-all; 174 | text-align: left; 175 | } 176 | 177 | .calculator-result { 178 | font-size: 32px; 179 | font-weight: bold; 180 | color: var(--result-color); 181 | min-height: 40px; 182 | word-break: break-all; 183 | text-align: right; 184 | } 185 | 186 | .calculator-keypad { 187 | display: grid; 188 | grid-template-columns: repeat(5, 1fr); 189 | gap: 8px; 190 | margin-bottom: 16px; 191 | } 192 | 193 | .calculator-key { 194 | background-color: var(--key-bg); 195 | border: none; 196 | font-size: 20px; 197 | padding: 16px 0; 198 | border-radius: 12px; 199 | cursor: pointer; 200 | transition: background-color 0.2s, color 0.2s, box-shadow 0.2s; 201 | box-shadow: 0 1px 3px var(--calculator-shadow); 202 | color: var(--text-color); 203 | } 204 | 205 | .calculator-key:hover { 206 | background-color: var(--key-hover-bg); 207 | } 208 | 209 | .calculator-key.operator { 210 | background-color: var(--operator-bg); 211 | color: var(--operator-color); 212 | } 213 | 214 | .calculator-key.equals { 215 | background-color: var(--equals-bg); 216 | color: var(--equals-color); 217 | } 218 | 219 | .calculator-key.clear { 220 | background-color: var(--clear-bg); 221 | color: var(--clear-color); 222 | } 223 | 224 | .calculator-key.function { 225 | background-color: var(--function-bg); 226 | color: var(--function-color); 227 | } 228 | 229 | .calculator-key.backspace { 230 | background-color: var(--key-hover-bg); 231 | } 232 | 233 | .calculator-functions { 234 | display: grid; 235 | grid-template-columns: repeat(5, 1fr); 236 | gap: 8px; 237 | margin-bottom: 16px; 238 | background-color: var(--functions-area-bg); 239 | padding: 8px; 240 | border-radius: 8px; 241 | transition: background-color 0.3s ease; 242 | } 243 | 244 | .calculator-function { 245 | background-color: var(--key-bg); 246 | border: none; 247 | font-size: 16px; 248 | padding: 10px 0; 249 | border-radius: 8px; 250 | cursor: pointer; 251 | transition: background-color 0.2s; 252 | color: var(--text-color); 253 | } 254 | 255 | .calculator-function:hover { 256 | background-color: var(--key-hover-bg); 257 | } 258 | 259 | .calculator-history { 260 | margin-top: 16px; 261 | background-color: var(--history-bg); 262 | border-radius: 12px; 263 | padding: 16px; 264 | box-shadow: 0 1px 3px var(--calculator-shadow); 265 | transition: background-color 0.3s ease, box-shadow 0.3s ease; 266 | } 267 | 268 | .calculator-history h3 { 269 | margin-top: 0; 270 | margin-bottom: 12px; 271 | font-size: 16px; 272 | color: var(--text-color); 273 | text-align: left; 274 | } 275 | 276 | .history-item { 277 | padding: 8px 0; 278 | border-bottom: 1px solid var(--history-border); 279 | text-align: right; 280 | } 281 | 282 | .history-item:last-child { 283 | border-bottom: none; 284 | } 285 | 286 | .history-expression { 287 | font-size: 14px; 288 | color: var(--history-expression); 289 | } 290 | 291 | .history-result { 292 | font-size: 16px; 293 | font-weight: bold; 294 | color: var(--history-result); 295 | } 296 | -------------------------------------------------------------------------------- /docs/assets/calculator-wireframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 공학용 계산기 8 | 9 | 10 | 11 | 일반 모드 12 | 13 | 14 | 공학용 모드 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | (3 + 5) * 2 - 4 / 2 26 | 27 | 28 | 29 | 14 30 | 31 | 32 | 33 | 34 | 35 | 36 | C 37 | 38 | 39 | +/- 40 | 41 | 42 | % 43 | 44 | 45 | / 46 | 47 | 48 | 49 | 50 | 51 | 52 | 7 53 | 54 | 55 | 8 56 | 57 | 58 | 9 59 | 60 | 61 | * 62 | 63 | 64 | ( 65 | 66 | 67 | 68 | 4 69 | 70 | 71 | 5 72 | 73 | 74 | 6 75 | 76 | 77 | - 78 | 79 | 80 | ) 81 | 82 | 83 | 84 | 1 85 | 86 | 87 | 2 88 | 89 | 90 | 3 91 | 92 | 93 | + 94 | 95 | 96 | ^ 97 | 98 | 99 | 100 | 0 101 | 102 | 103 | . 104 | 105 | 106 | = 107 | 108 | 109 | 110 | 111 | 112 | π 113 | 114 | 115 | 116 | 117 | 118 | 119 | sin 120 | 121 | 122 | cos 123 | 124 | 125 | tan 126 | 127 | 128 | log 129 | 130 | 131 | ln 132 | 133 | 134 | 135 | 계산 기록: 136 | 137 | 3 + 5 = 8 138 | (3 + 5) * 2 - 4 / 2 = 14 139 | 140 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # 공학용 계산기 웹앱 설계 문서 2 | 3 | ## 1. 개요 4 | 5 | 이 문서는 공학용 계산기 웹앱의 설계를 정의합니다. 이 웹앱은 기본 수학 연산과 공학용 함수를 지원하는 계산기로, 사용자 친화적인 인터페이스와 다크/라이트 모드, 반응형 디자인을 제공합니다. 6 | 7 | ### 1.1 목적 8 | 9 | 이 웹앱의 주요 목적은 다음과 같습니다: 10 | - 기본 수학 연산 및 공학용 함수 계산 제공 11 | - 복잡한 수식 계산 지원 12 | - 계산 기록 저장 및 조회 기능 제공 13 | - 다크/라이트 모드와 반응형 디자인으로 다양한 환경에서 사용 가능 14 | 15 | ### 1.2 범위 16 | 17 | 이 웹앱은 다음 기능을 포함합니다: 18 | - 기본 연산: 덧셈, 뺄셈, 곱셈, 나눗셈, 제곱, 제곱근 19 | - 공학용 함수: 삼각함수, 로그함수, 지수함수 20 | - 괄호를 사용한 복잡한 수식 계산 21 | - 계산 기록 저장 및 조회 22 | - 일반 계산기와 공학용 계산기 모드 전환 23 | - 다크/라이트 모드 지원 24 | - 반응형 디자인 25 | 26 | ## 2. 아키텍처 설계 27 | 28 | 공학용 계산기 웹앱은 Clean Architecture와 SOLID 원칙을 기반으로 설계됩니다. 이를 통해 코드의 유지보수성, 테스트 용이성, 확장성을 향상시킵니다. 29 | 30 | ### 2.1 아키텍처 개요 31 | 32 | ```mermaid 33 | graph TD 34 | A[UI Layer] --> B[Application Layer] 35 | B --> C[Domain Layer] 36 | B --> D[Infrastructure Layer] 37 | D --> C 38 | ``` 39 | 40 | - **UI Layer**: 사용자 인터페이스 컴포넌트 (React 컴포넌트) 41 | - **Application Layer**: 사용자 케이스 구현 (계산 수행, 히스토리 관리 등) 42 | - **Domain Layer**: 핵심 비즈니스 로직과 엔티티 (계산 모델, 수식 표현 등) 43 | - **Infrastructure Layer**: 외부 라이브러리와의 인터페이스 (Math.js 어댑터 등) 44 | 45 | ### 2.2 Clean Architecture 적용 46 | 47 | Clean Architecture에 따라 프로젝트 구조를 다음과 같이 구성합니다: 48 | 49 | ``` 50 | src/ 51 | ├── entities/ # 핵심 비즈니스 로직과 데이터 모델 52 | ├── usecases/ # 애플리케이션 특화 비즈니스 규칙 53 | ├── adapters/ # 외부 라이브러리와의 인터페이스 54 | ├── frameworks/ # UI 컴포넌트, 상태 관리 등 55 | └── main.tsx # 앱 진입점 56 | ``` 57 | 58 | ### 2.3 SOLID 원칙 적용 59 | 60 | - **단일 책임 원칙(SRP)**: 각 컴포넌트와 함수는 하나의 책임만 가집니다. 61 | - **개방-폐쇄 원칙(OCP)**: 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계합니다. 62 | - **리스코프 치환 원칙(LSP)**: 상위 타입의 객체를 하위 타입의 객체로 대체해도 프로그램의 정확성이 유지되도록 합니다. 63 | - **인터페이스 분리 원칙(ISP)**: 클라이언트가 사용하지 않는 인터페이스에 의존하지 않도록 합니다. 64 | - **의존성 역전 원칙(DIP)**: 고수준 모듈이 저수준 모듈에 의존하지 않도록 추상화에 의존합니다. 65 | 66 | ## 3. 컴포넌트 설계 67 | 68 | ### 3.1 엔티티 (Entities) 69 | 70 | #### 3.1.1 Expression 71 | 72 | 수학 표현식을 나타내는 엔티티입니다. 73 | 74 | ```typescript 75 | // src/entities/Expression.ts 76 | export interface Expression { 77 | value: string; 78 | evaluate(): number; 79 | toString(): string; 80 | } 81 | ``` 82 | 83 | #### 3.1.2 CalculationResult 84 | 85 | 계산 결과를 나타내는 엔티티입니다. 86 | 87 | ```typescript 88 | // src/entities/CalculationResult.ts 89 | export interface CalculationResult { 90 | expression: string; 91 | result: number; 92 | timestamp: Date; 93 | } 94 | ``` 95 | 96 | #### 3.1.3 CalculationHistory 97 | 98 | 계산 기록을 나타내는 엔티티입니다. 99 | 100 | ```typescript 101 | // src/entities/CalculationHistory.ts 102 | import { CalculationResult } from './CalculationResult'; 103 | 104 | export interface CalculationHistory { 105 | items: CalculationResult[]; 106 | add(item: CalculationResult): void; 107 | clear(): void; 108 | getItems(): CalculationResult[]; 109 | } 110 | ``` 111 | 112 | ### 3.2 유스케이스 (Usecases) 113 | 114 | #### 3.2.1 CalculateExpression 115 | 116 | 수학 표현식을 계산하는 유스케이스입니다. 117 | 118 | ```typescript 119 | // src/usecases/CalculateExpression.ts 120 | import { Expression } from '../entities/Expression'; 121 | import { CalculationResult } from '../entities/CalculationResult'; 122 | 123 | export interface CalculateExpressionUseCase { 124 | execute(expression: Expression): CalculationResult; 125 | } 126 | ``` 127 | 128 | #### 3.2.2 ManageHistory 129 | 130 | 계산 기록을 관리하는 유스케이스입니다. 131 | 132 | ```typescript 133 | // src/usecases/ManageHistory.ts 134 | import { CalculationResult } from '../entities/CalculationResult'; 135 | import { CalculationHistory } from '../entities/CalculationHistory'; 136 | 137 | export interface ManageHistoryUseCase { 138 | addToHistory(result: CalculationResult): void; 139 | clearHistory(): void; 140 | getHistory(): CalculationHistory; 141 | } 142 | ``` 143 | 144 | ### 3.3 어댑터 (Adapters) 145 | 146 | #### 3.3.1 MathJsAdapter 147 | 148 | Math.js 라이브러리와의 인터페이스를 제공하는 어댑터입니다. 149 | 150 | ```typescript 151 | // src/adapters/MathJsAdapter.ts 152 | import { Expression } from '../entities/Expression'; 153 | import * as mathjs from 'mathjs'; 154 | 155 | export class MathJsExpression implements Expression { 156 | constructor(public value: string) {} 157 | 158 | evaluate(): number { 159 | return mathjs.evaluate(this.value); 160 | } 161 | 162 | toString(): string { 163 | return this.value; 164 | } 165 | } 166 | ``` 167 | 168 | ### 3.4 프레임워크 (Frameworks) 169 | 170 | #### 3.4.1 UI 컴포넌트 171 | 172 | ```typescript 173 | // src/frameworks/ui/components/Calculator.tsx 174 | import React from 'react'; 175 | import { useCalculator } from '../hooks/useCalculator'; 176 | 177 | export const Calculator: React.FC = () => { 178 | const { 179 | expression, 180 | result, 181 | history, 182 | handleButtonClick, 183 | handleClear, 184 | handleCalculate, 185 | handleModeToggle, 186 | isEngineeringMode 187 | } = useCalculator(); 188 | 189 | return ( 190 | // 계산기 UI 구현 191 | ); 192 | }; 193 | ``` 194 | 195 | #### 3.4.2 상태 관리 196 | 197 | ```typescript 198 | // src/frameworks/state/calculatorContext.tsx 199 | import React, { createContext, useReducer } from 'react'; 200 | import { CalculationResult } from '../../entities/CalculationResult'; 201 | 202 | // 상태 타입 정의 203 | interface CalculatorState { 204 | expression: string; 205 | result: string; 206 | history: CalculationResult[]; 207 | isEngineeringMode: boolean; 208 | isDarkMode: boolean; 209 | } 210 | 211 | // 액션 타입 정의 212 | type CalculatorAction = 213 | | { type: 'SET_EXPRESSION'; payload: string } 214 | | { type: 'SET_RESULT'; payload: string } 215 | | { type: 'ADD_TO_HISTORY'; payload: CalculationResult } 216 | | { type: 'CLEAR_HISTORY' } 217 | | { type: 'TOGGLE_MODE' } 218 | | { type: 'TOGGLE_THEME' }; 219 | 220 | // 리듀서 함수 221 | const calculatorReducer = (state: CalculatorState, action: CalculatorAction): CalculatorState => { 222 | // 액션 처리 로직 223 | }; 224 | 225 | // 컨텍스트 생성 226 | export const CalculatorContext = createContext<{ 227 | state: CalculatorState; 228 | dispatch: React.Dispatch; 229 | }>({ 230 | state: { 231 | expression: '', 232 | result: '', 233 | history: [], 234 | isEngineeringMode: false, 235 | isDarkMode: false, 236 | }, 237 | dispatch: () => null, 238 | }); 239 | 240 | // 컨텍스트 프로바이더 241 | export const CalculatorProvider: React.FC = ({ children }) => { 242 | const [state, dispatch] = useReducer(calculatorReducer, { 243 | expression: '', 244 | result: '', 245 | history: [], 246 | isEngineeringMode: false, 247 | isDarkMode: false, 248 | }); 249 | 250 | return ( 251 | 252 | {children} 253 | 254 | ); 255 | }; 256 | ``` 257 | 258 | ## 4. 사용자 인터페이스 설계 259 | 260 | ### 4.1 레이아웃 261 | 262 | 계산기는 다음과 같은 레이아웃으로 구성됩니다: 263 | 264 | #### 와이어프레임 265 | 266 | ![계산기 와이어프레임](./assets/calculator-wireframe.svg) 267 | 268 | 이 와이어프레임은 다음 구성 요소를 보여줍니다: 269 | - 상단 모드 전환 버튼 (일반/공학용) 270 | - 다크/라이트 모드 토글 버튼 271 | - 표현식 입력 및 결과 출력 영역 272 | - 기본 키패드 영역 (숫자, 연산자, 괄호 등) 273 | - 공학용 키패드 영역 (삼각함수, 로그 등) 274 | - 계산 기록 영역 275 | 276 | 277 | ### 4.2 테마 278 | 279 | 다크 모드와 라이트 모드를 지원하며, Chakra UI의 테마 시스템을 활용합니다. 280 | 281 | ```typescript 282 | // src/frameworks/ui/theme.ts 283 | import { extendTheme } from '@chakra-ui/react'; 284 | 285 | const theme = extendTheme({ 286 | config: { 287 | initialColorMode: 'light', 288 | useSystemColorMode: true, 289 | }, 290 | colors: { 291 | // 색상 정의 292 | }, 293 | components: { 294 | // 컴포넌트 스타일 정의 295 | }, 296 | }); 297 | 298 | export default theme; 299 | ``` 300 | 301 | ### 4.3 반응형 디자인 302 | 303 | Chakra UI의 반응형 유틸리티를 활용하여 다양한 화면 크기에 대응합니다. 304 | 305 | ```typescript 306 | // 예시: 반응형 버튼 크기 307 | 313 | ``` 314 | 315 | ## 5. 테스트 전략 316 | 317 | ### 5.1 단위 테스트 318 | 319 | 각 엔티티, 유스케이스, 어댑터에 대한 단위 테스트를 작성합니다. 320 | 321 | ```typescript 322 | // src/entities/__tests__/Expression.test.ts 323 | import { MathJsExpression } from '../../adapters/MathJsAdapter'; 324 | 325 | describe('Expression', () => { 326 | it('should evaluate simple expression correctly', () => { 327 | const expression = new MathJsExpression('2 + 3'); 328 | expect(expression.evaluate()).toBe(5); 329 | }); 330 | 331 | it('should evaluate complex expression correctly', () => { 332 | const expression = new MathJsExpression('(3 + 5) * 2 - 4 / 2'); 333 | expect(expression.evaluate()).toBe(14); 334 | }); 335 | }); 336 | ``` 337 | 338 | ### 5.2 통합 테스트 339 | 340 | 유스케이스와 어댑터 간의 상호작용을 테스트합니다. 341 | 342 | ```typescript 343 | // src/usecases/__tests__/CalculateExpression.test.ts 344 | import { CalculateExpressionImpl } from '../CalculateExpressionImpl'; 345 | import { MathJsExpression } from '../../adapters/MathJsAdapter'; 346 | 347 | describe('CalculateExpression', () => { 348 | it('should calculate expression and return result', () => { 349 | const calculateExpression = new CalculateExpressionImpl(); 350 | const expression = new MathJsExpression('2 + 3'); 351 | const result = calculateExpression.execute(expression); 352 | 353 | expect(result.expression).toBe('2 + 3'); 354 | expect(result.result).toBe(5); 355 | expect(result.timestamp).toBeInstanceOf(Date); 356 | }); 357 | }); 358 | ``` 359 | 360 | ### 5.3 UI 테스트 361 | 362 | React Testing Library를 사용하여 UI 컴포넌트를 테스트합니다. 363 | 364 | ```typescript 365 | // src/frameworks/ui/components/__tests__/Calculator.test.tsx 366 | import { render, screen, fireEvent } from '@testing-library/react'; 367 | import { Calculator } from '../Calculator'; 368 | import { CalculatorProvider } from '../../state/calculatorContext'; 369 | 370 | describe('Calculator', () => { 371 | it('should render calculator', () => { 372 | render( 373 | 374 | 375 | 376 | ); 377 | 378 | expect(screen.getByText('0')).toBeInTheDocument(); 379 | }); 380 | 381 | it('should perform calculation when = is clicked', () => { 382 | render( 383 | 384 | 385 | 386 | ); 387 | 388 | fireEvent.click(screen.getByText('2')); 389 | fireEvent.click(screen.getByText('+')); 390 | fireEvent.click(screen.getByText('3')); 391 | fireEvent.click(screen.getByText('=')); 392 | 393 | expect(screen.getByText('5')).toBeInTheDocument(); 394 | }); 395 | }); 396 | ``` 397 | 398 | ## 6. 배포 전략 399 | 400 | ### 6.1 개발 환경 401 | 402 | - **로컬 개발**: Vite 개발 서버를 사용하여 로컬에서 개발 403 | - **코드 품질**: ESLint와 Prettier를 사용하여 코드 품질 유지 404 | - **테스트**: Vitest를 사용하여 테스트 자동화 405 | 406 | ### 6.2 CI/CD 파이프라인 407 | 408 | GitHub Actions를 사용하여 CI/CD 파이프라인을 구성합니다. 409 | 410 | ```yaml 411 | # .github/workflows/ci.yml 412 | name: CI 413 | 414 | on: 415 | push: 416 | branches: [ main ] 417 | pull_request: 418 | branches: [ main ] 419 | 420 | jobs: 421 | build: 422 | runs-on: ubuntu-latest 423 | steps: 424 | - uses: actions/checkout@v3 425 | - name: Use Node.js 426 | uses: actions/setup-node@v3 427 | with: 428 | node-version: '18' 429 | - name: Install dependencies 430 | run: yarn install 431 | - name: Lint 432 | run: yarn lint 433 | - name: Test 434 | run: yarn test 435 | - name: Build 436 | run: yarn build 437 | ``` 438 | 439 | ### 6.3 배포 440 | 441 | Vercel을 사용하여 자동 배포를 구성합니다. 442 | 443 | ``` 444 | # vercel.json 445 | { 446 | "version": 2, 447 | "builds": [ 448 | { 449 | "src": "package.json", 450 | "use": "@vercel/static-build", 451 | "config": { "distDir": "dist" } 452 | } 453 | ], 454 | "routes": [ 455 | { "handle": "filesystem" }, 456 | { "src": "/.*", "dest": "/index.html" } 457 | ] 458 | } 459 | ``` 460 | 461 | ## 7. 결론 462 | 463 | 이 설계 문서는 공학용 계산기 웹앱의 아키텍처, 컴포넌트, 사용자 인터페이스, 테스트 및 배포 전략을 정의합니다. Clean Architecture와 SOLID 원칙을 적용하여 유지보수가 용이하고 확장 가능한 웹앱을 구현할 수 있습니다. 464 | 465 | 이 설계를 바탕으로 React 19.1, TypeScript, Chakra UI, Math.js 등의 최신 기술을 활용하여 고품질의 공학용 계산기 웹앱을 개발할 수 있습니다. 466 | --------------------------------------------------------------------------------