├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── LICENSE
├── README.md
├── client
├── .babelrc
├── .env.sample
├── .eslintrc.json
├── .prettierrc
├── package-lock.json
├── package.json
├── public
│ ├── content.css
│ ├── font
│ │ ├── Lato-Bold.ttf
│ │ ├── Lato-BoldItalic.ttf
│ │ ├── Lato-Italic.ttf
│ │ ├── Lato-Regular.ttf
│ │ ├── NotoSansKR-Regular.otf
│ │ ├── brand-icons.eot
│ │ ├── brand-icons.ttf
│ │ ├── brand-icons.woff
│ │ ├── brand-icons.woff2
│ │ ├── icons.eot
│ │ ├── icons.otf
│ │ ├── icons.ttf
│ │ ├── icons.woff
│ │ ├── icons.woff2
│ │ ├── outline-icons.eot
│ │ ├── outline-icons.ttf
│ │ ├── outline-icons.woff
│ │ └── outline-icons.woff2
│ ├── icon.png
│ ├── image
│ │ ├── 1
│ │ │ ├── 1.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ └── 4.svg
│ │ ├── 2
│ │ │ ├── 1.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ └── 4.svg
│ │ ├── 3
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 4
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14-b.svg
│ │ │ ├── 15-b.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 5
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 15.svg
│ │ │ ├── 16.svg
│ │ │ ├── 17.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 6
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 15.svg
│ │ │ ├── 16.svg
│ │ │ ├── 17.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 7
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 15.svg
│ │ │ ├── 16.svg
│ │ │ ├── 17.svg
│ │ │ ├── 18.svg
│ │ │ ├── 19.svg
│ │ │ ├── 2.svg
│ │ │ ├── 20.svg
│ │ │ ├── 21.svg
│ │ │ ├── 22.svg
│ │ │ ├── 23.svg
│ │ │ ├── 24.svg
│ │ │ ├── 25.svg
│ │ │ ├── 26.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 8
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 15.svg
│ │ │ ├── 16.svg
│ │ │ ├── 17.svg
│ │ │ ├── 18.svg
│ │ │ ├── 19.svg
│ │ │ ├── 2.svg
│ │ │ ├── 20.svg
│ │ │ ├── 21.svg
│ │ │ ├── 22.svg
│ │ │ ├── 23.svg
│ │ │ ├── 24.svg
│ │ │ ├── 25.svg
│ │ │ ├── 26.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 9
│ │ │ ├── 1.svg
│ │ │ ├── 10-b.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12-b.svg
│ │ │ ├── 13-b.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 10
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 15.svg
│ │ │ ├── 16.svg
│ │ │ ├── 17.svg
│ │ │ ├── 18.svg
│ │ │ ├── 19.svg
│ │ │ ├── 2.svg
│ │ │ ├── 20.svg
│ │ │ ├── 21.svg
│ │ │ ├── 22.svg
│ │ │ ├── 23.svg
│ │ │ ├── 24.svg
│ │ │ ├── 25.svg
│ │ │ ├── 26.svg
│ │ │ ├── 27.svg
│ │ │ ├── 28.svg
│ │ │ ├── 29.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 11
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 2.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ │ ├── 1-btn.svg
│ │ ├── 10-btn.svg
│ │ ├── 11-btn.svg
│ │ ├── 12-btn.svg
│ │ ├── 13-btn.svg
│ │ ├── 14-btn.svg
│ │ ├── 15-btn.svg
│ │ ├── 16-btn.svg
│ │ ├── 17-btn.svg
│ │ ├── 2-btn.svg
│ │ ├── 3-btn.svg
│ │ ├── 4-btn.svg
│ │ ├── 5-btn.svg
│ │ ├── 6-btn.svg
│ │ ├── 7-btn.svg
│ │ ├── 8-btn.svg
│ │ ├── 9-btn.svg
│ │ ├── chef.gif
│ │ ├── formula.gif
│ │ ├── giphy.gif
│ │ ├── loading.gif
│ │ ├── login.png
│ │ ├── login2.png
│ │ ├── login3.png
│ │ ├── login4.png
│ │ ├── logo.png
│ │ ├── logout.png
│ │ ├── logout2.png
│ │ └── math.svg
│ ├── index.html
│ └── sementic.css
├── src
│ ├── App.tsx
│ ├── __tests__
│ │ ├── AlertItem.test.tsx
│ │ ├── ButtomItem.test.tsx
│ │ ├── DictionaryItem.test.tsx
│ │ ├── DictionaryMenuItem.test.tsx
│ │ ├── FontColorMenu.test.tsx
│ │ ├── FontSizeMenu.test.tsx
│ │ ├── IconMessage.test.tsx
│ │ ├── Loading.test.tsx
│ │ ├── MessageItem.test.tsx
│ │ ├── PageItem.test.tsx
│ │ ├── QrCode.test.tsx
│ │ ├── TableItem.test.tsx
│ │ ├── TextAreaItem.test.tsx
│ │ └── __snapshots__
│ │ │ ├── QrCode.test.tsx.snap
│ │ │ ├── TableItem.test.tsx.snap
│ │ │ └── TextAreaItem.test.tsx.snap
│ ├── components
│ │ ├── Ingredients
│ │ │ ├── AlertItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── ButtonItem
│ │ │ │ └── index.tsx
│ │ │ ├── CalculatorButton
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── CalculatorSymbol
│ │ │ │ └── index.tsx
│ │ │ ├── DictionaryItem
│ │ │ │ └── index.tsx
│ │ │ ├── DictionaryMenuItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── DropDownItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── FontColorMenu
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFontColorMenu.ts
│ │ │ ├── FontSizeMenu
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFontSizeMenu.ts
│ │ │ ├── FormulaItem
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFormulaItem.ts
│ │ │ ├── IconMessage
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── Loading
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── MessageItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── PageTabItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── usePageTabItem.ts
│ │ │ ├── QrCode
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── RecommendItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── TableItem
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useTableItem.ts
│ │ │ └── TextAreaItem
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useTextAreaItem.ts
│ │ ├── Meal
│ │ │ ├── CalculatorTab
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useCalculatorTab.ts
│ │ │ ├── DictionaryHeader
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── DictionaryItemList
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── DictionaryTab
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useDictionaryTab.ts
│ │ │ ├── FavoriteModal
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFavoriteModal.ts
│ │ │ ├── FavoriteTab
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFavoriteTab.ts
│ │ │ ├── FormatButtons
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFormatButtons.ts
│ │ │ ├── FormulaList
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useFormulaList.ts
│ │ │ ├── InputContents
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── MenuBar
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── MenuBarButtons
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── MenuBarInput
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── MenuBarOutput
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── OutputContents
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.ts
│ │ │ ├── OutputFormula
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useOutputFormula.ts
│ │ │ ├── OutputFormulaBox
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── useOutputFormulaBox.ts
│ │ │ └── PageTab
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.ts
│ │ │ │ └── usePageTab.ts
│ │ └── Set
│ │ │ ├── Header
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ │ │ ├── Input
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ │ │ ├── Output
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ │ │ ├── ResizeHeader
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ │ │ └── SaveButtons
│ │ │ ├── index.tsx
│ │ │ ├── style.ts
│ │ │ └── useSaveButtons.ts
│ ├── contexts
│ │ ├── index.ts
│ │ ├── latex
│ │ │ ├── actions.ts
│ │ │ ├── index.ts
│ │ │ ├── reducer.ts
│ │ │ └── types.ts
│ │ └── user
│ │ │ ├── actions.ts
│ │ │ ├── index.ts
│ │ │ ├── reducer.ts
│ │ │ ├── thunk.ts
│ │ │ └── types.ts
│ ├── hooks
│ │ ├── useCurrentTab.ts
│ │ ├── useDebounce.ts
│ │ ├── useInput.ts
│ │ ├── useModal.tsx
│ │ ├── useSelect.ts
│ │ ├── useThrottle.ts
│ │ └── useToggle.ts
│ ├── index.tsx
│ ├── lib
│ │ ├── apis
│ │ │ ├── common.ts
│ │ │ └── favorite.ts
│ │ ├── constants
│ │ │ ├── calculator.ts
│ │ │ ├── constants.ts
│ │ │ ├── latex-dictionary.ts
│ │ │ └── latex-header.ts
│ │ └── utils
│ │ │ ├── test-util.tsx
│ │ │ ├── token.ts
│ │ │ └── util.ts
│ ├── pages
│ │ └── MainPage.tsx
│ └── setUpTest.ts
├── tsconfig.json
└── webpack.config.js
└── server
├── .env.sample
├── .eslintrc.js
├── .prettierrc
├── ormconfig.js
├── package-lock.json
├── package.json
├── public
└── icon.png
├── src
├── app.ts
├── controllers
│ ├── auth-controller.ts
│ └── favorite-controller.ts
├── entity
│ ├── Favorite.ts
│ └── User.ts
├── repository
│ ├── favorite-repository.ts
│ └── user-repository.ts
├── router
│ ├── auth-router.ts
│ ├── favorite-router.ts
│ └── index.ts
├── service
│ ├── auth-service.ts
│ ├── favorite-service.ts
│ └── user-service.ts
├── types
│ └── express
│ │ └── index.d.ts
├── utils
│ ├── constant.ts
│ └── validator.ts
└── views
│ ├── index.pug
│ └── layout.pug
└── tsconfig.json
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 예상 화면
2 |
3 | ## 기능
4 |
5 | - 기능1
6 |
7 | ## 요구사항
8 |
9 | - 상세1
10 | - 상세2
11 | - 상세3
12 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # 00 - 구현한 내용
2 |
3 | ## 관련 이슈
4 |
5 | ## 구현한 내용
6 |
7 | ## 실행 화면
8 |
9 | ## 논의할 사항
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 부스트캠프 2020
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
--------------------------------------------------------------------------------
/client/.env.sample:
--------------------------------------------------------------------------------
1 | Client_ID=
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "eslint-config-prettier",
10 | "plugin:react/recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "tsx": true
17 | },
18 | "ecmaVersion": 12,
19 | "sourceType": "module"
20 | },
21 | "plugins": ["react", "@typescript-eslint", "prettier"],
22 | "rules": {
23 | "prettier/prettier": [
24 | "error",
25 | {
26 | "endOfLine": "auto"
27 | }
28 | ],
29 | "@typescript-eslint/no-explicit-any": "off"
30 | },
31 | "overrides": [
32 | {
33 | "files": ["*.js"],
34 | "rules": {
35 | "@typescript-eslint/no-var-requires": "off"
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "tabWidth": 2,
4 | "trailingComma": "es5",
5 | "bracketSpacing": true,
6 | "printWidth": 100
7 | }
8 |
--------------------------------------------------------------------------------
/client/public/font/Lato-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/Lato-Bold.ttf
--------------------------------------------------------------------------------
/client/public/font/Lato-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/Lato-BoldItalic.ttf
--------------------------------------------------------------------------------
/client/public/font/Lato-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/Lato-Italic.ttf
--------------------------------------------------------------------------------
/client/public/font/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/Lato-Regular.ttf
--------------------------------------------------------------------------------
/client/public/font/NotoSansKR-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/NotoSansKR-Regular.otf
--------------------------------------------------------------------------------
/client/public/font/brand-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/brand-icons.eot
--------------------------------------------------------------------------------
/client/public/font/brand-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/brand-icons.ttf
--------------------------------------------------------------------------------
/client/public/font/brand-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/brand-icons.woff
--------------------------------------------------------------------------------
/client/public/font/brand-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/brand-icons.woff2
--------------------------------------------------------------------------------
/client/public/font/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/icons.eot
--------------------------------------------------------------------------------
/client/public/font/icons.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/icons.otf
--------------------------------------------------------------------------------
/client/public/font/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/icons.ttf
--------------------------------------------------------------------------------
/client/public/font/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/icons.woff
--------------------------------------------------------------------------------
/client/public/font/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/icons.woff2
--------------------------------------------------------------------------------
/client/public/font/outline-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/outline-icons.eot
--------------------------------------------------------------------------------
/client/public/font/outline-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/outline-icons.ttf
--------------------------------------------------------------------------------
/client/public/font/outline-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/outline-icons.woff
--------------------------------------------------------------------------------
/client/public/font/outline-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/font/outline-icons.woff2
--------------------------------------------------------------------------------
/client/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/icon.png
--------------------------------------------------------------------------------
/client/public/image/1-btn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/1/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/1/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/1/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10-btn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/13.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/14.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/15.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/16.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/17.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/18.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/19.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/20.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/21.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/22.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/23.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/24.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/25.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/27.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/10/9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/11/8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/2-btn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/2/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/2/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/2/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/2/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3-btn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/3/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/4/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/4/13.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/4/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/5/9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6-btn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/13.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/14.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/17.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/6/9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/7/21.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8-btn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/13.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/14.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/15.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/16.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/17.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/18.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/19.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/20.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/21.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/22.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/23.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/24.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/25.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/26.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/8/9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/9/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/image/chef.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/chef.gif
--------------------------------------------------------------------------------
/client/public/image/formula.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/formula.gif
--------------------------------------------------------------------------------
/client/public/image/giphy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/giphy.gif
--------------------------------------------------------------------------------
/client/public/image/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/loading.gif
--------------------------------------------------------------------------------
/client/public/image/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/login.png
--------------------------------------------------------------------------------
/client/public/image/login2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/login2.png
--------------------------------------------------------------------------------
/client/public/image/login3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/login3.png
--------------------------------------------------------------------------------
/client/public/image/login4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/login4.png
--------------------------------------------------------------------------------
/client/public/image/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/logo.png
--------------------------------------------------------------------------------
/client/public/image/logout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/logout.png
--------------------------------------------------------------------------------
/client/public/image/logout2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/client/public/image/logout2.png
--------------------------------------------------------------------------------
/client/public/image/math.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 | 수식 셰프
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/client/src/__tests__/AlertItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react';
3 | import '@testing-library/jest-dom';
4 | import AlertItem from '@ingredients/AlertItem';
5 | import { AlertMessage } from '@constants/constants';
6 |
7 | describe('AlertItem TEST', () => {
8 | test('AlertItem Rendering TEST', () => {
9 | const { container } = render(
10 |
11 | );
12 |
13 | expect(container).toHaveTextContent(AlertMessage.NEED_LOGIN_MESSAGE);
14 | expect(container).not.toBeEmptyDOMElement();
15 | });
16 |
17 | test('AlertItem > MessageBox Style TEST', () => {
18 | const { container } = render(
19 |
20 | );
21 | const messageBox = container.querySelector('.globalFont');
22 |
23 | expect(messageBox).toHaveStyle({
24 | textAlign: 'center',
25 | fontSize: '20px',
26 | fontWeight: 'bold',
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/client/src/__tests__/ButtomItem.test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import * as React from 'react';
3 | import { render } from '@testing-library/react';
4 | import '@testing-library/jest-dom';
5 | import ButtonItem from '@ingredients/ButtonItem';
6 |
7 | describe('Buttomitem TEST', () => {
8 | test('Buttomitem Rendering TEST ', () => {
9 | const { container } = render( {}} />);
10 |
11 | expect(container).toHaveTextContent('제목');
12 | expect(container).not.toBeEmptyDOMElement();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/client/src/__tests__/DictionaryItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '../lib/utils/test-util';
3 | import '@testing-library/jest-dom';
4 | import DictionaryItem from '@ingredients/DictionaryItem';
5 |
6 | describe('DictionaryItem TEST', () => {
7 | test('DictionaryItem Rendering 테스트 ', () => {
8 | const { container } = render(
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | const dictionaryItem = container.querySelector('td');
18 | const stataicMathField = container.querySelector('span');
19 |
20 | expect(dictionaryItem).not.toBeEmptyDOMElement();
21 | expect(stataicMathField).toBeInTheDocument();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/client/src/__tests__/DictionaryMenuItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react';
3 | import '@testing-library/jest-dom';
4 | import DictionaryMenuItem from '@ingredients/DictionaryMenuItem';
5 |
6 | describe('DictionaryMenuItem TEST', () => {
7 | test('DictionaryMenuItem Rendering TEST ', () => {
8 | const { container } = render();
9 | const dictionaryMenuItem = container.querySelector('option');
10 |
11 | expect(dictionaryMenuItem).toHaveTextContent('옵션제목');
12 | expect(dictionaryMenuItem).not.toHaveAttribute('selected', true);
13 | expect(dictionaryMenuItem).toHaveStyle({
14 | fontSize: '12px',
15 | height: '15px',
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/client/src/__tests__/FontColorMenu.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@utils/test-util';
3 | import FontColorMenu from '@ingredients/FontColorMenu';
4 | import '@testing-library/jest-dom';
5 |
6 | test(' Test', () => {
7 | const { container } = render();
8 |
9 | expect(container).toHaveStyle(`
10 | background-color: '#000000',
11 | `);
12 | });
13 |
--------------------------------------------------------------------------------
/client/src/__tests__/FontSizeMenu.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@utils/test-util';
3 | import FontSizeMenu from '@ingredients/FontSizeMenu';
4 | import { FONT_SIZE_LISTS } from '@constants/constants';
5 | import '@testing-library/jest-dom';
6 |
7 | test(' Test', () => {
8 | const toggleSzieMenu = () => {};
9 | const { container } = render();
10 |
11 | FONT_SIZE_LISTS.forEach(({ size, checked }) => {
12 | expect(container).toHaveTextContent(size);
13 | if (size === '15') {
14 | expect(checked).toBeTruthy();
15 | } else {
16 | expect(checked).toBeFalsy();
17 | }
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/client/src/__tests__/IconMessage.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { getByTestId, render, screen, waitFor } from '@testing-library/react';
3 | import IconMessage from '@ingredients/IconMessage/index';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('', () => {
7 | it('renders IconMessage Component', () => {
8 | const downloadImage = () => {};
9 | const { container } = render(
10 |
16 | );
17 | const test = screen.getByTestId('icon-message');
18 | expect(test).toHaveClass('css-0');
19 | expect(test).toBeVisible();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/client/src/__tests__/Loading.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen, waitFor } from '@testing-library/react';
3 | import Loading from '../components/Ingredients/Loading';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('Loading TEST', () => {
7 | test('Loading big 테스트 ', () => {
8 | const { container } = render();
9 |
10 | expect(container).toHaveTextContent('수식 셰프');
11 | expect(document.querySelector('.pencil')).toBeInTheDocument();
12 | expect(document.querySelector('.pencil')).toHaveStyle({
13 | width: '180px',
14 | });
15 | expect(document.querySelector('.top')).toBeInTheDocument();
16 | expect(document.querySelector('.stroke_big')).toBeInTheDocument();
17 | });
18 |
19 | test('Loading mini 테스트 ', () => {
20 | const { container } = render();
21 |
22 | expect(container).toHaveTextContent('수식 셰프');
23 | expect(document.querySelector('.pencil')).toBeInTheDocument();
24 | expect(document.querySelector('.pencil')).toHaveStyle({
25 | width: '100px',
26 | });
27 | expect(document.querySelector('.top')).toBeInTheDocument();
28 | expect(document.querySelector('.stroke_mini')).toBeInTheDocument();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/client/src/__tests__/MessageItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen, waitFor } from '@testing-library/react';
3 | import MessageItem from '../components/Ingredients/MessageItem';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('MessageItem TEST', () => {
7 | test('MessageItem title 테스트 ', () => {
8 | const { container } = render();
9 |
10 | expect(container).toHaveTextContent('MessageItem');
11 | });
12 |
13 | test('MessageItem children 테스트 ', () => {
14 | const { container } = render(
15 |
16 | 테스트 중
17 |
18 | );
19 |
20 | expect(container).toHaveTextContent('MessageItem');
21 | expect(document.querySelector('div')).toBeInTheDocument();
22 | expect(container).toHaveTextContent('테스트 중');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/client/src/__tests__/PageItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '../lib/utils/test-util';
3 | import PageTabItem from '../components/Ingredients/PageTabItem';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('PageItem TEST', () => {
7 | const testInfo = {
8 | id: 0,
9 | latex: '\\frac',
10 | fontSize: '16',
11 | fontColor: 'black',
12 | textAlign: 'center',
13 | };
14 |
15 | test('PageItem title 테스트 ', () => {
16 | const { container } = render();
17 |
18 | expect(container).toHaveTextContent('수식 4');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/client/src/__tests__/QrCode.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '../lib/utils/test-util';
3 | import QrCode from '../components/Ingredients/QrCode/index';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('', () => {
7 | it('renders QrCode', () => {
8 | const { container } = render();
9 | expect(container).toMatchSnapshot();
10 | });
11 | it('in canvers', () => {
12 | const { container } = render();
13 | expect(container.querySelector('canvas')).toBeInTheDocument;
14 | expect(container.querySelector('canvas')).toHaveStyle({
15 | width: '128px',
16 | height: '128px',
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/client/src/__tests__/TableItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '../lib/utils/test-util';
3 | import TableItem from '../components/Ingredients/TableItem/index';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('', () => {
7 | const data = [{ id: 1, latex: '1+1', title: '나의 latex' }];
8 | it('renders TableItem', () => {
9 | const { container } = render(
10 |
11 | );
12 | expect(container).toMatchSnapshot();
13 | });
14 | it('in Table', () => {
15 | const { container, getByText } = render(
16 |
17 | );
18 | expect(container.querySelector('table')).toBeInTheDocument;
19 | const header = getByText('title');
20 | expect(header).toBeInTheDocument;
21 | const content = getByText('latex');
22 | expect(content).toBeInTheDocument;
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/client/src/__tests__/TextAreaItem.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '../lib/utils/test-util';
3 | import TextAreaItem from '../components/Ingredients/TextAreaItem/index';
4 | import '@testing-library/jest-dom';
5 |
6 | describe('', () => {
7 | it('renders TextAreaItem', () => {
8 | const { container } = render();
9 | expect(container).toMatchSnapshot();
10 | });
11 | it('in TextAreaItem', () => {
12 | const { container } = render();
13 | expect(container.querySelector('textarea')).toBeInTheDocument;
14 | expect(container.querySelector('textarea')).toHaveStyle({
15 | height: '100%',
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/client/src/__tests__/__snapshots__/QrCode.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders QrCode 1`] = `
4 |
15 | `;
16 |
--------------------------------------------------------------------------------
/client/src/__tests__/__snapshots__/TableItem.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders TableItem 1`] = `
4 |
5 |
8 |
11 |
14 |
17 | title
18 | |
19 |
22 | latex
23 | |
24 |
25 |
26 |
29 |
32 |
35 |
38 | 나의 latex
39 |
40 | |
41 |
44 |
47 |
50 | 1+1
51 |
52 |
61 |
62 | |
63 |
64 |
65 |
66 |
67 | `;
68 |
--------------------------------------------------------------------------------
/client/src/__tests__/__snapshots__/TextAreaItem.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders TextAreaItem 1`] = `
4 |
13 | `;
14 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/AlertItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './style';
3 | import { Icon } from 'semantic-ui-react';
4 | import { AlertMessage } from '@constants/constants';
5 | interface AlertItemProps {
6 | icon: any;
7 | message: AlertMessage;
8 | }
9 |
10 | function AlertItem({ icon, message }: AlertItemProps) {
11 | return (
12 |
13 |
14 |
15 |
16 | {message}
17 |
18 | );
19 | }
20 |
21 | export default AlertItem;
22 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/AlertItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const AlertItemContainer = styled.div`
4 | box-sizing: border-box;
5 | height: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: center;
9 | `;
10 |
11 | export const IconBox = styled.div`
12 | text-align: center;
13 | margin-bottom: 15px;
14 | color: #d3d3d3;
15 | `;
16 |
17 | export const MessageBox = styled.div`
18 | text-align: center;
19 | font-size: 20px;
20 | font-weight: bold;
21 | `;
22 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/ButtonItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Icon } from 'semantic-ui-react';
3 | interface ButtonItemProps {
4 | title?: string;
5 | icon?: any;
6 | size?: 'big' | 'small' | 'mini' | 'tiny' | 'medium' | 'large' | 'huge' | 'massive' | undefined;
7 | handler: () => void;
8 | color?: any;
9 | }
10 |
11 | function ButtonItem({ title, icon, size, handler, color }: ButtonItemProps) {
12 | return (
13 |
17 | );
18 | }
19 |
20 | export default ButtonItem;
21 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/CalculatorButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { calculatorButtons } from '@constants/calculator';
3 | import { Button } from 'semantic-ui-react';
4 | import * as S from './style';
5 |
6 | interface CalulatorButtonProps {
7 | row: number;
8 | col?: number;
9 | }
10 |
11 | function CalculatorButton({ row, col = 8 }: CalulatorButtonProps) {
12 | return (
13 |
14 | {calculatorButtons.slice(row * col, (row + 1) * col).map((button, index) => (
15 |
18 | ))}
19 |
20 | );
21 | }
22 |
23 | export default React.memo(CalculatorButton);
24 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/CalculatorButton/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const ButtonRow = styled.div`
4 | display: flex;
5 | `;
6 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/CalculatorSymbol/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'semantic-ui-react';
3 |
4 | interface CalculatorSymbolProps {
5 | name: string;
6 | symbol: string | number;
7 | }
8 |
9 | function CalulatorSymbol({ name, symbol }: CalculatorSymbolProps) {
10 | return ;
11 | }
12 |
13 | export default React.memo(CalulatorSymbol);
14 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/DictionaryItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StaticMathField } from 'boost-mathquill';
3 | import { useSelector } from 'react-redux';
4 | import { RootState } from '@contexts/index';
5 | import { Table } from 'semantic-ui-react';
6 |
7 | interface DictionaryItemProps {
8 | latex: string;
9 | }
10 |
11 | function DictionaryItem({ latex }: DictionaryItemProps) {
12 | const { mathfield } = useSelector((state: RootState) => state.latex);
13 |
14 | const onClick = () => {
15 | mathfield?.write(latex);
16 | };
17 |
18 | return (
19 |
20 | {latex}
21 |
22 | );
23 | }
24 |
25 | export default DictionaryItem;
26 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/DictionaryMenuItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './style';
3 |
4 | interface DictionaryMenuItemProps {
5 | title: string;
6 | }
7 |
8 | function DictionaryMenuItem({ title }: DictionaryMenuItemProps) {
9 | return {title};
10 | }
11 |
12 | export default React.memo(DictionaryMenuItem);
13 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/DictionaryMenuItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const DictionaryMenuItem = styled.option`
4 | font-size: 12px;
5 | height: 15px;
6 | &:hover {
7 | background-color: grey;
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/DropDownItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { getImageURL } from '@utils/util';
3 |
4 | interface DropDownItemStyleProps {
5 | header?: string;
6 | }
7 |
8 | export const DropDownItemStyle = styled.button`
9 | background: url(${(props) => getImageURL(props.header)});
10 | width: 60px;
11 | height: 60px;
12 | padding-bottom: 6px;
13 | font-size: 2rem;
14 | font-weight: bold;
15 | background-position: center;
16 | cursor: pointer;
17 | border: 4px solid transparent;
18 | background-repeat: no-repeat;
19 | filter: invert(28%) sepia(9%) saturate(10%) hue-rotate(1deg) brightness(91%) contrast(83%);
20 | outline: 0;
21 | &:focus {
22 | border: 4px dashed #6d9eeb;
23 | }
24 | &:hover {
25 | border: 4px dashed #6d9eeb;
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/FontColorMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import useFontColorMenu from './useFontColorMenu';
3 | import * as S from './style';
4 |
5 | function FontColorMenu() {
6 | const { currentTabInfo, handleChangeComplete } = useFontColorMenu();
7 | const onChangeHandeler = useCallback((e) => handleChangeComplete(e.target.value), []);
8 | return (
9 | <>
10 |
17 | >
18 | );
19 | }
20 |
21 | export default React.memo(FontColorMenu);
22 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/FontColorMenu/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const FontColorPicker = styled.input`
4 | opacity: 0;
5 | position: absolute;
6 | left: 0;
7 | top: 0;
8 | width: 100%;
9 | height: 100%;
10 | z-index: 9999;
11 |
12 | &:hover {
13 | cursor: pointer;
14 | }
15 | `;
16 |
17 | export const FontColorDiv = styled.div`
18 | position: fixed;
19 | width: 100%;
20 | height: 100%;
21 | top: 0;
22 | left: 0;
23 | background-color: red;
24 | z-index: 9999;
25 | `;
26 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/FontColorMenu/useFontColorMenu.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { editLatex } from '@contexts/latex';
3 | import useCurrentTab from '@hooks/useCurrentTab';
4 |
5 | const useFontColorMenu = () => {
6 | const { currentTabInfo } = useCurrentTab();
7 | const dispatch = useDispatch();
8 |
9 | const handleChangeComplete = (fontColor: string) => {
10 | dispatch(editLatex({ fontColor: fontColor }));
11 | };
12 | return {
13 | currentTabInfo,
14 | handleChangeComplete,
15 | };
16 | };
17 |
18 | export default useFontColorMenu;
19 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/FontSizeMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { SetStateAction, useCallback } from 'react';
2 | import { Icon } from 'semantic-ui-react';
3 | import { FONT_SIZE_LISTS } from '@constants/constants';
4 | import useFontSizeMenu from './useFontSizeMenu';
5 | import * as S from './style';
6 |
7 | export interface FontSizeListType {
8 | size: string;
9 | checked: boolean;
10 | }
11 | export interface FontSizeMenuProps {
12 | toggleSizeMenu: () => void;
13 | }
14 |
15 | function FontSizeMenu({ toggleSizeMenu }: FontSizeMenuProps) {
16 | const { clickHandler, menuRef } = useFontSizeMenu({ toggleSizeMenu });
17 | const changeSizeHandler = useCallback(
18 | (index: number, size: string) => () => {
19 | clickHandler(index, size);
20 | },
21 | []
22 | );
23 |
24 | return (
25 |
26 | {FONT_SIZE_LISTS.map((sizeList: FontSizeListType, index: number) => {
27 | return (
28 |
33 | {sizeList.size}
34 | {sizeList.checked && }
35 |
36 | );
37 | })}
38 |
39 | );
40 | }
41 |
42 | export default React.memo(FontSizeMenu);
43 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/FontSizeMenu/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const FontContainer = styled.div`
4 | margin: 0;
5 | padding: 0;
6 | border: 1px solid rgb(212, 212, 213);
7 | border-top: none;
8 | position: absolute;
9 | width: 78px;
10 | margin-top: 37px;
11 | background: white;
12 | z-index: 99999;
13 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
14 | `;
15 |
16 | export const FontWapper = styled.div`
17 | display: flex;
18 | justify-content: space-between;
19 | color: grey;
20 | font-size: 14px;
21 | padding: 5px 2px 5px 7px;
22 | cursor: pointer;
23 | &:hover {
24 | color: #00cc00;
25 | }
26 | `;
27 |
28 | export const FontSizeText = styled.span`
29 | background: white;
30 | padding-left: 5px;
31 | `;
32 |
33 | export const FontSizeClicked = styled.span`
34 | background: white;
35 | padding-right: 5px;
36 | color: green;
37 | `;
38 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/FontSizeMenu/useFontSizeMenu.ts:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useCallback } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { editLatex } from '@contexts/latex';
4 | import { FONT_SIZE_LISTS } from '@constants/constants';
5 | import { FontSizeListType, FontSizeMenuProps } from './index';
6 |
7 | const useFontSizeMenu = ({ toggleSizeMenu }: FontSizeMenuProps) => {
8 | const dispatch = useDispatch();
9 | const menuRef = useRef(null);
10 | const [stateList, setStateList] = useState(FONT_SIZE_LISTS);
11 |
12 | const clickHandler = useCallback(
13 | (clickIndex: number, fontSize: string) => {
14 | const newStateList = stateList.map((state, index) => {
15 | state.checked = clickIndex === index ? true : false;
16 | return state;
17 | });
18 |
19 | dispatch(editLatex({ fontSize }));
20 | toggleSizeMenu();
21 | setStateList(newStateList);
22 | },
23 | [stateList]
24 | );
25 |
26 | return {
27 | clickHandler,
28 | menuRef,
29 | };
30 | };
31 |
32 | export default useFontSizeMenu;
33 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/IconMessage/index.tsx:
--------------------------------------------------------------------------------
1 | import useToggle from '@hooks/useToggle';
2 | import React from 'react';
3 | import { Button } from 'semantic-ui-react';
4 | import * as S from './style';
5 | interface IconMessageProps {
6 | title: string;
7 | iconName: string;
8 | size: any;
9 | onClickHandler: () => void;
10 | }
11 |
12 | function IconMessage({ title, iconName, size, onClickHandler }: IconMessageProps) {
13 | const [message, , setToggleMessage] = useToggle(false);
14 | const onMouseHandler = () => {
15 | setToggleMessage(true);
16 | };
17 | const onMouseLeaveHandler = () => {
18 | setToggleMessage(false);
19 | };
20 | return (
21 |
22 |
29 |
30 | );
31 | }
32 |
33 | export default IconMessage;
34 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/IconMessage/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const IconMessageContainer = styled.div``;
4 |
5 | export const IconMessageWrapper = styled.div`
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | font-weight: bold;
10 | margin-bottom: 10px;
11 | position: fixed;
12 | bottom: 80px;
13 | height: auto;
14 | background-color: #f3f3f3;
15 | font-size: 20px !important;
16 | padding: 10px;
17 | border-radius: 0.5em;
18 | border: 1px solid #dddddd;
19 | :after {
20 | content: '';
21 | display: block;
22 | position: absolute;
23 | width: 0;
24 | height: 0;
25 | left: 20px;
26 | top: 39px;
27 | right: auto;
28 | width: 20px;
29 | height: 20px;
30 | background: #f3f3f3;
31 | border-bottom: 1px solid #dddddd;
32 | border-left: 1px solid #dddddd;
33 | -moz-transform: rotate(-45deg);
34 | -webkit-transform: rotate(-45deg);
35 | }
36 | `;
37 |
38 | export const IconMessageTitle = styled.span`
39 | color: lightslategrey;
40 | `;
41 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './style';
3 |
4 | export interface LoadingProps {
5 | size: 'mini' | 'big';
6 | }
7 |
8 | const Loading = ({ size }: LoadingProps) => {
9 | return (
10 |
11 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Loading;
21 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/MessageItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Message } from 'semantic-ui-react';
3 | import * as S from './style';
4 |
5 | interface MessageItemProps {
6 | title: string;
7 | children?: string | React.ReactChild;
8 | positive: boolean;
9 | }
10 |
11 | function MessageItem({ title, children, positive }: MessageItemProps) {
12 | return (
13 |
14 |
15 | {title}
16 | {children}
17 |
18 |
19 | );
20 | }
21 |
22 | export default MessageItem;
23 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/MessageItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const MessageContainer = styled.div`
4 | position: fixed;
5 | margin: 0 auto;
6 | left: 0;
7 | right: 0;
8 | width: 50%;
9 | `;
10 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/PageTabItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TabInfo } from '@contexts/latex';
3 | import useMenuItem from './usePageTabItem';
4 | import { Label, Menu } from 'semantic-ui-react';
5 |
6 | interface MenuItemProps {
7 | item: TabInfo;
8 | isCurrentTab: boolean;
9 | index: number;
10 | }
11 |
12 | function PageTabItem({ item, isCurrentTab, index }: MenuItemProps) {
13 | const { handleItemClick, onRemoveTab } = useMenuItem({ item });
14 |
15 | return (
16 |
17 |
20 | 수식 {index + 1}
21 |
22 | );
23 | }
24 |
25 | export default React.memo(PageTabItem);
26 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/PageTabItem/usePageTabItem.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { changeTab, removeTab, TabInfo } from '../../../contexts/latex';
4 |
5 | const usePageTabItem = ({ item }: { item: TabInfo }) => {
6 | const dispatch = useDispatch();
7 |
8 | const handleItemClick = useCallback(
9 | (id: number) => () => {
10 | dispatch(changeTab(id));
11 | },
12 | []
13 | );
14 |
15 | const removeTabHandler = useCallback((id: number) => {
16 | dispatch(removeTab(id));
17 | }, []);
18 |
19 | const onRemoveTab = useCallback(
20 | (e: React.MouseEvent) => {
21 | e.stopPropagation();
22 | removeTabHandler(item.id);
23 | },
24 | [item]
25 | );
26 |
27 | return {
28 | handleItemClick,
29 | onRemoveTab,
30 | };
31 | };
32 |
33 | export default usePageTabItem;
34 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/QrCode/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QRcode from 'qrcode.react';
3 | import { Dimmer, Loader } from 'semantic-ui-react';
4 | import * as S from './style';
5 |
6 | interface QrCodeProps {
7 | imageUrl: string;
8 | }
9 |
10 | function QrCode({ imageUrl }: QrCodeProps) {
11 | return (
12 |
13 |
14 | {!imageUrl && (
15 |
16 | Loading
17 |
18 | )}
19 |
20 | );
21 | }
22 |
23 | export default QrCode;
24 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/QrCode/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const QrCodeContainer = styled.div`
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | `;
8 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/RecommendItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { LatexContent } from '@constants/latex-header';
3 | import { getImageURL } from '@utils/util';
4 | import { useDispatch } from 'react-redux';
5 | import { editLatex } from '@contexts/latex';
6 | import * as S from './style';
7 | interface RecmmendItem extends LatexContent {
8 | currentLatex: string;
9 | clearAndCloseRecommend: () => void;
10 | }
11 |
12 | function RecommendItem({ latex, image, currentLatex, clearAndCloseRecommend }: RecmmendItem) {
13 | const dispatch = useDispatch();
14 |
15 | const onClick = () => {
16 | const lastIndex = currentLatex.lastIndexOf('@');
17 | const nextLatex = currentLatex.substring(0, lastIndex);
18 | dispatch(editLatex({ latex: nextLatex + latex }));
19 | clearAndCloseRecommend();
20 | };
21 |
22 | return (
23 |
24 |
25 | {latex}
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default RecommendItem;
35 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/RecommendItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const RecommendWrapper = styled.li`
4 | width: 100%;
5 | height: 40px;
6 | display: flex;
7 | justify-content: space-around;
8 | align-items: center;
9 | cursor: pointer;
10 | padding-left: 5px;
11 | border-top: 1px dashed #a6a6a6;
12 | `;
13 |
14 | export const RecommendLatexWrapper = styled.div`
15 | width: 40%;
16 | height: 100%;
17 | text-align: left;
18 | & span {
19 | overflow: hidden;
20 | text-overflow: ellipsis;
21 | }
22 | white-space: nowrap;
23 | display: flex;
24 | align-items: center;
25 | `;
26 |
27 | export const RecommendImageWrapper = styled.div`
28 | width: 60%;
29 | height: 100%;
30 | overflow: hidden;
31 | & img {
32 | width: 100%;
33 | height: 100%;
34 | transform: scale(1.6);
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/TableItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const TableContainer = styled.div`
4 | display: flex;
5 | width: 100%;
6 | & > div {
7 | &:hover {
8 | cursor: pointer;
9 | }
10 | & .ui.mini.label {
11 | background: rgb(215, 111, 126) !important;
12 | color: white !important;
13 | }
14 | }
15 | `;
16 |
17 | export const TableWrapper = styled.div`
18 | margin-left: auto;
19 | `;
20 |
21 | export const TableSpan = styled.span`
22 | &:hover {
23 | cursor: pointer;
24 | font-weight: bold;
25 | font-size: 15px;
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/TableItem/useTableItem.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@contexts/index';
2 | import { useSelector } from 'react-redux';
3 |
4 | const useTableAreaItem = () => {
5 | const { mathfield } = useSelector((state: RootState) => state.latex);
6 |
7 | const onChangeHandler = (latex: string) => {
8 | mathfield?.write(latex);
9 | };
10 |
11 | return { onChangeHandler };
12 | };
13 |
14 | export default useTableAreaItem;
15 |
--------------------------------------------------------------------------------
/client/src/components/Ingredients/TextAreaItem/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const TextAreaContainer = styled.div`
4 | display: flex;
5 | width: 100%;
6 | height: 100%;
7 | border: 1px solid #bdbdbd;
8 | background-color: white;
9 | padding: 10px;
10 | position: relative;
11 | `;
12 | interface TextAreaProps {
13 | isShow: boolean;
14 | }
15 | export const TextArea = styled.textarea`
16 | width: ${(props) => (props.isShow ? '70%' : '100%')};
17 | height: 100%;
18 | border: none;
19 | resize: none;
20 | &:focus {
21 | outline: none;
22 | }
23 | `;
24 |
25 | export const RecommendContainer = styled.div`
26 | border: 1px solid #a6a6a6;
27 | width: 190px;
28 | height: 90%;
29 | position: absolute;
30 | top: 9px;
31 | right: 0;
32 | margin-right: 10px;
33 | border-radius: 5px;
34 | background: white;
35 | overflow: hidden;
36 | text-align: center;
37 | & > span {
38 | font-weight: bold;
39 | font-size: 1.2rem;
40 | }
41 | & ul {
42 | width: 100%;
43 | height: 90%;
44 | display: flex;
45 | flex-direction: column;
46 | list-style: none;
47 | padding: 0.5rem;
48 | margin: 0;
49 | overflow-y: auto;
50 | }
51 | `;
52 |
53 | export const Divider = styled.div`
54 | height: 100%;
55 | border-right: 1px solid #bdbdbd;
56 | `;
57 |
58 | export const Span = styled.div`
59 | margin-top: 5px;
60 | font-weight: bold;
61 | font-size: 15px;
62 | `;
63 |
--------------------------------------------------------------------------------
/client/src/components/Meal/CalculatorTab/useCalculatorTab.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { RootState } from '@contexts/index';
3 | import { useRef, useState } from 'react';
4 | import { calculatorButtons, calculator } from '@constants/calculator';
5 |
6 | const useCalculatorTab = () => {
7 | const { mathfield } = useSelector((state: RootState) => state.latex);
8 | const [outputOperation, setOutputOperation] = useState('');
9 | const [outputResult, setOutputResult] = useState(0);
10 | const radRef = useRef(null);
11 | const degRef = useRef(null);
12 |
13 | const onClickHandler = (e: React.MouseEvent) => {
14 | const targetButton = e.target as HTMLButtonElement;
15 |
16 | calculatorButtons.forEach((button) => {
17 | if (targetButton && button.name === targetButton.id)
18 | calculator(button, radRef, degRef, setOutputOperation, setOutputResult);
19 | });
20 | };
21 |
22 | const onClickOutputHandler = () => {
23 | if (mathfield) {
24 | mathfield.write(outputResult.toString());
25 | }
26 | };
27 |
28 | return {
29 | radRef,
30 | degRef,
31 | outputOperation,
32 | outputResult,
33 | onClickHandler,
34 | onClickOutputHandler,
35 | };
36 | };
37 |
38 | export default useCalculatorTab;
39 |
--------------------------------------------------------------------------------
/client/src/components/Meal/DictionaryHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DictionaryMenuItem from '@ingredients/DictionaryMenuItem';
3 | import { DICTIONARY_MENU_TITLE } from '@constants/latex-dictionary';
4 | import * as S from './style';
5 |
6 | interface DictionaryHeaderProps {
7 | menuTitle: string;
8 | searchWord: string;
9 | onSelectHandler: (event: React.ChangeEvent) => void;
10 | onSearchHandler: (event: React.ChangeEvent) => void;
11 | }
12 |
13 | function DictionaryHeader({
14 | menuTitle,
15 | searchWord,
16 | onSelectHandler,
17 | onSearchHandler,
18 | }: DictionaryHeaderProps) {
19 | return (
20 |
21 |
22 | {Object.keys(DICTIONARY_MENU_TITLE).map((title, index) => (
23 |
24 | ))}
25 |
26 |
27 |
32 |
33 | );
34 | }
35 |
36 | export default DictionaryHeader;
37 |
--------------------------------------------------------------------------------
/client/src/components/Meal/DictionaryHeader/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const DictionaryHeader = styled.section`
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | `;
8 |
9 | export const DictionaryMenu = styled.select`
10 | height: 30px;
11 | color: #444;
12 | padding: 5px 10px;
13 | background: #f3f3f3;
14 | margin: 0;
15 | border: 1px solid #aaa;
16 | border-radius: 0.5em;
17 | box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.04);
18 | `;
19 |
20 | export const DictionarySearch = styled.input`
21 | width: 190px;
22 | height: 30px;
23 | color: #444;
24 | padding: 5px 10px;
25 | background: #f3f3f3;
26 | border: 1px solid #aaa;
27 | border-radius: 0.5em;
28 | box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.04);
29 | `;
30 |
--------------------------------------------------------------------------------
/client/src/components/Meal/DictionaryItemList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DictionaryItem from '@ingredients/DictionaryItem';
3 | import { MenuLatex } from '@constants/latex-dictionary';
4 | import { Table } from 'semantic-ui-react';
5 | import * as S from './style';
6 |
7 | interface DictionaryItemListProps {
8 | displayedContent: MenuLatex[];
9 | onScroll: (e: React.UIEvent) => void;
10 | containerRef: any;
11 | }
12 |
13 | function DictionaryItemList({ displayedContent, onScroll, containerRef }: DictionaryItemListProps) {
14 | return (
15 |
16 |
17 |
18 | {displayedContent.map((dictionary: MenuLatex, index: number) => (
19 |
20 |
21 |
22 | ))}
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default DictionaryItemList;
30 |
--------------------------------------------------------------------------------
/client/src/components/Meal/DictionaryItemList/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const DictionaryItemList = styled.div`
4 | margin-top: 13px;
5 | margin-right: 1px;
6 | overflow-y: auto;
7 | border: 1px solid rgba(34, 36, 38, 0.15);
8 | flex: 0px;
9 | border-radius: 7px;
10 | & td {
11 | padding: 7px 15px !important;
12 | }
13 |
14 | & table {
15 | border: none !important;
16 | }
17 |
18 | & tr:hover {
19 | cursor: pointer;
20 | }
21 |
22 | & span:hover {
23 | cursor: pointer;
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/client/src/components/Meal/DictionaryTab/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DictionaryHeader from '@meal/DictionaryHeader';
3 | import DictionaryItemList from '@meal/DictionaryItemList';
4 | import useDictionaryTab from './useDictionaryTab';
5 |
6 | import { Tab } from 'semantic-ui-react';
7 | import * as S from './style';
8 |
9 | function DictionaryTab() {
10 | const {
11 | onScroll,
12 | currentMenuContent,
13 | menuTitle,
14 | onSearchHandler,
15 | onSelectHandler,
16 | searchWord,
17 | searchedContent,
18 | containerRef,
19 | } = useDictionaryTab();
20 |
21 | return (
22 |
23 |
24 |
30 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default DictionaryTab;
41 |
--------------------------------------------------------------------------------
/client/src/components/Meal/DictionaryTab/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const DictionaryContainer = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | height: 100%;
7 | padding: 5px 5px 0 5px;
8 | `;
9 |
--------------------------------------------------------------------------------
/client/src/components/Meal/FavoriteModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ButtonItem from '@ingredients/ButtonItem';
3 | import useFavoriteModal from './useFavoriteModal';
4 | import * as S from './style';
5 |
6 | function FavoriteModal({ onToggle }: { onToggle: () => void }) {
7 | const {
8 | title,
9 | currentTabInfo,
10 | onChangeTitle,
11 | onClickModalClosed,
12 | onClickRegister,
13 | } = useFavoriteModal(onToggle);
14 |
15 | return (
16 | <>
17 | 즐겨찾기 등록
18 |
19 | Title
20 |
21 | Latex
22 | {currentTabInfo.latex}
23 |
24 |
25 |
26 |
27 |
28 | >
29 | );
30 | }
31 |
32 | export default FavoriteModal;
33 |
--------------------------------------------------------------------------------
/client/src/components/Meal/FavoriteModal/useFavoriteModal.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { createFavorites } from '@contexts/user';
4 | import { FavoriteItem } from '@contexts/user/types';
5 | import favoriteAPI from '@lib/apis/favorite';
6 | import useCurrentTab from '@hooks/useCurrentTab';
7 | import { RootState } from '@contexts/index';
8 | import useInput from '@hooks/useInput';
9 |
10 | const useFavoriteModal = (onToggle: () => void) => {
11 | const dispatch = useDispatch();
12 | const { currentTabInfo } = useCurrentTab();
13 | const { userInfo } = useSelector((state: RootState) => state.user);
14 | const { userId } = userInfo;
15 | const [title, onChangeTitle, clearTitle] = useInput('');
16 |
17 | const onClickModalClosed = useCallback(() => {
18 | onToggle();
19 | }, []);
20 |
21 | const onClickRegister = async (props: FavoriteItem, clearTitle: () => void) => {
22 | const newFavoriteItem = await favoriteAPI.createFavorite(props);
23 | const { id, title, latex } = newFavoriteItem.favorite;
24 | dispatch(createFavorites({ id, title, latex }));
25 | clearTitle();
26 | onToggle();
27 | };
28 |
29 | return {
30 | title,
31 | onChangeTitle,
32 | currentTabInfo,
33 | onClickModalClosed,
34 | onClickRegister: () =>
35 | onClickRegister({ userId, title, latex: currentTabInfo.latex }, clearTitle),
36 | };
37 | };
38 |
39 | export default useFavoriteModal;
40 |
--------------------------------------------------------------------------------
/client/src/components/Meal/FavoriteTab/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TableItem from '@ingredients/TableItem';
3 | import AlertItem from '@ingredients/AlertItem';
4 | import { NEED_LOGIN_ICON, NO_LIST_ICON, AlertMessage } from '@constants/constants';
5 | import Loading from '@ingredients/Loading';
6 | import useFavoriteTab from './useFavoriteTab';
7 | import { Tab } from 'semantic-ui-react';
8 | import * as S from './style';
9 |
10 | function FavoriteTab() {
11 | const { userId, userInfo, error, loading } = useFavoriteTab();
12 |
13 | if (loading)
14 | return (
15 |
16 |
17 |
18 | );
19 | if (error || !userInfo?.favoriteLists)
20 | return ;
21 | const { favoriteLists } = userInfo;
22 |
23 | return (
24 |
25 |
26 | {!userId ? (
27 |
28 | ) : !favoriteLists.length ? (
29 |
30 | ) : (
31 |
32 | )}
33 |
34 |
35 | );
36 | }
37 |
38 | export default FavoriteTab;
39 |
--------------------------------------------------------------------------------
/client/src/components/Meal/FavoriteTab/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const FavoriteContainer = styled.div`
4 | padding: 12px;
5 | flex: 0px;
6 | overflow-y: auto;
7 |
8 | & table {
9 | width: 100% !important;
10 |
11 | & thead tr th:first-of-type {
12 | width: 70px;
13 | }
14 | & tbody tr td:last-of-type {
15 | overflow-x: auto !important;
16 | }
17 |
18 | & .label {
19 | margin-left: 10px !important;
20 | }
21 | }
22 | `;
23 |
24 | export const LoadingContainer = styled.div`
25 | width: 100%;
26 | height: 100%;
27 | position: relative;
28 | `;
29 |
--------------------------------------------------------------------------------
/client/src/components/Meal/FavoriteTab/useFavoriteTab.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { getFavoritesThunk } from '@contexts/user';
4 | import { RootState } from '@contexts/index';
5 |
6 | function useFavoriteTab() {
7 | const dispatch = useDispatch();
8 | const { userInfo, loading, error } = useSelector((state: RootState) => state.user);
9 | const { userId } = userInfo;
10 |
11 | useEffect(() => {
12 | if (userId) {
13 | dispatch(getFavoritesThunk(userId));
14 | }
15 | }, [dispatch]);
16 |
17 | return {
18 | userInfo,
19 | loading,
20 | error,
21 | userId,
22 | };
23 | }
24 |
25 | export default useFavoriteTab;
26 |
--------------------------------------------------------------------------------
/client/src/components/Meal/FormatButtons/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const FormatButtonsContainer = styled.div`
4 | text-align: right;
5 | margin-top: 1px;
6 | padding-bottom: 5px;
7 | `;
8 |
9 | export const CololrDiv = styled.div`
10 | position: relative;
11 | display: inline-block;
12 | `;
13 |
--------------------------------------------------------------------------------
/client/src/components/Meal/InputContents/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tab } from 'semantic-ui-react';
3 | import TextAreaItem from '@ingredients/TextAreaItem';
4 | import FavoriteTab from '@meal/FavoriteTab';
5 | import DictionaryTab from '@meal/DictionaryTab';
6 | import CaculatorTab from '@meal/CalculatorTab';
7 | import * as S from './style';
8 |
9 | const panes = [
10 | {
11 | menuItem: 'Tex',
12 | render: function tabContent() {
13 | return (
14 |
15 |
16 |
17 | );
18 | },
19 | },
20 | {
21 | menuItem: '수식사전',
22 | render: function tabContent() {
23 | return ;
24 | },
25 | },
26 | {
27 | menuItem: '즐겨찾기',
28 | render: function tabContent() {
29 | return ;
30 | },
31 | },
32 | {
33 | menuItem: '계산기',
34 | render: function tabContent() {
35 | return ;
36 | },
37 | },
38 | ];
39 |
40 | function InputContents() {
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | export default InputContents;
49 |
--------------------------------------------------------------------------------
/client/src/components/Meal/InputContents/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const ContentsContainer = styled.div`
4 | width: 100%;
5 | height: 100%;
6 |
7 | & > div {
8 | height: 100%;
9 | display: flex;
10 | flex-direction: column;
11 | }
12 | & .tab {
13 | padding: 7px !important;
14 | height: 100%;
15 | }
16 | & > div > .active {
17 | display: flex !important;
18 | flex-direction: column;
19 | height: 100%;
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/client/src/components/Meal/MenuBarButtons/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconMessage from '@ingredients/IconMessage';
3 | import { Button } from 'semantic-ui-react';
4 | import * as S from './style';
5 |
6 | interface MenuBarSaveButtonsProps {
7 | title: string;
8 | iconName: string;
9 | size: string;
10 | onClickHandler: () => void;
11 | }
12 | function MenuBarButtons({ MenuBarSaveButtons }: any) {
13 | return (
14 |
15 |
16 | {MenuBarSaveButtons.map((Button: any, index: number) => {
17 | return (
18 |
25 | );
26 | })}
27 |
28 |
29 | );
30 | }
31 |
32 | export default MenuBarButtons;
33 |
--------------------------------------------------------------------------------
/client/src/components/Meal/MenuBarButtons/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const ButtonWrapper = styled.div`
4 | padding-top: 10px;
5 | & div > button {
6 | height: 100%;
7 | padding: 15px 10px 10px 10px !important;
8 | align-self: center;
9 | & i {
10 | font-size: 1.5em;
11 | }
12 | }
13 | & div > div {
14 | height: 60px;
15 | }
16 | & div > div > div {
17 | height: 50px;
18 | }
19 | `;
20 |
--------------------------------------------------------------------------------
/client/src/components/Meal/MenuBarInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TextAreaItem from '@ingredients/TextAreaItem';
3 | import { INPUT_TEXT } from '@constants/constants';
4 | import { Icon } from 'semantic-ui-react';
5 | import * as S from './style';
6 |
7 | function MenuBarInput() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | {INPUT_TEXT}
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default MenuBarInput;
22 |
--------------------------------------------------------------------------------
/client/src/components/Meal/MenuBarInput/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const TextAreaItemWrapper = styled.div`
4 | width: 35%;
5 | margin: 10px;
6 | display: flex;
7 | `;
8 |
9 | export const LabelWrapper = styled.div`
10 | display: flex;
11 | flex-direction: column;
12 | width: 70px;
13 | min-width: 50px;
14 | padding: 10px;
15 | text-align: center;
16 | font-weight: bold;
17 | border: 1px solid #d4d4d5;
18 | border-right: none;
19 | height: 100%;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | `;
24 |
25 | export const LabelIcon = styled.div`
26 | padding-left: 4px;
27 | `;
28 |
29 | export const LabelText = styled.div``;
30 |
--------------------------------------------------------------------------------
/client/src/components/Meal/MenuBarOutput/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import OutputFormulaBox from '@meal/OutputFormulaBox';
3 | import { OUTPUT_TEXT } from '@constants/constants';
4 | import { Icon } from 'semantic-ui-react';
5 | import * as S from './style';
6 |
7 | function MenuBarOutput() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | {OUTPUT_TEXT}
15 |
16 |
23 |
24 | );
25 | }
26 |
27 | export default MenuBarOutput;
28 |
--------------------------------------------------------------------------------
/client/src/components/Meal/MenuBarOutput/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const OutputWrapper = styled.div`
4 | width: 35%;
5 | margin: 10px;
6 | display: flex;
7 | `;
8 |
9 | export const LabelWrapper = styled.div`
10 | display: flex;
11 | flex-direction: column;
12 | width: 70px;
13 | min-width: 50px;
14 | padding: 10px;
15 | text-align: center;
16 | font-weight: bold;
17 | border: 1px solid #d4d4d5;
18 | border-right: none;
19 | height: 100%;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | `;
24 |
25 | export const LabelIcon = styled.div`
26 | padding-left: 4px;
27 | `;
28 |
29 | export const LabelText = styled.div``;
30 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputContents/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import OutputFormula from '@meal/OutputFormula';
3 | import FormatButtons from '@meal/FormatButtons';
4 | import * as S from './style';
5 |
6 | function OutputContents() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default OutputContents;
18 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputContents/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const ContentsContainer = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | width: 100%;
7 | height: 100%;
8 | `;
9 |
10 | export const ContentsHeader = styled.div`
11 | flex-basis: 50px;
12 | margin-top: -5px;
13 | `;
14 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputFormula/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useOutputFormula from './useOutputFormula';
3 | import * as S from './style';
4 | import useModal from '@hooks/useModal';
5 | import FavoriteModal from '@meal/FavoriteModal';
6 | import OutputFormulaBox from '@meal/OutputFormulaBox/index';
7 |
8 | export interface OutputFormulaProps {
9 | isMenuBar?: boolean;
10 | backgroundColor?: string;
11 | padding?: string;
12 | border?: string;
13 | width?: string;
14 | fontSize?: string;
15 | fontColor?: string;
16 | textAlign?: string;
17 | toggleModal?: () => void;
18 | }
19 |
20 | function OutputFormula({ padding, border }: OutputFormulaProps) {
21 | const { onClickHandler } = useOutputFormula();
22 |
23 | const [toggleModal, Modal] = useModal({});
24 |
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 | );
35 | }
36 |
37 | export default OutputFormula;
38 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputFormula/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { OutputFormulaProps } from './index';
3 |
4 | export const OutputFormulaWrapper = styled.div`
5 | border: ${(props) => props.border};
6 | border-radius: 0 5px 5px 5px;
7 | height: 100%;
8 | padding: ${(props) => (props.padding ? '10px' : '7px')};
9 | position: relative;
10 | display: flex;
11 | flex-direction: column;
12 | `;
13 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputFormula/useOutputFormula.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { RootState } from '../../../contexts';
3 |
4 | const useOutputFormula = () => {
5 | const { mathfield } = useSelector((state: RootState) => state.latex);
6 |
7 | const onClickHandler = () => {
8 | if (mathfield) mathfield.focus();
9 | };
10 |
11 | return {
12 | onClickHandler,
13 | };
14 | };
15 |
16 | export default useOutputFormula;
17 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputFormulaBox/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { OutputFormulaProps } from '@meal/OutputFormula/index';
3 |
4 | export const OutputFormulaBox = styled.div`
5 | width: ${(props) => props.width};
6 | position: relative;
7 | border: 1px solid #bdbdbd;
8 | height: 100%;
9 | padding: 10px;
10 | font-size: ${(props) => props.fontSize}px;
11 | color: ${(props) => props.fontColor};
12 | text-align: ${(props) => props.textAlign};
13 | background: ${(props) => props.backgroundColor && props.backgroundColor};
14 | overflow-x: auto;
15 | flex: 0px;
16 | `;
17 |
18 | export const OutputFormulaContent = styled.span`
19 | display: inline-block;
20 | `;
21 |
22 | export const StarButtonBox = styled.div`
23 | position: absolute;
24 | top: 0;
25 | right: 0;
26 | margin-top: 6px;
27 | margin-right: 1px;
28 | z-index: 2;
29 | font-size: 1rem !important;
30 | & i {
31 | color: #f7d100;
32 | cursor: pointer;
33 | }
34 | `;
35 |
--------------------------------------------------------------------------------
/client/src/components/Meal/OutputFormulaBox/useOutputFormulaBox.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { MathField } from 'boost-mathquill';
4 | import { editLatex, initLatex } from '@contexts/latex';
5 | import { RootState } from '@contexts/index';
6 | import useCurrentTab from '@hooks/useCurrentTab';
7 |
8 | const useOutputFormulaBox = () => {
9 | const dispatch = useDispatch();
10 | const { mathfield } = useSelector((state: RootState) => state.latex);
11 | const { currentTabInfo } = useCurrentTab();
12 | const mathfieldRef = useRef(null);
13 |
14 | const initmathInput = (mathField: MathField) => {
15 | dispatch(initLatex({ mathfield: mathField, mathfieldRef: mathfieldRef.current }));
16 | };
17 |
18 | const onChangeHandler = (mathField: MathField) => {
19 | dispatch(editLatex({ latex: mathField.latex() }));
20 | };
21 |
22 | const onKeyDownHandler = (e: any) => {
23 | if (e.key === 'Enter') {
24 | mathfield?.write('\\newline ');
25 | }
26 | };
27 |
28 | return {
29 | currentTabInfo,
30 | initmathInput,
31 | onChangeHandler,
32 | mathfieldRef,
33 | onKeyDownHandler,
34 | };
35 | };
36 |
37 | export default useOutputFormulaBox;
38 |
--------------------------------------------------------------------------------
/client/src/components/Meal/PageTab/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ButtonItem from '@ingredients/ButtonItem';
3 | import PageTabItem from '@ingredients/PageTabItem';
4 | import useTab from './usePageTab';
5 | import { TAB_LIMIT } from '@constants/constants';
6 | import { Menu } from 'semantic-ui-react';
7 | import { TabContainer } from './style';
8 |
9 | function PageTab() {
10 | const { addTabHandler, currentTab, totalLatex } = useTab();
11 |
12 | return (
13 |
14 |
28 | {totalLatex.length < TAB_LIMIT && (
29 |
30 | )}
31 |
32 | );
33 | }
34 |
35 | export default PageTab;
36 |
--------------------------------------------------------------------------------
/client/src/components/Meal/PageTab/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const TabContainer = styled.div`
4 | margin-top: 40px;
5 | display: flex;
6 | flex-direction: column;
7 | flex-shrink: 0;
8 | flex-basis: 110px;
9 | margin-right: 10px;
10 | align-items: center;
11 |
12 | & .menu {
13 | width: 100% !important;
14 | }
15 |
16 | & .ui.vertical.menu .item > .label {
17 | background: #d76f7e !important;
18 | }
19 |
20 | & button {
21 | background: #d76f7e !important;
22 | color: white !important;
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/client/src/components/Meal/PageTab/usePageTab.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { RootState } from '@contexts/index';
4 | import { addTab } from '@contexts/latex';
5 |
6 | const useTab = () => {
7 | const dispatch = useDispatch();
8 | const { currentTab, totalLatex } = useSelector((state: RootState) => state.latex);
9 |
10 | const addTabHandler = useCallback(() => {
11 | dispatch(addTab());
12 | }, []);
13 |
14 | return { currentTab, totalLatex, addTabHandler };
15 | };
16 |
17 | export default useTab;
18 |
--------------------------------------------------------------------------------
/client/src/components/Set/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'semantic-ui-react';
3 | import FormulaList from '@meal/FormulaList';
4 | import { getImageURL } from '@utils/util';
5 | import * as S from './style';
6 |
7 | interface HeaderProps {
8 | onToggle: () => void;
9 | }
10 |
11 | function Header({ onToggle }: HeaderProps) {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default Header;
28 |
--------------------------------------------------------------------------------
/client/src/components/Set/Header/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const HeaderContainer = styled.div`
4 | display: flex;
5 | border-top: 1px solid #e1e4e8;
6 | border-bottom: 1px solid #e1e4e8;
7 | background-color: #f6f8fa;
8 | width: 100%;
9 | height: 74px;
10 | align-items: center;
11 | position: relative;
12 | `;
13 |
14 | export const Logo = styled.div`
15 | height: 100%;
16 | width: 65px;
17 | margin-left: 15px;
18 | `;
19 |
20 | export const LogoImg = styled.img`
21 | margin-top: 5px;
22 | height: 90%;
23 | width: 65px;
24 | `;
25 |
26 | export const ButtonWrapper = styled.div`
27 | margin: 0px 20px;
28 | & div {
29 | height: 50px;
30 | }
31 | & div > button {
32 | height: 100%;
33 | padding: 15px 10px 10px 10px !important;
34 | align-self: center;
35 | & i {
36 | padding-top: 3px;
37 | font-size: 2em;
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/client/src/components/Set/Input/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PageTab from '@meal/PageTab';
3 | import InputContents from '@meal/InputContents';
4 | import * as S from './style';
5 |
6 | function InputArea() {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default InputArea;
16 |
--------------------------------------------------------------------------------
/client/src/components/Set/Input/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const InputAreaContainer = styled.div`
4 | height: 100%;
5 | display: flex;
6 | width: 57%;
7 | min-width: 530px;
8 | margin-right: 15px;
9 | `;
10 |
--------------------------------------------------------------------------------
/client/src/components/Set/Output/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import OutputContents from '@meal/OutputContents';
3 | import * as S from './style';
4 |
5 | function OutputArea() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default OutputArea;
14 |
--------------------------------------------------------------------------------
/client/src/components/Set/Output/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const OutputAreaContainer = styled.div`
4 | height: 100%;
5 | width: 43%;
6 | min-width: 330px;
7 | margin-left: 15px;
8 | `;
9 |
--------------------------------------------------------------------------------
/client/src/components/Set/ResizeHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MenuBar from '@meal/MenuBar';
3 | import * as S from './style';
4 |
5 | export interface ResizeHeaderProps {
6 | onToggle: () => void;
7 | }
8 |
9 | function ResizeHeader({ onToggle }: ResizeHeaderProps) {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default ResizeHeader;
18 |
--------------------------------------------------------------------------------
/client/src/components/Set/ResizeHeader/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const ResizeHeaderContainer = styled.div`
4 | height: 100%;
5 | width: 100%;
6 | animation: slidein 0.5s;
7 | @keyframes slidein {
8 | from {
9 | opacity: 0;
10 | scale: 0;
11 | }
12 | to {
13 | opacity: 1;
14 | scale: 1;
15 | }
16 | }
17 | `;
18 |
--------------------------------------------------------------------------------
/client/src/components/Set/SaveButtons/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const SaveButtonsContainer = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | margin-right: 7px;
8 | height: 100%;
9 | flex-basis: 135px;
10 | flex-shrink: 0;
11 | & > div {
12 | margin-top: auto !important;
13 | }
14 | padding-bottom: 10px;
15 | `;
16 |
17 | export const Img = styled.img`
18 | border: 0.1px solid #d4d4d5;
19 | border-radius: 5px;
20 | margin-top: 6px;
21 | width: 114px;
22 | height: 44px;
23 | &:hover {
24 | cursor: pointer;
25 | }
26 | `;
27 |
--------------------------------------------------------------------------------
/client/src/contexts/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | // import user from './user';
3 | import latex from './latex';
4 | import user from './user';
5 |
6 | const rootReducer = combineReducers({
7 | latex,
8 | user,
9 | });
10 |
11 | // 루트 리듀서를 내보내주세요.
12 | export default rootReducer;
13 |
14 | // 루트 리듀서의 반환값를 유추해줍니다
15 | // 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줍니다.
16 | export type RootState = ReturnType;
17 |
--------------------------------------------------------------------------------
/client/src/contexts/latex/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@utils/util';
2 | import { EditTabInfo, InitLatex } from './types';
3 |
4 | export const EDIT_LATEX = 'latex/EDIT';
5 | export const INIT_LATEX = 'latex/INIT';
6 | export const ADD_TAB = 'tab/ADD';
7 | export const REMOVE_TAB = 'tab/REMOVE';
8 | export const CHANGE_TAB = 'tab/CHANGE';
9 |
10 | export const editLatex = createAction(EDIT_LATEX);
11 | export const initLatex = createAction(INIT_LATEX);
12 | export const addTab = createAction(ADD_TAB);
13 | export const removeTab = createAction(REMOVE_TAB);
14 | export const changeTab = createAction(CHANGE_TAB);
15 |
--------------------------------------------------------------------------------
/client/src/contexts/latex/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions';
2 | export { default } from './reducer';
3 | export * from './types';
4 |
--------------------------------------------------------------------------------
/client/src/contexts/latex/types.ts:
--------------------------------------------------------------------------------
1 | import { MathField } from 'boost-mathquill';
2 | import { addTab, changeTab, editLatex, removeTab, initLatex } from './actions';
3 |
4 | export interface EditTabInfo {
5 | latex?: string;
6 | fontSize?: string;
7 | fontColor?: string;
8 | textAlign?: string;
9 | }
10 | export interface TabInfo {
11 | id: number;
12 | latex: string;
13 | fontSize: string;
14 | fontColor: string;
15 | textAlign: string;
16 | }
17 |
18 | export interface InitLatex {
19 | mathfield: MathField | null;
20 | mathfieldRef: HTMLDivElement | null;
21 | }
22 |
23 | export type LatexAction =
24 | | ReturnType
25 | | ReturnType
26 | | ReturnType
27 | | ReturnType
28 | | ReturnType;
29 |
30 | export interface LatexState {
31 | maxId: number;
32 | currentTab: number;
33 | totalLatex: TabInfo[];
34 | mathfield: MathField | null;
35 | mathfieldRef: HTMLDivElement | null;
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/contexts/user/actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@utils/util';
2 | import { AxiosError } from 'axios';
3 | import { UserDataType, FavoriteItem } from './types';
4 |
5 | export const USER_LOGIN = 'user/LOGIN';
6 | export const USER_LOGOUT = 'user/LOGOUT';
7 | export const GET_FAVORITES_REQUEST = 'favorites/request';
8 | export const GET_FAVORITES_SUCCESS = 'favorites/success';
9 | export const GET_FAVORITES_FAILURE = 'favorites/failure';
10 | export const CREATE_FAVORITES = 'favorites/create';
11 | export const DELETE_FAVORITES = 'favorites/delete';
12 |
13 | export const userLogin = createAction(USER_LOGIN);
14 | export const userLogout = createAction(USER_LOGOUT);
15 | export const createFavorites = createAction(
16 | CREATE_FAVORITES
17 | );
18 | export const deleteFavorites = createAction(DELETE_FAVORITES);
19 | export const getFavoritesRequest = createAction(
20 | GET_FAVORITES_REQUEST
21 | );
22 | export const getFavoritesSuccess = createAction(
23 | GET_FAVORITES_SUCCESS
24 | );
25 | export const getFavoritesFailure = createAction(
26 | GET_FAVORITES_FAILURE
27 | );
28 |
--------------------------------------------------------------------------------
/client/src/contexts/user/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions';
2 | export { default } from './reducer';
3 | export * from './types';
4 | export * from './thunk';
5 |
--------------------------------------------------------------------------------
/client/src/contexts/user/thunk.ts:
--------------------------------------------------------------------------------
1 | import { ThunkAction } from 'redux-thunk';
2 | import { RootState } from '@contexts/index';
3 | import { UserAction } from './types';
4 | import { getFavoritesRequest, getFavoritesSuccess, getFavoritesFailure } from './actions';
5 | import favoriteAPI from '@lib/apis/favorite';
6 |
7 | export function getFavoritesThunk(userId: number): ThunkAction {
8 | return async (dispatch) => {
9 | dispatch(getFavoritesRequest(userId));
10 | try {
11 | const favoriteLists = await favoriteAPI.getFavorites(userId);
12 | dispatch(
13 | getFavoritesSuccess({
14 | userId,
15 | favoriteLists: favoriteLists.favorites,
16 | })
17 | );
18 | } catch (error) {
19 | dispatch(getFavoritesFailure(error));
20 | }
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/contexts/user/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | userLogin,
3 | userLogout,
4 | getFavoritesRequest,
5 | getFavoritesSuccess,
6 | getFavoritesFailure,
7 | createFavorites,
8 | deleteFavorites,
9 | } from './actions';
10 |
11 | export type UserAction =
12 | | ReturnType
13 | | ReturnType
14 | | ReturnType
15 | | ReturnType
16 | | ReturnType
17 | | ReturnType
18 | | ReturnType;
19 |
20 | export interface FavoriteItem {
21 | userId?: number | null;
22 | id?: number;
23 | latex: string;
24 | title: string;
25 | }
26 |
27 | export interface UserState {
28 | loading: boolean;
29 | error: Error | null;
30 | userInfo: UserDataType;
31 | }
32 |
33 | export interface UserDataType {
34 | userId: number | null;
35 | favoriteLists: FavoriteItem[];
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/hooks/useCurrentTab.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@contexts/index';
2 | import { useMemo } from 'react';
3 | import { useSelector } from 'react-redux';
4 |
5 | function useCurrentTab() {
6 | const { currentTab, totalLatex } = useSelector((state: RootState) => state.latex);
7 | const currentTabInfo = useMemo(() => totalLatex.filter((latex) => latex.id === currentTab)[0], [
8 | currentTab,
9 | totalLatex,
10 | ]);
11 |
12 | return { currentTabInfo, currentTab, totalLatex };
13 | }
14 |
15 | export default useCurrentTab;
16 |
--------------------------------------------------------------------------------
/client/src/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 |
3 | function useDebounce(callback: (...params: T) => void, time: number) {
4 | const timer = useRef | null>(null);
5 | return (...params: T) => {
6 | if (timer.current) clearTimeout(timer.current);
7 |
8 | timer.current = setTimeout(() => {
9 | callback(...params);
10 | timer.current = null;
11 | }, time);
12 | };
13 | }
14 |
15 | export default useDebounce;
16 |
--------------------------------------------------------------------------------
/client/src/hooks/useInput.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 |
3 | const useInput = (
4 | initialValue: string
5 | ): [string, (event: React.ChangeEvent) => void, () => void] => {
6 | const [input, setInput] = useState(initialValue);
7 |
8 | const onChange = useCallback((event) => {
9 | // eslint-disable-next-line no-shadow
10 | setInput(event.target.value);
11 | }, []);
12 |
13 | const clearInput = useCallback((): void => {
14 | setInput('');
15 | }, []);
16 |
17 | return [input, onChange, clearInput];
18 | };
19 |
20 | export default useInput;
21 |
--------------------------------------------------------------------------------
/client/src/hooks/useSelect.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 |
3 | const useSelect = (
4 | initialValue: string
5 | ): [
6 | string,
7 | (event: React.ChangeEvent) => void,
8 | React.Dispatch>
9 | ] => {
10 | const [value, setValue] = useState(initialValue);
11 |
12 | const onChange = useCallback((event) => {
13 | // eslint-disable-next-line no-shadow
14 | setValue(event.target.value);
15 | }, []);
16 |
17 | return [value, onChange, setValue];
18 | };
19 |
20 | export default useSelect;
21 |
--------------------------------------------------------------------------------
/client/src/hooks/useThrottle.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 |
3 | function useThrottle(callback: (...params: T) => void, time: number) {
4 | const timer = useRef | null>(null);
5 |
6 | return (...params: T) => {
7 | if (!timer.current) {
8 | timer.current = setTimeout(() => {
9 | callback(...params);
10 | timer.current = null;
11 | }, time);
12 | }
13 | };
14 | }
15 |
16 | export default useThrottle;
17 |
--------------------------------------------------------------------------------
/client/src/hooks/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | const useToggle = (
4 | initialValue: boolean
5 | ): [boolean, () => void, React.Dispatch>] => {
6 | const [value, setValue] = useState(initialValue);
7 |
8 | const onToggle = useCallback(() => {
9 | // eslint-disable-next-line no-shadow
10 | setValue((value) => !value);
11 | }, []);
12 |
13 | return [value, onToggle, setValue];
14 | };
15 |
16 | export default useToggle;
17 |
--------------------------------------------------------------------------------
/client/src/lib/apis/common.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const BASE_URL = 'https://www.formulachef.tk/api';
4 | //export const BASE_URL = 'http://localhost:3000/api';
5 |
6 | export const API = axios.create({
7 | baseURL: BASE_URL,
8 | headers: {
9 | Accept: 'application/json',
10 | 'Content-Type': 'application/json',
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/client/src/lib/apis/favorite.ts:
--------------------------------------------------------------------------------
1 | import { API } from './common';
2 | import { FavoriteItem } from '@contexts/user/types';
3 | import { getToken } from '@utils/token';
4 |
5 | const favoriteAPI = {
6 | getFavorites: async (id: number) => {
7 | const token = await getToken();
8 | try {
9 | const response = await API.get(`/favorite/${id}`, {
10 | headers: { Authorization: token },
11 | });
12 | return response.data;
13 | } catch (error) {
14 | console.error(error);
15 | }
16 | },
17 | deleteFavorite: async (id: number) => {
18 | const token = await getToken();
19 | try {
20 | const response = await API.delete(`/favorite/${id}`, {
21 | headers: { Authorization: token },
22 | });
23 | return response.data;
24 | } catch (error) {
25 | console.error(error);
26 | }
27 | },
28 | createFavorite: async (body: FavoriteItem) => {
29 | const token = await getToken();
30 | try {
31 | const response = await API.post(`/favorite`, body, {
32 | headers: { Authorization: token },
33 | });
34 | return response.data;
35 | } catch (error) {
36 | console.error(error);
37 | }
38 | },
39 | };
40 |
41 | export default favoriteAPI;
42 |
--------------------------------------------------------------------------------
/client/src/lib/constants/constants.ts:
--------------------------------------------------------------------------------
1 | export const INPUT_TEXT = '입력';
2 | export const OUTPUT_TEXT = '출력';
3 |
4 | export const TAB_LIMIT = 4;
5 |
6 | export const FONT_SIZE_LISTS = [
7 | { size: '10', checked: false },
8 | { size: '15', checked: true },
9 | { size: '20', checked: false },
10 | { size: '25', checked: false },
11 | { size: '30', checked: false },
12 | ];
13 |
14 | export const NEED_LOGIN_ICON = 'dont';
15 |
16 | export const NO_LIST_ICON = 'warning';
17 |
18 | export enum AlertMessage {
19 | NEED_LOGIN_MESSAGE = '로그인이 필요합니다.',
20 | NO_LIST_MESSAGE = '항목이 없습니다.',
21 | }
22 |
23 | export enum LoginMessage {
24 | LOGIN_SUCCESS = '로그인에 성공하였습니다.',
25 | LOGIN_FAUILRE = '로그인에 실패하였습니다.',
26 | }
27 | export const MESSAGE_TIME = 2500;
28 | export const DISPLAY_INTERVAL = 6;
29 |
30 | export const CLIPBOARD_MESSAGE = '클립보드에 복사가 완료되었습니다.';
31 |
32 | export const DOWNLOAD_TEXTFILE_NAME = `수식셰프${Date.now()}.txt`;
33 | export const DOWNLOAD_IMAGEFILE_NAME = `수식셰프${Date.now()}.png`;
34 | export const DOWNLOAD_QRCODE_NAME = `QRCODE${Date.now()}.png`;
35 |
--------------------------------------------------------------------------------
/client/src/lib/utils/test-util.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render as rtlRender } from '@testing-library/react';
3 | import { createStore, Store } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import reducer, { RootState } from '../../contexts';
6 |
7 | function render(
8 | ui: React.ReactElement,
9 | {
10 | initialState = {
11 | latex: {
12 | maxId: 0,
13 | currentTab: 0,
14 | totalLatex: [
15 | {
16 | id: 0,
17 | latex: '',
18 | fontSize: '15',
19 | fontColor: '#000000',
20 | textAlign: 'left',
21 | },
22 | ],
23 | mathfield: null,
24 | mathfieldRef: null,
25 | },
26 | user: {
27 | loading: false,
28 | error: null,
29 | userInfo: {
30 | userId: null,
31 | favoriteLists: [],
32 | },
33 | },
34 | },
35 | store = createStore(reducer, initialState),
36 | }: { initialState?: RootState; store?: Store } = { initialState: undefined, store: undefined }
37 | ) {
38 | function Wrapper({ children }: { children?: React.ReactNode }) {
39 | return {children};
40 | }
41 | return rtlRender(ui, { wrapper: Wrapper });
42 | }
43 |
44 | // re-export everything
45 | export * from '@testing-library/react';
46 | // override render method
47 | export { render };
48 |
--------------------------------------------------------------------------------
/client/src/lib/utils/token.ts:
--------------------------------------------------------------------------------
1 | const USER_TOKEN = 'USER_TOKEN';
2 |
3 | export const setToken = (userToken: string): void => {
4 | if (chrome.storage) chrome.storage.sync.set({ USER_TOKEN: userToken }, function () {});
5 | };
6 |
7 | export const getToken = () => {
8 | return new Promise((resolve, reject) => {
9 | if (chrome.storage) {
10 | chrome.storage.sync.get([USER_TOKEN], function (result) {
11 | resolve(result[USER_TOKEN]);
12 | });
13 | }
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/lib/utils/util.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'redux';
2 | import React from 'react';
3 | import { getToken } from './token';
4 | import { API } from '@apis/common';
5 | import { userLogin } from '@contexts/user';
6 |
7 | export function createAction(type: T): (payload: P) => Action & { payload: P };
8 | export function createAction(type: T): () => Action;
9 | export function createAction(type: T) {
10 | return (payload?: P) => {
11 | return {
12 | type,
13 | payload,
14 | };
15 | };
16 | }
17 |
18 | export function getImageURL(imageUrl: string | undefined) {
19 | return process.env.NODE_ENV === 'development'
20 | ? imageUrl && `./image/${imageUrl}`
21 | : imageUrl && chrome.extension.getURL(`image/${imageUrl}`);
22 | }
23 |
24 | function observe() {
25 | const subscriber = new Set();
26 | const subscribe = (callback: any) => {
27 | subscriber.add(callback);
28 | };
29 | const notify = (e: React.MouseEvent) => {
30 | subscriber.forEach((fn) => fn(e.target));
31 | };
32 |
33 | return {
34 | subscribe,
35 | notify,
36 | };
37 | }
38 |
39 | export const observer = observe();
40 |
--------------------------------------------------------------------------------
/client/src/setUpTest.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import { setupServer } from 'msw/node';
3 | import { rest } from 'msw';
4 |
5 | const handlers = [
6 | rest.get('http://127.0.0.1:5000/api/label/1', (req, res, ctx) => {
7 | return res(
8 | ctx.json({
9 | id: 1,
10 | title: 'bug',
11 | description: "Something isn't working",
12 | color: '#cf0000',
13 | issueTrackerId: 1,
14 | })
15 | );
16 | }),
17 | rest.get('http://127.0.0.1:5000/api/labels/1', (req, res, ctx) => {
18 | return res(
19 | ctx.json([
20 | {
21 | id: 1,
22 | title: 'bug',
23 | description: "Something isn't working",
24 | color: '#cf0000',
25 | issueTrackerId: 1,
26 | },
27 | {
28 | id: 2,
29 | title: 'documentation',
30 | description: 'Edit documentation',
31 | color: '#33b1ff',
32 | issueTrackerId: 1,
33 | },
34 | {
35 | id: 3,
36 | title: 'duplicate',
37 | description: 'This issue already exists',
38 | color: '#b5babd',
39 | issueTrackerId: 1,
40 | },
41 | ])
42 | );
43 | }),
44 | ];
45 |
46 | const server = setupServer(...handlers);
47 |
48 | beforeAll(() => {
49 | server.listen();
50 | });
51 |
52 | afterAll(() => {
53 | server.close();
54 | });
55 |
56 | export default server;
57 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "target": "es5",
5 | "module": "commonjs",
6 | "lib": ["ES2020", "DOM"],
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "baseUrl": ".",
12 | "paths": {
13 | "@ingredients/*": ["src/components/Ingredients/*"],
14 | "@meal/*": ["src/components/Meal/*"],
15 | "@set/*": ["src/components/Set/*"],
16 | "@apis/*": ["src/lib/apis/*"],
17 | "@constants/*": ["src/lib/constants/*"],
18 | "@utils/*": ["src/lib/utils/*"],
19 | "@contexts/*": ["src/contexts/*"],
20 | "@hooks/*": ["src/hooks/*"],
21 | "@lib/*": ["src/lib/*"],
22 | "@test/*": ["src/__test__/*"]
23 | }
24 | },
25 | "include": ["./src/**/*"]
26 | }
27 |
--------------------------------------------------------------------------------
/server/.env.sample:
--------------------------------------------------------------------------------
1 | # PORT
2 | PORT=port
3 |
4 | # DB
5 | DB_HOST=db_host
6 | DB_PORT=db_port
7 | DB_USERNAME=db_username
8 | DB_PASSWORD=db_password
9 | DB_DATABASE=db_database
10 |
11 | # NAVER OAUTH
12 | CLIENT_ID=client_id
13 | CLIENT_SECRET=client_secret
14 |
15 | # JWT
16 | JWT_SECRET=jwt_secret
17 |
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const OS = require('os');
2 |
3 | module.exports = {
4 | env: {
5 | es2021: true,
6 | node: true,
7 | },
8 | extends: [
9 | 'eslint:recommended',
10 | 'eslint-config-prettier',
11 | 'plugin:@typescript-eslint/recommended',
12 | 'airbnb-base',
13 | ],
14 | parser: '@typescript-eslint/parser',
15 | plugins: ['@typescript-eslint', 'prettier'],
16 | ignorePatterns: ['dist/', 'node_modules/'],
17 | settings: {
18 | 'import/resolver': {
19 | node: {
20 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts'],
21 | },
22 | },
23 | },
24 | rules: {
25 | 'linebreak-style': ['error', OS.EOL === '\r\n' ? 'windows' : 'unix'],
26 | 'prettier/prettier': [
27 | 'error',
28 | {
29 | endOfLine: 'auto',
30 | },
31 | ],
32 | '@typescript-eslint/no-explicit-any': 'off',
33 | 'class-methods-use-this': 0,
34 | 'import/extensions': 'off',
35 | },
36 | overrides: [
37 | {
38 | files: ['*.js'],
39 | rules: {
40 | '@typescript-eslint/no-var-requires': 'off',
41 | },
42 | },
43 | ],
44 | };
45 |
--------------------------------------------------------------------------------
/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "tabWidth": 2,
4 | "trailingComma": "es5",
5 | "bracketSpacing": true,
6 | "printWidth": 100
7 | }
8 |
--------------------------------------------------------------------------------
/server/ormconfig.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'mysql',
3 | host: process.env.DB_HOST,
4 | port: process.env.DB_PORT,
5 | username: process.env.DB_USERNAME,
6 | password: process.env.DB_PASSWORD,
7 | database: process.env.DB_DATABASE,
8 | synchronize: false,
9 | logging: false,
10 | entities: ['src/entity/**/*.ts'],
11 | migrations: ['src/migration/**/*.ts'],
12 | subscribers: ['src/subscriber/**/*.ts'],
13 | cli: {
14 | entitiesDir: 'src/entity',
15 | migrationsDir: 'src/migration',
16 | subscribersDir: 'src/subscriber',
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "tsc && node build/app.js",
9 | "dev": "nodemon --exec ts-node -r tsconfig-paths/register --files src/app.ts"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "@types/cors": "^2.8.8",
16 | "@types/express": "^4.17.9",
17 | "@types/jsonwebtoken": "^8.5.0",
18 | "@types/morgan": "^1.9.2",
19 | "@types/node": "^14.14.8",
20 | "@types/pug": "^2.0.4",
21 | "@typescript-eslint/eslint-plugin": "^4.8.1",
22 | "@typescript-eslint/parser": "^4.8.1",
23 | "eslint": "^7.13.0",
24 | "eslint-config-airbnb-base": "^14.2.1",
25 | "eslint-config-prettier": "^6.15.0",
26 | "eslint-plugin-import": "^2.22.1",
27 | "eslint-plugin-prettier": "^3.1.4",
28 | "nodemon": "^2.0.6",
29 | "prettier": "^2.1.2",
30 | "ts-node": "^9.0.0",
31 | "tsconfig-paths": "^3.9.0",
32 | "typeorm": "^0.2.29",
33 | "typescript": "^4.0.5"
34 | },
35 | "dependencies": {
36 | "axios": "^0.21.0",
37 | "cors": "^2.8.5",
38 | "dotenv": "^8.2.0",
39 | "express": "^4.17.1",
40 | "jsonwebtoken": "^8.5.1",
41 | "morgan": "^1.10.0",
42 | "mysql2": "^2.2.5",
43 | "pug": "^3.0.0",
44 | "reflect-metadata": "^0.1.13"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/server/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcamp-2020/Project15-C-Client-Based-Formula-Editor/409fde8ec7e7eb288214498fb2efd292c37c7830/server/public/icon.png
--------------------------------------------------------------------------------
/server/src/entity/Favorite.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
2 | import { User } from './User';
3 |
4 | @Entity({ name: 'favorite' })
5 | export class Favorite {
6 | @PrimaryGeneratedColumn()
7 | id!: number;
8 |
9 | @Column()
10 | latex!: string;
11 |
12 | @Column('varchar', { length: 45 })
13 | title!: string;
14 |
15 | @ManyToOne(() => User, (user) => user.favorite, { onDelete: 'CASCADE' })
16 | user!: User;
17 | }
18 |
--------------------------------------------------------------------------------
/server/src/entity/User.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
2 | import { Favorite } from './Favorite';
3 |
4 | @Entity({ name: 'user' })
5 | export class User {
6 | @PrimaryGeneratedColumn()
7 | id!: number;
8 |
9 | @Column('varchar', { length: 20 })
10 | userId!: string;
11 |
12 | @Column('varchar', { length: 45 })
13 | email!: string;
14 |
15 | @OneToMany(() => Favorite, (favorite) => favorite.user, {
16 | onDelete: 'CASCADE',
17 | })
18 | favorite!: Favorite[];
19 | }
20 |
--------------------------------------------------------------------------------
/server/src/repository/favorite-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import { Favorite } from '@entity/Favorite';
3 | import { CreateParams } from '@service/favorite-service';
4 |
5 | @EntityRepository(Favorite)
6 | export default class FavoriteRepository extends Repository {
7 | getByUserId(userId: number) {
8 | return this.find({ user: { id: userId } });
9 | }
10 |
11 | createAndSave({ title, latex, userId }: CreateParams) {
12 | const favorite = this.create({ latex, title, user: { id: userId } });
13 | return this.manager.save(favorite);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/repository/user-repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository, Repository } from 'typeorm';
2 | import { User } from '@entity/User';
3 | import { CreateParams } from '@service/user-service';
4 |
5 | @EntityRepository(User)
6 | export default class UserRepository extends Repository {
7 | findUser(userId: string) {
8 | return this.findOne({ userId });
9 | }
10 |
11 | insertUesr({ email, userId }: CreateParams) {
12 | const newUser = this.create({ email, userId });
13 | return this.manager.save(newUser);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/router/auth-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import AuthController from '@controllers/auth-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.post('/login', AuthController.login);
7 | router.post('/autologin', AuthController.authCheck, AuthController.autoLogin);
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/server/src/router/favorite-router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import FavoriteController from '@controllers/favorite-controller';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/:userId', FavoriteController.getFavoritesByUserId);
7 | router.post('/', FavoriteController.createFavorite);
8 | router.delete('/:id', FavoriteController.deleteFavorite);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/server/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import AuthController from '@controllers/auth-controller';
3 | import favoriteRouter from './favorite-router';
4 | import authRouter from './auth-router';
5 |
6 | const router = express.Router();
7 |
8 | router.use('/auth', authRouter);
9 | router.use('/favorite', AuthController.authCheck, favoriteRouter);
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/server/src/service/favorite-service.ts:
--------------------------------------------------------------------------------
1 | import { getCustomRepository } from 'typeorm';
2 | import FavoriteRepository from '@repository/favorite-repository';
3 |
4 | export interface CreateParams {
5 | title: string;
6 | latex: string;
7 | userId: number;
8 | }
9 |
10 | class FavoriteService {
11 | static instance: FavoriteService;
12 |
13 | private favoriteRepository: FavoriteRepository;
14 |
15 | constructor() {
16 | this.favoriteRepository = getCustomRepository(FavoriteRepository);
17 | }
18 |
19 | static getInstance(): FavoriteService {
20 | if (!FavoriteService.instance) {
21 | FavoriteService.instance = new FavoriteService();
22 | }
23 | return FavoriteService.instance;
24 | }
25 |
26 | async getFavoritesByUserId(userId: number) {
27 | const favorites = await this.favoriteRepository.getByUserId(userId);
28 | return favorites;
29 | }
30 |
31 | async createFavorites({ latex, title, userId }: CreateParams) {
32 | return await this.favoriteRepository.createAndSave({ title, latex, userId });
33 | }
34 |
35 | async deleteFavorites(id: number) {
36 | await this.favoriteRepository.delete(id);
37 | }
38 | }
39 |
40 | export default FavoriteService;
41 |
--------------------------------------------------------------------------------
/server/src/service/user-service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import UserRepository from '@repository/user-repository';
3 | import { getCustomRepository } from 'typeorm';
4 |
5 | export interface CreateParams {
6 | email: string;
7 | userId: string;
8 | }
9 |
10 | class UserService {
11 | static instance: UserService;
12 |
13 | private userRepository: UserRepository;
14 |
15 | constructor() {
16 | this.userRepository = getCustomRepository(UserRepository);
17 | }
18 |
19 | static getInstance(): UserService {
20 | if (!UserService.instance) {
21 | UserService.instance = new UserService();
22 | }
23 | return UserService.instance;
24 | }
25 |
26 | async getUser(userId: string) {
27 | const user = await this.userRepository.findUser(userId);
28 | return user;
29 | }
30 |
31 | async createUser({ email, userId }: CreateParams) {
32 | const newUser = await this.userRepository.insertUesr({ email, userId });
33 | return newUser;
34 | }
35 | }
36 |
37 | export default UserService;
38 |
--------------------------------------------------------------------------------
/server/src/types/express/index.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace Express {
2 | interface Request {
3 | user?: {
4 | id: number;
5 | userId?: string;
6 | email?: string;
7 | };
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/utils/constant.ts:
--------------------------------------------------------------------------------
1 | export enum STATUS_CODE {
2 | SUCCESS = 200,
3 | CREATED = 201,
4 | CLIENT_ERROR = 400,
5 | UNAUTHORIZED = 401,
6 | SERVER_ERROR = 500,
7 | }
8 |
9 | export enum ERROR_MESSAGE {
10 | CLIENT_ERROR = 'Request Error',
11 | UNAUTHORIZED_ERROR = 'Unauthorized Error',
12 | SERVER_ERROR = 'Server Error',
13 | }
14 |
--------------------------------------------------------------------------------
/server/src/utils/validator.ts:
--------------------------------------------------------------------------------
1 | export enum ValidateType {
2 | Number = 'number',
3 | String = 'string',
4 | Object = 'object',
5 | Array = 'array',
6 | Boolean = 'boolean',
7 | Undefined = 'undefined',
8 | Null = 'null',
9 | }
10 |
11 | export function isValidateType(
12 | target: T,
13 | type: ValidateType,
14 | callback?: (target: T) => boolean
15 | ): boolean {
16 | if (type === ValidateType.Null && target !== null) {
17 | return false;
18 | }
19 |
20 | if (type === ValidateType.Number && !Number.isFinite(Number(target))) {
21 | return false;
22 | }
23 |
24 | if (type === ValidateType.Array && !Array.isArray(target)) return false;
25 |
26 | if (
27 | (type === ValidateType.Object ||
28 | type === ValidateType.String ||
29 | type === ValidateType.Boolean ||
30 | type === ValidateType.Undefined) &&
31 | typeof target !== type
32 | )
33 | return false;
34 |
35 | if (callback && !callback(target)) return false;
36 |
37 | return true;
38 | }
39 |
--------------------------------------------------------------------------------
/server/src/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 | block append head
3 |
4 | block content
5 | header
6 | img(class='logo' src='/icon.png' alt='수식 셰프 로고')
7 | h1 수식 셰프
8 |
9 | section.main
10 | h2 QR 코드로 이미지 보기
11 | img(class='result' src= `https://i.imgur.com/${url}.png` alt='수식')
12 |
13 | footer
14 | div 제작자
15 | ul
16 | li 김동현
17 | li 문건우
18 | li 박기호
19 | li 장우영
20 | div in BoostCamp 2020
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2015", "es2016", "es2017", "es2018", "es2019", "es2020"],
4 | "outDir": "./dist",
5 | "target": "es5",
6 | "sourceMap": true,
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "esModuleInterop": true,
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "strict": true,
13 | "baseUrl": ".",
14 | "paths": {
15 | "@utils/*": ["src/utils/*"],
16 | "@controllers/*": ["src/controllers/*"],
17 | "@entity/*": ["src/entity/*"],
18 | "@repository/*": ["src/repository/*"],
19 | "@router/*": ["src/router/*"],
20 | "@service/*": ["src/service/*"]
21 | },
22 | "typeRoots": ["src/types"]
23 | },
24 | "include": ["src/**/*"],
25 | // 컴파일에서 제외되는 파일들
26 | "exclude": ["node_modules", "**/*.spec.ts"]
27 | }
28 |
--------------------------------------------------------------------------------