├── .eslintrc ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .gitmessage ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── public ├── icon.svg ├── images │ ├── blueprint.png │ ├── christamas.png │ ├── dark.png │ ├── default.png │ ├── logo.png │ ├── map.png │ ├── monochrome.png │ └── sketch.png └── index.html ├── src ├── App.tsx ├── components │ ├── Canvas.tsx │ ├── Icon │ │ ├── AddIcon.tsx │ │ ├── CloseIcon.tsx │ │ ├── Compare.tsx │ │ ├── Copy.tsx │ │ ├── FullScreen.tsx │ │ ├── MoreVertIcon.tsx │ │ ├── RedoIcon.tsx │ │ ├── RemoveIcon.tsx │ │ ├── SmallScreen.tsx │ │ └── UndoIcon.tsx │ ├── Map │ │ ├── ButtonGroup │ │ │ ├── Button.tsx │ │ │ ├── LowerButtons.tsx │ │ │ └── UpperButtons.tsx │ │ ├── History │ │ │ ├── History.tsx │ │ │ ├── ReplaceHistoryContent.tsx │ │ │ └── SetHistoryContent.tsx │ │ ├── Map.tsx │ │ ├── Marker │ │ │ └── MarkerPopup.tsx │ │ └── SearchInput │ │ │ └── SearchInput.tsx │ ├── Sidebar │ │ ├── Sidebar.tsx │ │ ├── SidebarContentFewer │ │ │ ├── DepthItem.tsx │ │ │ ├── SidebarContent.tsx │ │ │ ├── SidebarContentDepth.tsx │ │ │ ├── SidebarContentTheme.tsx │ │ │ └── ThemeItem.tsx │ │ ├── SidebarContentMore │ │ │ ├── ColorStyle.tsx │ │ │ ├── DetailType.tsx │ │ │ ├── DetailTypeItem.tsx │ │ │ ├── DetailTypeSubList.tsx │ │ │ ├── FeatureType.tsx │ │ │ ├── FeatureTypeItem.tsx │ │ │ ├── LightnessStyle.tsx │ │ │ ├── SaturationStyle.tsx │ │ │ ├── SidebarContent.tsx │ │ │ ├── Styler.tsx │ │ │ ├── VisibilityStyle.tsx │ │ │ └── WeightStyle.tsx │ │ ├── SidebarFooter │ │ │ └── SidebarFooter.tsx │ │ ├── SidebarHeader │ │ │ ├── SidebarDropdown.tsx │ │ │ └── SidebarHeader.tsx │ │ └── SidebarModal │ │ │ ├── ExportModal.tsx │ │ │ ├── ImportModal.tsx │ │ │ └── common.tsx │ └── common │ │ └── Overlay.tsx ├── hooks │ ├── common │ │ ├── useInputRange.ts │ │ ├── useInputText.ts │ │ └── useWholeStyle.ts │ ├── map │ │ ├── getCompareMap.js │ │ ├── useCompareFeature.ts │ │ ├── useHistoryFeature.ts │ │ ├── useHistoryMap.ts │ │ ├── useLowerButtons.ts │ │ ├── useMap.ts │ │ ├── useMapTheme.ts │ │ ├── useMarkerPopUp.ts │ │ ├── useMarkerPosition.ts │ │ ├── useMarkerRegister.ts │ │ └── useUpperButtons.ts │ ├── sidebar │ │ ├── useCopyToClipboard.ts │ │ ├── useDetailType.ts │ │ ├── useExportStyle.ts │ │ ├── useFeatureTypeItem.ts │ │ ├── useImportModalStatus.ts │ │ ├── useInitAllColor.ts │ │ ├── useSidebarDepthItem.ts │ │ ├── useSidebarDropdown.ts │ │ ├── useSidebarFooter.ts │ │ ├── useSidebarHeader.ts │ │ ├── useSidebarType.ts │ │ ├── useStyleType.ts │ │ ├── useTheme.ts │ │ ├── useToggleStatus.ts │ │ └── useUndoRedo.ts │ └── useCanvas.ts ├── index.tsx ├── pages │ ├── Entry.tsx │ └── Main.tsx ├── react-app-env.d.ts ├── store │ ├── common │ │ └── type.ts │ ├── depth-theme │ │ ├── action.ts │ │ └── reducer.ts │ ├── history │ │ ├── action.ts │ │ └── reducer.ts │ ├── index.ts │ ├── map │ │ ├── action.ts │ │ ├── geocoder.d.ts │ │ ├── initializeMap.ts │ │ └── reducer.ts │ ├── marker │ │ ├── action.ts │ │ └── reducer.ts │ ├── sidebar │ │ ├── action.ts │ │ └── reducer.ts │ └── style │ │ ├── action.ts │ │ ├── compareStyle.ts │ │ ├── getReducer.ts │ │ ├── manageCategories.ts │ │ ├── properties.ts │ │ └── reducer.ts └── utils │ ├── applyStyle.ts │ ├── colorFormat.ts │ ├── deepCopy.ts │ ├── getRandomId.ts │ ├── getTypeName.ts │ ├── map-styling │ ├── administrative.ts │ ├── index.ts │ ├── landscape.ts │ ├── macgyver │ │ ├── mappingDetailToFunc.ts │ │ ├── seperatedLayers.ts │ │ ├── utils.ts │ │ └── weightTemplate.ts │ ├── poi.ts │ ├── road.ts │ ├── transit.ts │ └── water.ts │ ├── removeNullFromObject.ts │ ├── rendering-data │ ├── defaultStyle.ts │ ├── featureTypeData.ts │ ├── layers │ │ └── init.ts │ ├── sidebarThemeData.ts │ └── theme │ │ ├── blueprint.json │ │ ├── christmas.json │ │ ├── dark.json │ │ ├── monochrome.json │ │ └── sketch.json │ ├── setFeatureStyle.ts │ ├── styles │ ├── globalStyle.tsx │ └── styled.ts │ ├── updateMarkerStorage.ts │ ├── urlParsing.ts │ └── validateStyle.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "airbnb", 5 | "prettier", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | ], 9 | "plugins": ["prettier", "@typescript-eslint"], 10 | "parser": "@typescript-eslint/parser", 11 | "rules": { 12 | "prettier/prettier": ["error", { "endOfLine": "auto" }], 13 | "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], 14 | "import/no-unresolved": "off", 15 | "import/extensions": "off", 16 | "no-use-before-define": "off", 17 | "@typescript-eslint/no-use-before-define": ["error"], 18 | "react-hooks/exhaustive-deps": "off", 19 | "react/require-default-props": "off", 20 | "import/order": "off", 21 | "import/prefer-default-export": "off", 22 | "no-shadow": "off", 23 | "no-empty-function": "off", 24 | "@typescript-eslint/no-empty-function": "off" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: frontend 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: [ubuntu-latest] 10 | steps: 11 | - name: executing remote ssh commands using password 12 | uses: appleboy/ssh-action@master 13 | with: 14 | host: ${{ secrets.HOST }} 15 | username: ${{ secrets.USERNAME }} 16 | password: ${{ secrets.PASSWORD }} 17 | script: | 18 | bash deploy.sh 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.gitmessage: -------------------------------------------------------------------------------- 1 | [TYPE] [#] 제목 2 | 3 | - 4 | - 5 | 6 | ## 말머리 7 | # feat: 새로운 기능 추가 8 | # refactor: 코드 리팩토링 9 | # fix: 버그 수정 10 | # test: 테스트 코드 작성 11 | # docs: 문서 12 | # chore: 환경설정 파일 13 | # style: 코드 형식, 정렬, 린트 등의 변경 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "arrowParens": "always", 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project08", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/core": "^10.1.1", 7 | "@emotion/styled": "^10.0.27", 8 | "@mapbox/mapbox-gl-geocoder": "^4.7.0", 9 | "dotenv": "^8.2.0", 10 | "emotion-reset": "^2.0.7", 11 | "emotion-theming": "^10.0.27", 12 | "immer": "^8.0.0", 13 | "mapbox-gl": "^1.12.0", 14 | "mapbox-gl-compare": "^0.4.0", 15 | "react": "^17.0.1", 16 | "react-dom": "^17.0.1", 17 | "react-redux": "^7.2.2", 18 | "react-router-dom": "^5.2.0", 19 | "react-scripts": "4.0.0", 20 | "redux": "^4.0.5", 21 | "typescript": "^4.0.5" 22 | }, 23 | "scripts": { 24 | "start": "NODE_ENV=development react-scripts start", 25 | "build": "GENERATE_SOURCEMAP=false react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@testing-library/jest-dom": "^5.11.6", 49 | "@testing-library/react": "^11.1.2", 50 | "@testing-library/user-event": "^12.2.2", 51 | "@types/jest": "^26.0.15", 52 | "@types/mapbox-gl": "^1.12.7", 53 | "@types/node": "^12.19.4", 54 | "@types/react": "^16.9.56", 55 | "@types/react-dom": "^16.9.9", 56 | "@types/react-redux": "^7.1.11", 57 | "@types/react-router-dom": "^5.1.6", 58 | "@types/redux": "^3.6.0", 59 | "@typescript-eslint/eslint-plugin": "^4.8.0", 60 | "@typescript-eslint/parser": "^4.8.0", 61 | "eslint": "^7.11.0", 62 | "eslint-config-airbnb": "^18.2.1", 63 | "eslint-config-prettier": "^6.15.0", 64 | "eslint-plugin-import": "^2.22.1", 65 | "eslint-plugin-jsx-a11y": "^6.4.1", 66 | "eslint-plugin-prettier": "^3.1.4", 67 | "eslint-plugin-react": "^7.21.5", 68 | "eslint-plugin-react-hooks": "^4.2.0", 69 | "module-alias": "^2.2.2", 70 | "prettier": "^2.1.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 33 | 34 | B 35 | 36 | -------------------------------------------------------------------------------- /public/images/blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/blueprint.png -------------------------------------------------------------------------------- /public/images/christamas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/christamas.png -------------------------------------------------------------------------------- /public/images/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/dark.png -------------------------------------------------------------------------------- /public/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/default.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/logo.png -------------------------------------------------------------------------------- /public/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/map.png -------------------------------------------------------------------------------- /public/images/monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/monochrome.png -------------------------------------------------------------------------------- /public/images/sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project08-A-Styled-Map-Admin-Tool/c7722bbafe70c0be960fc9d199c1c8d6d54bdbce/public/images/sketch.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Styled Map 13 | 14 | 15 |
Styled Map
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import Main from './pages/Main'; 4 | import Entry from './pages/Entry'; 5 | import { URLPathNameType } from './store/common/type'; 6 | 7 | /** 추후에 라우팅해야되면 수정할 예정 */ 8 | function App(): React.ReactElement { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/components/Canvas.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../utils/styles/styled'; 3 | import useCanvas from '../hooks/useCanvas'; 4 | 5 | const Canvas = styled.canvas``; 6 | 7 | const Img = styled.img` 8 | display: none; 9 | `; 10 | 11 | const CANVAS_WIDTH = 804; 12 | const CANVAS_HEIGHT = 420; 13 | 14 | function CanvasComponent(): React.ReactElement { 15 | const { mapImgRef, logoImgRef, canvasRef, mouseDownHandler } = useCanvas(); 16 | 17 | return ( 18 | <> 19 | map 25 | logo 31 | 37 | 38 | ); 39 | } 40 | 41 | export default CanvasComponent; 42 | -------------------------------------------------------------------------------- /src/components/Icon/AddIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (): React.ReactElement => { 4 | return ( 5 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Icon/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface CloseIconProps { 4 | className?: string; 5 | onClick?: (e: React.MouseEvent) => void; 6 | } 7 | 8 | export default ({ className, onClick }: CloseIconProps): React.ReactElement => { 9 | return ( 10 | 18 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Icon/Compare.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (): React.ReactElement => { 4 | return ( 5 | 12 | 13 | 14 | 15 | 16 | 17 | Created by Sergey Novosyolov 18 | 19 | 20 | from the Noun Project 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Icon/Copy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface CopyProps { 4 | className?: string; 5 | onClick: (e: React.MouseEvent) => void; 6 | } 7 | 8 | export default ({ className, onClick }: CopyProps): React.ReactElement => { 9 | return ( 10 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Icon/FullScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (): React.ReactElement => { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/Icon/MoreVertIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface MoreVertIconProps { 4 | className?: string; 5 | onClick?: () => void; 6 | } 7 | 8 | export default ({ 9 | className, 10 | onClick, 11 | }: MoreVertIconProps): React.ReactElement => { 12 | return ( 13 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Icon/RedoIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface RedoIconProps { 4 | className?: string; 5 | onClick: () => void; 6 | } 7 | 8 | function RedoIcon({ className, onClick }: RedoIconProps): React.ReactElement { 9 | return ( 10 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default RedoIcon; 25 | -------------------------------------------------------------------------------- /src/components/Icon/RemoveIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (): React.ReactElement => { 4 | return ( 5 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Icon/SmallScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (): React.ReactElement => { 4 | return ( 5 | 13 | 14 | 18 | 23 | 28 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/Icon/UndoIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface UndoIconProps { 4 | className?: string; 5 | onClick: () => void; 6 | } 7 | 8 | function UndoIcon({ className, onClick }: UndoIconProps): React.ReactElement { 9 | return ( 10 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default UndoIcon; 25 | -------------------------------------------------------------------------------- /src/components/Map/ButtonGroup/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, MouseEvent } from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | 4 | interface ButtonPropsInterface { 5 | width?: string; 6 | height?: string; 7 | fontSize?: string; 8 | children?: React.ReactNode; 9 | onClick?: (e: MouseEvent) => void; 10 | } 11 | 12 | const Button = styled.button` 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | 17 | font-size: ${(props) => props.fontSize}; 18 | width: ${(props) => props.width}; 19 | height: ${(props) => props.height}; 20 | margin: 7px 0 0 7px; 21 | 22 | border: 0; 23 | background-color: ${(props) => props.theme.WHITE}; 24 | border-radius: 4px; 25 | 26 | box-sizing: border-box; 27 | box-shadow: 0 0 1px 2px ${(props) => props.theme.GREY}; 28 | font-weight: 600; 29 | color: ${(props) => props.theme.DARKGREY}; 30 | 31 | &:hover { 32 | color: ${(props) => props.theme.GREEN}; 33 | background-color: rgb(230, 230, 230); 34 | } 35 | `; 36 | 37 | function ButtonPresenter({ 38 | fontSize = '10px', 39 | width = '50px', 40 | height = '50px', 41 | children, 42 | onClick, 43 | }: ButtonPropsInterface): ReactElement { 44 | return ( 45 | 48 | ); 49 | } 50 | 51 | export default ButtonPresenter; 52 | -------------------------------------------------------------------------------- /src/components/Map/ButtonGroup/LowerButtons.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import Button from './Button'; 5 | import PlusIcon from '../../Icon/AddIcon'; 6 | import MinusIcon from '../../Icon/RemoveIcon'; 7 | 8 | // Hook 9 | import useLowerButtons, { 10 | LowerButtonsHookType, 11 | } from '../../../hooks/map/useLowerButtons'; 12 | 13 | const LowerButtonsWrapper = styled.div` 14 | display: flex; 15 | flex-direction: column; 16 | align-items: flex-end; 17 | 18 | position: fixed; 19 | bottom: 88px; 20 | right: 10px; 21 | z-index: 10; 22 | `; 23 | 24 | function LowerButtons(): React.ReactElement { 25 | const { plusZoom, minusZoom }: LowerButtonsHookType = useLowerButtons(); 26 | 27 | return ( 28 | 29 | 32 | 35 | 36 | ); 37 | } 38 | 39 | export default LowerButtons; 40 | -------------------------------------------------------------------------------- /src/components/Map/ButtonGroup/UpperButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, RefObject } from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import Button from './Button'; 4 | import FullScreenIcon from '../../Icon/FullScreen'; 5 | import SmallScreenIcon from '../../Icon/SmallScreen'; 6 | 7 | // Hook 8 | import useUpperButtons, { 9 | useUpperButtonsType, 10 | } from '../../../hooks/map/useUpperButtons'; 11 | 12 | interface UpperButtonsProps { 13 | mapRef: RefObject; 14 | historyBtnHandler: () => void; 15 | } 16 | 17 | const UpperButtonsWrapper = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-around; 21 | 22 | position: fixed; 23 | top: 15px; 24 | right: 15px; 25 | z-index: 10; 26 | 27 | width: 240px; 28 | `; 29 | 30 | const SearchInput = styled.div` 31 | width: 100%; 32 | input[type='text'] { 33 | outline: none; 34 | box-shadow: 0 0 5px 1px ${(props) => props.theme.LIGHTGREY}; 35 | } 36 | * { 37 | z-index: 20; 38 | } 39 | `; 40 | 41 | const ButtonsWrapper = styled.div` 42 | display: flex; 43 | flex-direction: row; 44 | justify-content: flex-end; 45 | z-index: 10; 46 | `; 47 | 48 | function UpperButtons({ 49 | mapRef, 50 | historyBtnHandler, 51 | }: UpperButtonsProps): ReactElement { 52 | const { 53 | isFullscreen, 54 | fullScreenButtonClickHandler, 55 | smallScreenButtonClickHandler, 56 | }: useUpperButtonsType = useUpperButtons({ mapRef }); 57 | 58 | return ( 59 | 60 | 61 | 62 | 70 | 81 | 82 | 83 | ); 84 | } 85 | 86 | export default UpperButtons; 87 | -------------------------------------------------------------------------------- /src/components/Map/History/History.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { ReactElement } from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | 5 | // Components 6 | import SetHistoryContent from './SetHistoryContent'; 7 | import ReplaceHistoryContent from './ReplaceHistoryContent'; 8 | 9 | // Hook 10 | import useHistoryFeature from '../../../hooks/map/useHistoryFeature'; 11 | 12 | // Type 13 | import { 14 | HistorySetLogType, 15 | HistoryReplaceLogType, 16 | ReplaceType, 17 | } from '../../../store/common/type'; 18 | 19 | const HistoryWapper = styled.div` 20 | z-index: 30; 21 | width: 250px; 22 | height: 270px; 23 | background-color: white; 24 | padding: 15px 10px; 25 | position: fixed; 26 | top: 110px; 27 | right: 15px; 28 | border-radius: 10px; 29 | box-shadow: 0 0 10px ${(props) => props.theme.GREY}; 30 | display: flex; 31 | flex-direction: column; 32 | `; 33 | 34 | const HistoryList = styled.ul` 35 | margin-top: 10px; 36 | margin-bottom: 20px; 37 | overflow-y: scroll; 38 | `; 39 | 40 | interface HistoryItemProps { 41 | isCurrent: boolean; 42 | isCompared: boolean; 43 | } 44 | 45 | const HistoryItem = styled.li` 46 | margin-bottom: 5px; 47 | padding: 3px; 48 | border-radius: 3px; 49 | font-size: 1.3rem; 50 | color: ${(props) => (props.isCurrent ? props.theme.GREEN : props.theme.GREY)}; 51 | background-color: ${(props) => 52 | props.isCompared ? props.theme.LIGHTGREY : props.theme.WHITE}; 53 | 54 | cursor: pointer; 55 | 56 | &:hover { 57 | background-color: ${(props) => props.theme.LIGHTGREY}; 58 | } 59 | `; 60 | 61 | const HistoryTitle = styled.div` 62 | margin-bottom: 5px; 63 | font-size: 1.7rem; 64 | font-weight: bold; 65 | text-align: center; 66 | `; 67 | 68 | const Explain = styled.p` 69 | padding-left: 7px; 70 | font-size: 1.3rem; 71 | color: ${(props) => props.theme.DARKGREY}; 72 | `; 73 | 74 | const ResetHistory = styled.button` 75 | position: absolute; 76 | top: 240px; 77 | align-self: center; 78 | width: 60%; 79 | &:hover { 80 | color: ${(props) => props.theme.GREEN}; 81 | outline: none; 82 | } 83 | `; 84 | interface HistoryProps { 85 | isHistoryOpen: boolean; 86 | comparisonButtonClickHandler: (id: string) => void; 87 | compareId: string | undefined; 88 | setLogId: (id: string | undefined) => void; 89 | } 90 | 91 | function History({ 92 | isHistoryOpen, 93 | comparisonButtonClickHandler, 94 | compareId, 95 | setLogId, 96 | }: HistoryProps): ReactElement { 97 | const { log, currentIdx, resetHistoryAndStyle } = useHistoryFeature(); 98 | if (!isHistoryOpen) return <>; 99 | 100 | return ( 101 | 102 | 103 | HISTORY LIST 104 | 클릭 시 현재 화면과 비교 가능 105 | 106 | 107 | {log 108 | ?.map((item, idx) => ( 109 | comparisonButtonClickHandler(item.id as string)} 114 | > 115 | {item.changedKey in ReplaceType ? ( 116 | 117 | ) : ( 118 | 119 | )} 120 | 121 | )) 122 | .reverse()} 123 | 124 | { 126 | setLogId(undefined); 127 | resetHistoryAndStyle(); 128 | }} 129 | > 130 | History Reset 131 | 132 | 133 | ); 134 | } 135 | 136 | export default History; 137 | -------------------------------------------------------------------------------- /src/components/Map/History/ReplaceHistoryContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import { 4 | HistoryReplaceLogType, 5 | ReplaceType, 6 | DepthLogChangedValueType, 7 | } from '../../../store/common/type'; 8 | import { 9 | featureName, 10 | replaceName, 11 | depthName, 12 | } from '../../../utils/getTypeName'; 13 | 14 | const Content = styled.div` 15 | padding: 2px; 16 | position: relative; 17 | `; 18 | 19 | interface ReplaceHistoryContentProps { 20 | item: HistoryReplaceLogType; 21 | } 22 | 23 | function ReplaceHistoryContent({ 24 | item, 25 | }: ReplaceHistoryContentProps): React.ReactElement { 26 | let description = `${replaceName[item.changedKey]} `; 27 | 28 | if (item.changedValue) { 29 | if (item.changedKey === ReplaceType.depth) { 30 | const { feature, depth } = item.changedValue as DepthLogChangedValueType; 31 | description += `> ${featureName.feature[feature]} > ${ 32 | depthName[depth - 1] 33 | }`; 34 | } else description += `> ${item.changedValue}`; 35 | } 36 | return {`${description} 실행`}; 37 | } 38 | 39 | export default ReplaceHistoryContent; 40 | -------------------------------------------------------------------------------- /src/components/Map/History/SetHistoryContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import { HistorySetLogType } from '../../../store/common/type'; 4 | import { featureName, elementName } from '../../../utils/getTypeName'; 5 | 6 | const Content = styled.div` 7 | padding: 2px; 8 | position: relative; 9 | `; 10 | 11 | interface SetHistoryContentProps { 12 | item: HistorySetLogType; 13 | } 14 | 15 | function SetHistoryContent({ 16 | item, 17 | }: SetHistoryContentProps): React.ReactElement { 18 | return ( 19 | 20 | {`${featureName.feature[item.feature]} > ${ 21 | featureName.subFeature[item.feature][item.subFeature] 22 | } > ${elementName.element[item.element]} > ${ 23 | item.subElement ? elementName.subElement[item.subElement] : '' 24 | }`} 25 | {`> ${elementName.style[item.changedKey]} 26 | ${item.changedValue}로 변경`} 27 | 28 | ); 29 | } 30 | 31 | export default SetHistoryContent; 32 | -------------------------------------------------------------------------------- /src/components/Map/Map.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../utils/styles/styled'; 4 | import 'mapbox-gl-compare/style.css'; 5 | 6 | // Components 7 | import LowerButtons from './ButtonGroup/LowerButtons'; 8 | import UpperButtons from './ButtonGroup/UpperButtons'; 9 | import History from './History/History'; 10 | import MarkerPopUp from './Marker/MarkerPopup'; 11 | 12 | // Hook 13 | import useMarkerFeature from '../../hooks/map/useMarkerPosition'; 14 | import useMap, { MapHookType } from '../../hooks/map/useMap'; 15 | import useHistoryMap from '../../hooks/map/useHistoryMap'; 16 | import useCompareFeature from '../../hooks/map/useCompareFeature'; 17 | 18 | // Type 19 | import { URLPathNameType } from '../../store/common/type'; 20 | 21 | interface CurrentMapWrapperProps { 22 | isPageShow: boolean; 23 | } 24 | 25 | interface CompareMapWrapperProps { 26 | isOpened: boolean; 27 | } 28 | 29 | const MapsWrapper = styled.div` 30 | position: absolute; 31 | top: 0px; 32 | right: 0px; 33 | height: 100vh; 34 | width: ${(props) => (props.isPageShow ? '100%' : 'calc(100% - 370px)')}; 35 | overflow: hidden; 36 | `; 37 | 38 | const CurrentMapWrapper = styled.div` 39 | position: absolute; 40 | top: 0px; 41 | right: 0px; 42 | height: 100vh; 43 | width: 100%; 44 | display: flex; 45 | flex: 1 1 auto; 46 | user-select: none; 47 | 48 | canvas { 49 | outline: none; 50 | } 51 | `; 52 | 53 | const CompareMapWrapper = styled.div` 54 | position: absolute; 55 | top: 0; 56 | 57 | width: 100%; 58 | height: 100vh; 59 | background-color: ${(props) => props.theme.BLACK}; 60 | display: ${(props) => (props.isOpened ? 'block' : 'hidden')}; 61 | 62 | canvas { 63 | outline: none; 64 | } 65 | `; 66 | 67 | interface MapProps { 68 | pathname?: string; 69 | } 70 | 71 | function Map({ pathname }: MapProps): React.ReactElement { 72 | const { markerPosition, resetMarkerPos, markerLngLat } = useMarkerFeature(); 73 | const { containerRef, afterMapRef, beforeMapRef }: MapHookType = useMap(); 74 | 75 | const { isHistoryOpen, historyBtnHandler } = useHistoryMap(); 76 | const { logId, setLogId, comparisonButtonClickHandler } = useCompareFeature({ 77 | containerRef, 78 | beforeMapRef, 79 | }); 80 | 81 | return ( 82 | 86 | 87 | 88 | 93 | 94 | 100 | 104 | 105 | 106 | ); 107 | } 108 | 109 | export default Map; 110 | -------------------------------------------------------------------------------- /src/components/Map/Marker/MarkerPopup.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import CloseIcon from '../../Icon/CloseIcon'; 5 | 6 | // Hooks 7 | import useInputText from '../../../hooks/common/useInputText'; 8 | import useMarkerPopUp from '../../../hooks/map/useMarkerPopUp'; 9 | import { MarkerLngLatType } from '../../../hooks/map/useMarkerPosition'; 10 | 11 | const LIMIT_LENGTH = 10; 12 | interface WrapperProps { 13 | pos: { 14 | x: number | null; 15 | y: number | null; 16 | }; 17 | } 18 | export interface RegisterMarkerType { 19 | id?: string; 20 | text: string; 21 | lngLat?: MarkerLngLatType; 22 | instance?: mapboxgl.Marker; 23 | } 24 | 25 | const Wrapper = styled.div` 26 | position: relative; 27 | top: ${(props) => props.pos.y}px; 28 | left: ${(props) => props.pos.x}px; 29 | width: 110px; 30 | height: 120px; 31 | padding: 5px; 32 | background-color: ${(props) => props.theme.WHITE}; 33 | border-radius: 4px; 34 | 35 | z-index: 1000; 36 | box-sizing: border-box; 37 | box-shadow: 0 0 1px 2px ${(props) => props.theme.GREY}; 38 | font-weight: 600; 39 | color: ${(props) => props.theme.GREEN}; 40 | text-align: center; 41 | `; 42 | 43 | const PopUpHeader = styled.div` 44 | display: flex; 45 | justify-content: space-between; 46 | `; 47 | 48 | const MarkerInput = styled.textarea` 49 | width: 100%; 50 | height: 50px; 51 | `; 52 | 53 | const OkButton = styled.button` 54 | color: ${(props) => props.theme.WHITE}; 55 | background-color: ${(props) => props.theme.GREY}; 56 | &:hover { 57 | background-color: ${(props) => props.theme.GREEN}; 58 | } 59 | border-radius: 4px; 60 | border: none; 61 | padding-top: 2px; 62 | margin-top: 5px; 63 | `; 64 | 65 | const CloseIconTag = styled(CloseIcon)` 66 | cursor: pointer; 67 | height: 15px; 68 | width: 15px; 69 | `; 70 | 71 | interface MarkerPopUpProps { 72 | markerPosition: { x: number | null; y: number | null }; 73 | resetMarkerPos: () => void; 74 | markerLngLat: MarkerLngLatType; 75 | } 76 | 77 | function MarkerPopUp({ 78 | markerPosition, 79 | resetMarkerPos, 80 | markerLngLat, 81 | }: MarkerPopUpProps): React.ReactElement { 82 | const { inputText, onInputChange } = useInputText(); 83 | const { onClickButton } = useMarkerPopUp({ 84 | inputText, 85 | resetMarkerPos, 86 | markerLngLat, 87 | }); 88 | 89 | if (markerPosition.x && markerPosition.y) 90 | return ( 91 | e.stopPropagation()} 95 | > 96 |
97 | 98 |

마커 등록

99 | 100 |
101 | 102 | {inputText.length > LIMIT_LENGTH ? ( 103 |
10자 이하의 텍스트 입력만 가능합니다.
104 | ) : ( 105 | 106 | 확인 107 | 108 | )} 109 |
110 |
111 | ); 112 | return <>; 113 | } 114 | 115 | export default MarkerPopUp; 116 | -------------------------------------------------------------------------------- /src/components/Map/SearchInput/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import useInputText, { 4 | InputTextHookType, 5 | } from '../../../hooks/common/useInputText'; 6 | 7 | const SearchInput = styled.input` 8 | width: 100%; 9 | height: 40px; 10 | margin: 0; 11 | padding-left: 15px; 12 | z-index: 10; 13 | 14 | border: 0; 15 | border-radius: 5px; 16 | 17 | background-color: ${(props) => props.theme.WHITE}; 18 | box-shadow: 0 0 1px 2px ${(props) => props.theme.GREY}; 19 | 20 | text-align: left; 21 | font-family: 'Noto Sans KR', sans-serif; 22 | `; 23 | 24 | function SearchInputPresenter(): ReactElement { 25 | const { inputText, onInputChange }: InputTextHookType = useInputText(); 26 | 27 | return ( 28 | 33 | ); 34 | } 35 | 36 | export default SearchInputPresenter; 37 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { memo } from 'react'; 3 | import styled from '../../utils/styles/styled'; 4 | import SidebarHeader from './SidebarHeader/SidebarHeader'; 5 | import SidebarContentFewer from './SidebarContentFewer/SidebarContent'; 6 | import SidebarContentMore from './SidebarContentMore/SidebarContent'; 7 | import SidebarFooter from './SidebarFooter/SidebarFooter'; 8 | 9 | // Hook 10 | import useSidebar, { 11 | ToggleStatusHook, 12 | } from '../../hooks/sidebar/useToggleStatus'; 13 | 14 | const SidebarWrapper = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: space-between; 18 | flex: 0 0 370px; 19 | height: 100vh; 20 | z-index: 30; 21 | background-color: white; 22 | font-family: 'Noto Sans KR', sans-serif; 23 | `; 24 | 25 | function SidebarPresenter(): React.ReactElement { 26 | const { isActive, setIsActive }: ToggleStatusHook = useSidebar(); 27 | 28 | return ( 29 | 30 | 31 | {isActive ? : } 32 | 33 | 34 | ); 35 | } 36 | 37 | export default memo(SidebarPresenter); 38 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentFewer/DepthItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import useSidebarDepthItem, { 3 | DepthItemKeyTypes, 4 | } from '../../../hooks/sidebar/useSidebarDepthItem'; 5 | import styled from '../../../utils/styles/styled'; 6 | 7 | const ItemWrapper = styled.li` 8 | display: flex; 9 | padding: 0 16px 24px 16px; 10 | justify-content: space-between; 11 | align-items: center; 12 | `; 13 | 14 | const Label = styled.label` 15 | font-size: 1.6rem; 16 | font-weight: 600; 17 | `; 18 | 19 | export const Range = styled.input` 20 | -webkit-appearance: none; 21 | opacity: 0.7; 22 | width: 70%; 23 | height: 3px; 24 | background-color: ${(props) => props.theme.GREY}; 25 | outline: none; 26 | border: none; 27 | &::-webkit-slider-thumb { 28 | -webkit-appearance: none; 29 | width: 20px; 30 | height: 20px; 31 | border-radius: 50%; 32 | background-color: ${(props) => props.theme.GREEN}; 33 | cursor: pointer; 34 | } 35 | &::-moz-range-thumb { 36 | width: 20px; 37 | height: 20px; 38 | border-radius: 50%; 39 | background-color: ${(props) => props.theme.GREEN}; 40 | cursor: pointer; 41 | border: none; 42 | } 43 | `; 44 | 45 | interface ItemPresenterProps { 46 | name: string; 47 | itemKey: DepthItemKeyTypes; 48 | } 49 | 50 | function DepthItemPresenter({ 51 | name, 52 | itemKey, 53 | }: ItemPresenterProps): React.ReactElement { 54 | const { itemDepth, depthRangeHandler } = useSidebarDepthItem(itemKey); 55 | 56 | return ( 57 | 58 | 59 | 68 | 69 | ); 70 | } 71 | 72 | export default memo(DepthItemPresenter); 73 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentFewer/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import SidebarContentDepth from './SidebarContentDepth'; 4 | import SidebarContentTheme from './SidebarContentTheme'; 5 | 6 | const ContentWrapper = styled.div` 7 | width: 100%; 8 | height: 100%; 9 | padding: 20px; 10 | overflow-y: scroll; 11 | `; 12 | 13 | function SidebarContent(): React.ReactElement { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default memo(SidebarContent); 23 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentFewer/SidebarContentDepth.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { DepthItemKeyTypes } from '../../../hooks/sidebar/useSidebarDepthItem'; 3 | import styled from '../../../utils/styles/styled'; 4 | import DepthItem from './DepthItem'; 5 | 6 | function SidebarDepth(): React.ReactElement { 7 | const DepthController = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | width: 100%; 11 | `; 12 | 13 | const DepthControllerTitle = styled.h2` 14 | padding: 20px 8px 24px 8px; 15 | font-size: 2rem; 16 | font-weight: 600; 17 | `; 18 | 19 | const DepthList = styled.ul` 20 | width: 90%; 21 | margin: 0 auto; 22 | `; 23 | 24 | return ( 25 | 26 | 표기 단계 조절 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default memo(SidebarDepth); 36 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentFewer/SidebarContentTheme.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import useMapTheme, { MapThemeHookType } from '../../../hooks/map/useMapTheme'; 3 | import ThemeItem from './ThemeItem'; 4 | import styled from '../../../utils/styles/styled'; 5 | import data from '../../../utils/rendering-data/sidebarThemeData'; 6 | 7 | const ThemeWrapper = styled.div` 8 | padding: 50px 8px; 9 | `; 10 | 11 | const Title = styled.h2` 12 | font-size: 2rem; 13 | font-weight: 600; 14 | `; 15 | 16 | const List = styled.ul` 17 | width: 90%; 18 | margin: 0 auto; 19 | margin-top: 25px; 20 | `; 21 | 22 | function SidebarContentTheme(): React.ReactElement { 23 | const { themeIdx, checkHandler }: MapThemeHookType = useMapTheme(); 24 | 25 | return ( 26 | 27 | 지도 테마 선택 28 | 29 | {data.map((d, i) => ( 30 | checkHandler(i)} 34 | data={d} 35 | /> 36 | ))} 37 | 38 | 39 | ); 40 | } 41 | 42 | export default memo(SidebarContentTheme); 43 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentFewer/ThemeItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import useTheme from '../../../hooks/sidebar/useTheme'; 4 | import { objType } from '../../../store/common/type'; 5 | 6 | interface ImageProp { 7 | src: string; 8 | } 9 | 10 | interface CheckedProp { 11 | checked: boolean; 12 | } 13 | 14 | const Item = styled.li` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | margin: 10px 0; 19 | cursor: pointer; 20 | `; 21 | 22 | const Image = styled.div` 23 | background-image: url('${(props) => props.src}'); 24 | background-size: cover; 25 | width: 25%; 26 | height: 50px; 27 | `; 28 | 29 | const Name = styled.span` 30 | width: 55%; 31 | font-size: 1.5rem; 32 | color: ${(props) => (props.checked ? props.theme.GREEN : props.theme.GREY)}; 33 | `; 34 | 35 | const Checkbox = styled.div` 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | width: 24px; 40 | height: 24px; 41 | border-radius: 12px; 42 | background-color: ${(props) => 43 | props.checked ? props.theme.GREEN : 'lightgray'}; 44 | `; 45 | 46 | const Circle = styled.div` 47 | width: 16px; 48 | height: 16px; 49 | border-radius: 8px; 50 | background-color: ${(props) => (props.checked ? 'white' : 'lightgray')}; 51 | `; 52 | 53 | interface ThemeItemProps { 54 | data: { 55 | src: string; 56 | name: string; 57 | theme?: objType; 58 | }; 59 | checked: boolean; 60 | clickHandler: () => void; 61 | } 62 | 63 | function ThemeItem({ 64 | data, 65 | checked, 66 | clickHandler, 67 | }: ThemeItemProps): React.ReactElement { 68 | const { applyTheme } = useTheme({ 69 | clickHandler, 70 | }); 71 | 72 | return ( 73 | applyTheme(data as objType)}> 74 | 75 | {data.name} 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | 83 | export default ThemeItem; 84 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/ColorStyle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import useInputRange from '../../../hooks/common/useInputRange'; 4 | import { StyleDefaultKeyType, StyleKeyType } from '../../../store/common/type'; 5 | 6 | const ColorWrapper = styled.div` 7 | display: flex; 8 | flex-wrap: wrap; 9 | `; 10 | 11 | const ColorTitle = styled.label` 12 | margin: auto 0; 13 | font-size: 1.7rem; 14 | font-weight: 600; 15 | color: ${(props) => props.theme.GREY}; 16 | `; 17 | 18 | const ColorCode = styled.div` 19 | margin: auto 0 auto 5px; 20 | font-size: 1.3rem; 21 | font-weight: 600; 22 | color: ${(props) => props.theme.LIGHTGREY}; 23 | `; 24 | 25 | const ColorPalette = styled.input` 26 | margin: 20px 20px 10px 20px; 27 | width: 100px; 28 | height: 40px; 29 | `; 30 | 31 | const Button = styled.button` 32 | width: 90%; 33 | margin: 10px 0; 34 | background-color: ${(props) => props.theme.WHITE}; 35 | border: 1px solid ${(props) => props.theme.BLACK}; 36 | border-radius: 5px; 37 | &:hover { 38 | background-color: ${(props) => props.theme.GREEN}; 39 | color: ${(props) => props.theme.WHITE}; 40 | } 41 | `; 42 | 43 | interface ColorStyleProps { 44 | color: string; 45 | onStyleChange: (key: StyleDefaultKeyType, value: string | number) => void; 46 | } 47 | 48 | function ColorStyle({ 49 | color, 50 | onStyleChange, 51 | }: ColorStyleProps): React.ReactElement { 52 | const { 53 | curRange, 54 | rangeChangeHandler, 55 | rangeMouseUpHandler, 56 | initStyle, 57 | } = useInputRange({ 58 | range: color, 59 | onStyleChange, 60 | }); 61 | 62 | return ( 63 | 64 | 색상 65 | {curRange} 66 | 67 | rangeMouseUpHandler(StyleKeyType.color)} 72 | value={curRange} 73 | /> 74 | 75 | ); 76 | } 77 | 78 | export default ColorStyle; 79 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/DetailType.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import DetailTypeSubList from './DetailTypeSubList'; 5 | import Styler from './Styler'; 6 | 7 | // Hook 8 | import useSidebarType, { 9 | SidebarHookType, 10 | } from '../../../hooks/sidebar/useSidebarType'; 11 | import ListItem from './DetailTypeItem'; 12 | import useDetailType, { 13 | UseDetailHookType, 14 | } from '../../../hooks/sidebar/useDetailType'; 15 | 16 | // Type 17 | import { 18 | ElementNameType, 19 | SubElementNameType, 20 | } from '../../../store/common/type'; 21 | 22 | const DetailWrapper = styled.div` 23 | width: 230px; 24 | height: 100%; 25 | padding: 20px; 26 | 27 | overflow-y: scroll; 28 | border-left: 1px solid ${(props) => props.theme.LIGHTGREY}; 29 | `; 30 | 31 | const Title = styled.h2` 32 | padding-bottom: 40px; 33 | text-align: center; 34 | font-size: 2rem; 35 | font-weight: 600; 36 | `; 37 | 38 | const List = styled.ul` 39 | position: relative; 40 | margin-bottom: 30px; 41 | `; 42 | 43 | const Check = styled.div` 44 | position: absolute; 45 | font-size: 1.2rem; 46 | font-weight: 600; 47 | color: ${(props) => props.theme.GREEN}; 48 | `; 49 | 50 | function DetailType(): React.ReactElement { 51 | const { 52 | feature, 53 | element, 54 | sidebarTypeClickHandler, 55 | sidebarSubTypeClickHandler, 56 | }: SidebarHookType = useSidebarType(); 57 | 58 | const { 59 | detail: { section, labelText, labelIcon }, 60 | styleClickHandler, 61 | checkIsSelected, 62 | }: UseDetailHookType = useDetailType({ 63 | sidebarTypeClickHandler, 64 | sidebarSubTypeClickHandler, 65 | }); 66 | 67 | if (!feature) { 68 | return <>; 69 | } 70 | 71 | return ( 72 | <> 73 | 74 | 세부 유형 75 | {section ? ( 76 | 97 | ) : null} 98 | {labelText ? ( 99 | 125 | ) : null} 126 | {labelIcon ? ( 127 | 128 | {labelIcon.isChanged ? : <>} 129 | { 133 | styleClickHandler(ElementNameType.labelIcon); 134 | }} 135 | name="아이콘" 136 | /> 137 | 138 | ) : null} 139 | 140 | {element ? : <>} 141 | 142 | ); 143 | } 144 | 145 | export default DetailType; 146 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/DetailTypeItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | 4 | export type paddingStepType = 'first' | 'second' | 'third'; 5 | 6 | interface ListItemProps { 7 | isSelected: boolean; 8 | padding: paddingStepType; 9 | clickHandler: () => void; 10 | name: string; 11 | } 12 | 13 | interface PaddingProp { 14 | padding: paddingStepType; 15 | } 16 | 17 | interface ItemProps extends PaddingProp { 18 | isSelected: boolean; 19 | } 20 | 21 | export const paddingStep = { 22 | first: '0px', 23 | second: '15px', 24 | third: '30px', 25 | }; 26 | 27 | const Item = styled.li` 28 | position: relative; 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | margin: 10px 0; 33 | padding-left: ${(props) => paddingStep[props.padding]}; 34 | font-size: 1.7rem; 35 | color: ${(props) => 36 | props.isSelected ? props.theme.GREEN : props.theme.DARKGREY}; 37 | cursor: pointer; 38 | 39 | &:hover { 40 | color: ${(props) => 41 | props.isSelected ? props.theme.GREEN : props.theme.BLACK}; 42 | } 43 | `; 44 | 45 | const Pointer = styled.span``; 46 | 47 | function DetailTypeItem({ 48 | isSelected, 49 | padding, 50 | clickHandler, 51 | name, 52 | }: ListItemProps): React.ReactElement { 53 | return ( 54 | 55 | {name} 56 | {'>'} 57 | 58 | ); 59 | } 60 | 61 | export default DetailTypeItem; 62 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/DetailTypeSubList.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import ListItem, { paddingStepType, paddingStep } from './DetailTypeItem'; 5 | 6 | // Type 7 | import { 8 | ElementNameType, 9 | SubElementNameType, 10 | } from '../../../store/common/type'; 11 | 12 | interface PaddingProp { 13 | padding: paddingStepType; 14 | } 15 | 16 | export const List = styled.ul` 17 | position: relative; 18 | margin-bottom: 30px; 19 | `; 20 | 21 | export const Text = styled.h3` 22 | margin: 10px 0; 23 | padding-left: ${(props) => paddingStep[props.padding]}; 24 | font-size: 1.7rem; 25 | font-weight: 600; 26 | color: ${(props) => props.theme.GREY}; 27 | `; 28 | 29 | export const Check = styled.div` 30 | position: absolute; 31 | font-size: 1.2rem; 32 | font-weight: 600; 33 | color: ${(props) => props.theme.GREEN}; 34 | `; 35 | 36 | interface ChildrenType { 37 | isChanged?: boolean; 38 | title: string; 39 | elementName?: string; 40 | subElementName?: string; 41 | childrenProps: ChildrenType[]; 42 | } 43 | 44 | interface PropsType { 45 | title: string; 46 | checkIsSelected: ( 47 | elementName: ElementNameType, 48 | subElementName?: SubElementNameType | undefined 49 | ) => boolean; 50 | styleClickHandler: ( 51 | elementName: ElementNameType, 52 | subElementName?: SubElementNameType | undefined 53 | ) => void; 54 | childrenProps: ChildrenType[]; 55 | } 56 | 57 | function DetailTypeSubList({ 58 | title, 59 | checkIsSelected, 60 | styleClickHandler, 61 | childrenProps, 62 | }: PropsType): React.ReactElement { 63 | const childComponent = childrenProps.map((child: ChildrenType) => { 64 | return child.childrenProps.length !== 0 ? ( 65 |
66 | {child.title} 67 | {child.childrenProps.map((innerChild: ChildrenType) => { 68 | return ( 69 |
70 | {innerChild.isChanged ? : null} 71 | { 78 | styleClickHandler( 79 | innerChild.elementName as ElementNameType, 80 | innerChild.subElementName as SubElementNameType 81 | ); 82 | }} 83 | name={innerChild.title} 84 | /> 85 |
86 | ); 87 | })} 88 |
89 | ) : ( 90 |
91 | {child.isChanged ? : null} 92 | { 99 | styleClickHandler( 100 | child.elementName as ElementNameType, 101 | child.subElementName as SubElementNameType 102 | ); 103 | }} 104 | name={child.title} 105 | /> 106 |
107 | ); 108 | }); 109 | return ( 110 | 111 | {title} 112 | {childComponent} 113 | 114 | ); 115 | } 116 | 117 | export default DetailTypeSubList; 118 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/FeatureType.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import FeatureTypeItem from './FeatureTypeItem'; 5 | import DetailType from './DetailType'; 6 | 7 | // Data 8 | import data from '../../../utils/rendering-data/featureTypeData'; 9 | 10 | // Type 11 | import useSidebarType, { 12 | SidebarHookType, 13 | } from '../../../hooks/sidebar/useSidebarType'; 14 | 15 | interface WrapperProps { 16 | isFeatureName: string | null; 17 | } 18 | 19 | const FeatureTypeWrapper = styled.ul` 20 | width: ${(props) => (props.isFeatureName ? '250px' : '370px')}; 21 | padding: 20px; 22 | overflow-y: scroll; 23 | `; 24 | 25 | const FeatureTypeTitle = styled.h2` 26 | text-align: center; 27 | font-size: 2rem; 28 | font-weight: 600; 29 | padding-bottom: 40px; 30 | `; 31 | 32 | function FeatureType(): React.ReactElement { 33 | const { 34 | feature, 35 | sidebarTypeClickHandler, 36 | sidebarSubTypeClickHandler, 37 | }: SidebarHookType = useSidebarType(); 38 | 39 | return ( 40 | <> 41 | 42 | 기능 유형 43 | {data.map(({ typeKey, typeName, subFeatures }) => ( 44 | 52 | ))} 53 | 54 | 55 | 56 | ); 57 | } 58 | 59 | export default FeatureType; 60 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/FeatureTypeItem.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import useFeatureTypeItemHook from '../../../hooks/sidebar/useFeatureTypeItem'; 5 | 6 | // Data 7 | import { FeaturesType } from '../../../utils/rendering-data/featureTypeData'; 8 | 9 | // Type 10 | import { FeatureNameType, ElementNameType } from '../../../store/common/type'; 11 | 12 | interface ListProps { 13 | isChecked: boolean; 14 | } 15 | 16 | const FeatureList = styled.li` 17 | position: relative; 18 | width: 100%; 19 | display: flex; 20 | font-size: 1.8rem; 21 | font-weight: 600; 22 | padding: 10px 0; 23 | color: ${(props) => 24 | props.isChecked ? props.theme.GREEN : props.theme.DARKGREY}; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | color: ${(props) => 29 | props.isChecked ? props.theme.GREEN : props.theme.BLACK}; 30 | } 31 | `; 32 | 33 | const SectionList = styled.div` 34 | position: relative; 35 | width: 100%; 36 | display: flex; 37 | font-size: 1.5rem; 38 | font-weight: 600; 39 | padding: 10px 0 10px 10px; 40 | color: ${(props) => (props.isChecked ? props.theme.GREEN : props.theme.GREY)}; 41 | cursor: pointer; 42 | 43 | &:hover { 44 | color: ${(props) => 45 | props.isChecked ? props.theme.GREEN : props.theme.BLACK}; 46 | } 47 | `; 48 | 49 | const CheckTitle = styled.div` 50 | position: absolute; 51 | left: -14px; 52 | color: ${(props) => props.theme.GREEN}; 53 | `; 54 | 55 | const Check = styled.div` 56 | position: absolute; 57 | left: -7px; 58 | color: ${(props) => props.theme.GREEN}; 59 | `; 60 | 61 | const Pointer = styled.span` 62 | margin: 0 0 0 auto; 63 | `; 64 | 65 | interface FeatureTypeItemProps { 66 | typeKey: FeatureNameType; 67 | typeName: string; 68 | subFeatures: FeaturesType[]; 69 | sidebarTypeClickHandler: (name: FeatureNameType | ElementNameType) => void; 70 | sidebarSubTypeClickHandler: (name: string) => void; 71 | } 72 | 73 | function FeatureTypeItem({ 74 | typeKey, 75 | typeName, 76 | subFeatures, 77 | sidebarTypeClickHandler, 78 | sidebarSubTypeClickHandler, 79 | }: FeatureTypeItemProps): React.ReactElement { 80 | const { featureList, feature, subFeature } = useFeatureTypeItemHook({ 81 | featureName: typeKey, 82 | }); 83 | 84 | return ( 85 | <> 86 | { 89 | sidebarTypeClickHandler(typeKey); 90 | sidebarSubTypeClickHandler('all'); 91 | }} 92 | > 93 | {featureList.all?.isChanged ? : <>} 94 | {typeName} 95 | {'>'} 96 | 97 | {subFeatures.map(({ key, name }) => ( 98 | { 102 | sidebarTypeClickHandler(typeKey); 103 | sidebarSubTypeClickHandler(key); 104 | }} 105 | > 106 | {featureList[key]?.isChanged && } 107 | {name} 108 | {'>'} 109 | 110 | ))} 111 | 112 | ); 113 | } 114 | 115 | export default FeatureTypeItem; 116 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/LightnessStyle.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import { Range } from '../SidebarContentFewer/DepthItem'; 5 | 6 | // Hook 7 | import useInputRange from '../../../hooks/common/useInputRange'; 8 | 9 | // Type 10 | import { 11 | ColorSubStyleType, 12 | StyleDefaultKeyType, 13 | } from '../../../store/common/type'; 14 | 15 | const LightnessWrapper = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | margin: 15px 0; 19 | `; 20 | 21 | const LightnessTitle = styled.label` 22 | margin-bottom: 10px; 23 | font-size: 1.7rem; 24 | font-weight: 600; 25 | color: ${(props) => props.theme.GREY}; 26 | `; 27 | 28 | const LightnessControlBar = styled(Range)` 29 | width: 100%; 30 | height: 2px; 31 | `; 32 | 33 | interface LightnessPropsInterface { 34 | lightness: number; 35 | onStyleChange: (key: StyleDefaultKeyType, value: string | number) => void; 36 | } 37 | 38 | function LightnessStyle({ 39 | lightness, 40 | onStyleChange, 41 | }: LightnessPropsInterface): React.ReactElement { 42 | const { curRange, rangeChangeHandler, rangeMouseUpHandler } = useInputRange({ 43 | range: lightness, 44 | onStyleChange, 45 | }); 46 | 47 | return ( 48 |
49 | 50 | 밝기 51 | rangeChangeHandler(e)} 59 | onMouseUp={() => rangeMouseUpHandler(ColorSubStyleType.lightness)} 60 | /> 61 | 62 |
63 | ); 64 | } 65 | 66 | export default LightnessStyle; 67 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/SaturationStyle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import { Range } from '../SidebarContentFewer/DepthItem'; 4 | 5 | // Hook 6 | import useInputRange from '../../../hooks/common/useInputRange'; 7 | 8 | // Type 9 | import { 10 | StyleDefaultKeyType, 11 | ColorSubStyleType, 12 | } from '../../../store/common/type'; 13 | 14 | const SaturationWrapper = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | margin: 15px 0; 18 | `; 19 | 20 | const SaturationTitle = styled.label` 21 | margin-bottom: 10px; 22 | font-size: 1.7rem; 23 | font-weight: 600; 24 | color: ${(props) => props.theme.GREY}; 25 | `; 26 | 27 | const SaturationControlBar = styled(Range)` 28 | width: 100%; 29 | height: 2px; 30 | `; 31 | 32 | interface SaturationStyleProps { 33 | saturation: number; 34 | onStyleChange: (key: StyleDefaultKeyType, value: string | number) => void; 35 | } 36 | 37 | function SaturationStyle({ 38 | saturation, 39 | onStyleChange, 40 | }: SaturationStyleProps): React.ReactElement { 41 | const { curRange, rangeChangeHandler, rangeMouseUpHandler } = useInputRange({ 42 | range: saturation, 43 | onStyleChange, 44 | }); 45 | 46 | return ( 47 | 48 | 채도 49 | rangeChangeHandler(e)} 57 | onMouseUp={() => rangeMouseUpHandler(ColorSubStyleType.saturation)} 58 | /> 59 | 60 | ); 61 | } 62 | 63 | export default SaturationStyle; 64 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import FeatureType from './FeatureType'; 4 | 5 | const ContentWrapper = styled.div` 6 | position: relative; 7 | width: 100%; 8 | height: calc(100vh - 5.5rem - 63px); 9 | display: flex; 10 | flex-direction: row; 11 | `; 12 | 13 | function SidebarContent(): React.ReactElement { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default SidebarContent; 22 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/Styler.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import ColorStyle from './ColorStyle'; 5 | import SaturationStyle from './SaturationStyle'; 6 | import LightnessStyle from './LightnessStyle'; 7 | import WeightStyle from './WeightStyle'; 8 | import VisibilityStyle from './VisibilityStyle'; 9 | 10 | // Type 11 | import useStyleType, { 12 | UseStyleHookType, 13 | } from '../../../hooks/sidebar/useStyleType'; 14 | 15 | // Utils 16 | import { hexToHSL } from '../../../utils/colorFormat'; 17 | 18 | const StylerWrapper = styled.div` 19 | height: 100%; 20 | width: 230px; 21 | display: flex; 22 | flex-direction: column; 23 | border-left: 1px solid ${(props) => props.theme.LIGHTGREY}; 24 | padding: 20px 30px; 25 | overflow-y: scroll; 26 | `; 27 | 28 | const StylerTitle = styled.h2` 29 | font-size: 2rem; 30 | font-weight: 600; 31 | padding-bottom: 40px; 32 | text-align: center; 33 | `; 34 | 35 | const Hr = styled.hr` 36 | width: 80%; 37 | margin-top: 25px; 38 | margin-bottom: 25px; 39 | color: ${(props) => props.theme.GREY}; 40 | `; 41 | 42 | function Styler(): React.ReactElement { 43 | const { 44 | styleElement: { visibility, color, weight }, 45 | onStyleChange, 46 | subFeature, 47 | element, 48 | }: UseStyleHookType = useStyleType(); 49 | 50 | if (!element) { 51 | return <>; 52 | } 53 | 54 | const { s: saturation, l: lightness } = hexToHSL(color); 55 | 56 | return ( 57 | 58 | 스타일 59 | 64 |
65 | 66 |
67 | 68 | 69 | 70 |
71 | ); 72 | } 73 | 74 | export default Styler; 75 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/VisibilityStyle.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | 5 | // Type 6 | import { 7 | VisibilityValueType, 8 | StyleDefaultKeyType, 9 | StyleKeyType, 10 | } from '../../../store/common/type'; 11 | 12 | interface CheckedProp { 13 | checked: boolean; 14 | } 15 | 16 | const VisibilityWrapper = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | `; 20 | 21 | const VisibilityTitle = styled.h2` 22 | font-size: 1.7rem; 23 | font-weight: 600; 24 | color: ${(props) => props.theme.GREY}; 25 | margin: 10px 0; 26 | `; 27 | 28 | const VisibilityItem = styled.div` 29 | display: flex; 30 | align-items: center; 31 | font-size: 1.5rem; 32 | margin: 5px 10px; 33 | cursor: pointer; 34 | `; 35 | 36 | const Checkbox = styled.div` 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | margin-right: 10px; 41 | width: 18px; 42 | height: 18px; 43 | border-radius: 9px; 44 | background-color: ${(props) => 45 | props.checked ? props.theme.GREEN : 'lightgray'}; 46 | `; 47 | 48 | const Circle = styled.div` 49 | width: 10px; 50 | height: 10px; 51 | border-radius: 5px; 52 | background-color: ${(props) => (props.checked ? 'white' : 'lightgray')}; 53 | `; 54 | 55 | interface VisibilityStyleProps { 56 | visibility: string; 57 | subFeature: string | null; 58 | onStyleChange: (key: StyleDefaultKeyType, value: string | number) => void; 59 | } 60 | 61 | function VisibilityStyle({ 62 | visibility, 63 | onStyleChange, 64 | subFeature, 65 | }: VisibilityStyleProps): React.ReactElement { 66 | const list = [ 67 | { title: '상위와 동일', value: 'inherit' }, 68 | { title: '보임', value: 'visible' }, 69 | { title: '숨김', value: 'none' }, 70 | ]; 71 | 72 | return ( 73 | 74 | 가시성 75 | {list.map((item) => { 76 | if (subFeature === 'all' && item.value === VisibilityValueType.inherit) 77 | return null; 78 | return ( 79 | { 82 | onStyleChange(StyleKeyType.visibility, item.value); 83 | }} 84 | > 85 | 86 | 87 | 88 | {item.title} 89 | 90 | ); 91 | })} 92 | 93 | ); 94 | } 95 | 96 | export default VisibilityStyle; 97 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarContentMore/WeightStyle.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import { Range } from '../SidebarContentFewer/DepthItem'; 5 | 6 | // Hook 7 | import useInputRange from '../../../hooks/common/useInputRange'; 8 | 9 | // Type 10 | import { StyleDefaultKeyType, StyleKeyType } from '../../../store/common/type'; 11 | 12 | const WeightWrapper = styled.div` 13 | display: flex; 14 | flex-direction: column; 15 | margin: 15px 0; 16 | `; 17 | 18 | const WeightTitle = styled.label` 19 | margin-bottom: 10px; 20 | font-size: 1.7rem; 21 | font-weight: 600; 22 | color: ${(props) => props.theme.GREY}; 23 | `; 24 | 25 | const WeightControlBar = styled(Range)` 26 | width: 100%; 27 | height: 2px; 28 | `; 29 | 30 | interface WeightStyleProps { 31 | weight: number; 32 | onStyleChange: (key: StyleDefaultKeyType, value: string | number) => void; 33 | } 34 | 35 | function WeightStyle({ 36 | weight, 37 | onStyleChange, 38 | }: WeightStyleProps): React.ReactElement { 39 | const { curRange, rangeChangeHandler, rangeMouseUpHandler } = useInputRange({ 40 | range: weight, 41 | onStyleChange, 42 | }); 43 | 44 | return ( 45 | 46 | 굵기 47 | rangeMouseUpHandler(StyleKeyType.weight)} 56 | /> 57 | 58 | ); 59 | } 60 | 61 | export default WeightStyle; 62 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarFooter/SidebarFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from 'react'; 2 | import useSidebarFooter from '../../../hooks/sidebar/useSidebarFooter'; 3 | import styled from '../../../utils/styles/styled'; 4 | import ExportModal from '../SidebarModal/ExportModal'; 5 | 6 | const FooterWrapper = styled.footer` 7 | display: flex; 8 | justify-content: flex-end; 9 | align-items: center; 10 | padding: 10px; 11 | box-shadow: -15px 5px 40px -25px ${(props) => props.theme.BLACK}; 12 | `; 13 | 14 | const Button = styled.button` 15 | width: 100px; 16 | background-color: ${(props) => props.theme.WHITE}; 17 | border: none; 18 | border-radius: 5px; 19 | padding: 12px 0; 20 | font-size: 1.5em; 21 | font-weight: 600; 22 | color: ${(props) => props.theme.GREEN}; 23 | 24 | &:hover { 25 | color: ${(props) => props.theme.BLACK}; 26 | } 27 | `; 28 | 29 | interface SidebarFooterProps { 30 | isAdvanced: boolean; 31 | setIsAdvanced: Dispatch>; 32 | } 33 | 34 | function SidebarFooter({ 35 | isAdvanced, 36 | setIsAdvanced, 37 | }: SidebarFooterProps): React.ReactElement { 38 | const { isOpen, onClickExport, onCloseModal, style } = useSidebarFooter(); 39 | 40 | return ( 41 | 42 | 45 | 46 | 47 | 48 | ); 49 | } 50 | 51 | export default SidebarFooter; 52 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarHeader/SidebarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../../../utils/styles/styled'; 3 | import ImportModal from '../SidebarModal/ImportModal'; 4 | import Overlay from '../../common/Overlay'; 5 | import useSidebarDropdown, { 6 | useSidebarDropdownType, 7 | } from '../../../hooks/sidebar/useSidebarDropdown'; 8 | 9 | const DropdownWrapper = styled.div` 10 | position: absolute; 11 | top: 50px; 12 | right: -100px; 13 | width: 150px; 14 | padding: 5px 10px; 15 | background-color: ${(props) => props.theme.WHITE}; 16 | box-shadow: 0 0 10px ${(props) => props.theme.GREY}; 17 | border-radius: 7px; 18 | z-index: 10; 19 | `; 20 | 21 | const DropdownItem = styled.div` 22 | width: 90%; 23 | margin: 10px 0; 24 | font-size: 1.5rem; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | color: ${(props) => props.theme.GREEN}; 29 | } 30 | `; 31 | 32 | interface SidebarDropdownProps { 33 | isOpened: boolean; 34 | dropdownToggleHandler: () => void; 35 | } 36 | 37 | function SidebarDropdownPresenter({ 38 | isOpened, 39 | dropdownToggleHandler, 40 | }: SidebarDropdownProps): React.ReactElement { 41 | const { 42 | importModalToggleHandler, 43 | resetClickHandler, 44 | isModalOpened, 45 | }: useSidebarDropdownType = useSidebarDropdown({ 46 | isOpened, 47 | dropdownToggleHandler, 48 | }); 49 | 50 | return ( 51 | <> 52 | {isOpened && ( 53 | <> 54 | 55 | 56 | 초기화 57 | 58 | JSON 불러오기 59 | 60 | 61 | 62 | )} 63 | {isModalOpened && ( 64 | 65 | )} 66 | 67 | ); 68 | } 69 | 70 | export default SidebarDropdownPresenter; 71 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarHeader/SidebarHeader.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import UndoIcon from '../../Icon/UndoIcon'; 5 | import RedoIcon from '../../Icon/RedoIcon'; 6 | import MoreVertIcon from '../../Icon/MoreVertIcon'; 7 | import SidebarDropdown from './SidebarDropdown'; 8 | 9 | // Hook 10 | import useUndoRedo from '../../../hooks/sidebar/useUndoRedo'; 11 | import useSidebarHeader, { 12 | useSidebarHeaderType, 13 | } from '../../../hooks/sidebar/useSidebarHeader'; 14 | 15 | const HeaderWrapper = styled.header` 16 | flex: 0 0 auto; 17 | height: 5.5rem; 18 | width: 100%; 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | padding: 0 15px 0 25px; 23 | background-color: ${(props) => props.theme.GREEN}; 24 | `; 25 | 26 | const HeaderTitle = styled.h1` 27 | font-size: 2.5rem; 28 | color: ${(props) => props.theme.WHITE}; 29 | width: 60%; 30 | `; 31 | 32 | const Btns = styled.div` 33 | position: relative; 34 | flex: 0 0 content; 35 | display: flex; 36 | align-items: center; 37 | width: 40%; 38 | height: 100%; 39 | `; 40 | 41 | const UndoBtn = styled(UndoIcon)` 42 | margin: 0 0 0 auto; 43 | fill: ${(props) => props.theme.WHITE}; 44 | cursor: pointer; 45 | 46 | &:hover { 47 | fill: ${(props) => props.theme.DARKGREY}; 48 | } 49 | `; 50 | 51 | const RedoBtn = styled(RedoIcon)` 52 | margin: 0 0 0 10px; 53 | fill: ${(props) => props.theme.WHITE}; 54 | cursor: pointer; 55 | 56 | &:hover { 57 | fill: ${(props) => props.theme.DARKGREY}; 58 | } 59 | `; 60 | 61 | const DropdownBtn = styled(MoreVertIcon)` 62 | margin-left: 10px; 63 | fill: ${(props) => props.theme.WHITE}; 64 | cursor: pointer; 65 | 66 | &:hover { 67 | fill: ${(props) => props.theme.DARKGREY}; 68 | } 69 | `; 70 | 71 | interface SidebarHeaderPresenterProps { 72 | isAdvanced: boolean; 73 | } 74 | 75 | function SidebarHeader({ 76 | isAdvanced, 77 | }: SidebarHeaderPresenterProps): React.ReactElement { 78 | const { 79 | isOpened, 80 | dropdownToggleHandler, 81 | }: useSidebarHeaderType = useSidebarHeader(); 82 | const { undoHandler, redoHandler } = useUndoRedo(); 83 | 84 | return ( 85 | 86 | {isAdvanced ? '고급 설정' : '스타일 맵 만들기'} 87 | 88 | 89 | 90 | 91 | 95 | 96 | 97 | ); 98 | } 99 | 100 | export default SidebarHeader; 101 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarModal/ExportModal.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { useEffect } from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import CloseIcon from '../../Icon/CloseIcon'; 5 | import Copy from '../../Icon/Copy'; 6 | import Overlay from '../../common/Overlay'; 7 | import { 8 | ModalWrapper, 9 | ModalHeader, 10 | ModalTitle, 11 | ModalCloseButton, 12 | } from './common'; 13 | 14 | // Hook 15 | import { 16 | ExportType, 17 | getStringifyStyleObject, 18 | geturlParsedStyle, 19 | } from '../../../hooks/sidebar/useExportStyle'; 20 | import useCopyToClipboard from '../../../hooks/sidebar/useCopyToClipboard'; 21 | 22 | const ExportModalWrapper = styled(ModalWrapper)``; 23 | 24 | const ModalBody = styled.div` 25 | width: 100%; 26 | height: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: space-between; 30 | `; 31 | 32 | const Content = styled.article` 33 | font-size: 1.3rem; 34 | overflow-y: scroll; 35 | height: 55%; 36 | word-break: break-all; 37 | white-space: normal; 38 | color: ${(props) => props.theme.GREY}; 39 | line-height: 16px; 40 | background-color: ${(props) => props.theme.GOOGLE_GREY}; 41 | padding: 10px; 42 | -ms-overflow-style: none; 43 | &::-webkit-scrollbar { 44 | display: none !important; 45 | } 46 | `; 47 | 48 | const ExportToJson = styled.div` 49 | padding: 0px 30px; 50 | height: 50%; 51 | `; 52 | 53 | const ExportToURL = styled.div` 54 | padding: 0px 30px; 55 | height: 50%; 56 | `; 57 | 58 | const SubTitle = styled.h2` 59 | padding: 10px 0; 60 | font-size: 1.6rem; 61 | color: ${(props) => props.theme.GREEN}; 62 | display: flex; 63 | `; 64 | 65 | const CopyBtn = styled(Copy)` 66 | margin: 0 10px; 67 | `; 68 | 69 | const CopyStatus = styled.div` 70 | color: black; 71 | `; 72 | 73 | function ExportModal({ 74 | isOpen, 75 | onClose, 76 | style, 77 | }: { 78 | isOpen: boolean; 79 | onClose: () => void; 80 | style: ExportType; 81 | }): React.ReactElement { 82 | const { 83 | completeUrlCopy, 84 | completeJsonCopy, 85 | setCompleteUrlCopy, 86 | setCompleteJsonCopy, 87 | copyToClipboard, 88 | } = useCopyToClipboard(); 89 | 90 | useEffect(() => { 91 | setCompleteJsonCopy(false); 92 | setCompleteUrlCopy(false); 93 | }, [style]); 94 | 95 | if (!isOpen) return <>; 96 | 97 | const stringifiedStyleObject = getStringifyStyleObject(style); 98 | const urlParsedStyle = geturlParsedStyle(style); 99 | 100 | return ( 101 | <> 102 | 103 | 104 | 105 | 스타일 내보내기 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | JSON 형식으로 내보내기 114 | { 116 | copyToClipboard({ newJson: stringifiedStyleObject }); 117 | }} 118 | /> 119 | {completeJsonCopy ? '복사완료' : ''} 120 | 121 | 122 | {stringifiedStyleObject} 123 | 124 | 125 | 126 | URL로 내보내기 127 | copyToClipboard({ newUrl: urlParsedStyle })} 129 | /> 130 | {completeUrlCopy ? '복사완료' : ''} 131 | 132 | {urlParsedStyle} 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | 140 | export default ExportModal; 141 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarModal/ImportModal.tsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import styled from '../../../utils/styles/styled'; 4 | import Overlay from '../../common/Overlay'; 5 | import CloseIcon from '../../Icon/CloseIcon'; 6 | import { 7 | ModalWrapper, 8 | ModalHeader, 9 | ModalTitle, 10 | ModalCloseButton, 11 | } from './common'; 12 | 13 | // Hook 14 | import useSidebarImportModal, { 15 | useModalStatusProps, 16 | useModalStatusType, 17 | } from '../../../hooks/sidebar/useImportModalStatus'; 18 | import useInputText, { 19 | InputTextHookType, 20 | } from '../../../hooks/common/useInputText'; 21 | 22 | const ModalInput = styled.textarea` 23 | width: 100%; 24 | height: 480px; 25 | padding: 10px; 26 | outline: none; 27 | border-left: none; 28 | border-right: none; 29 | background-color: ${(props) => props.theme.GOOGLE_GREY}; 30 | `; 31 | 32 | interface ModalButtonProps { 33 | inputStatus: boolean; 34 | } 35 | 36 | const ModalOKButton = styled.button` 37 | position: relative; 38 | background-color: transparent; 39 | padding: 20px 0; 40 | color: ${(props) => 41 | props.inputStatus ? props.theme.GREEN : props.theme.RED}; 42 | width: 100%; 43 | border: none; 44 | font-weight: 600; 45 | font-size: 1.6rem; 46 | 47 | &:hover { 48 | color: ${(props) => 49 | props.inputStatus ? props.theme.WHITE : props.theme.RED}; 50 | background-color: ${(props) => 51 | props.inputStatus ? props.theme.GREEN : props.theme.WHITE}; 52 | } 53 | `; 54 | 55 | function ImportModal({ 56 | importModalToggleHandler, 57 | }: useModalStatusProps): React.ReactElement { 58 | const { 59 | inputStatus, 60 | onClickClose, 61 | onClickOK, 62 | }: useModalStatusType = useSidebarImportModal({ 63 | importModalToggleHandler, 64 | }); 65 | const { inputText, onInputChange }: InputTextHookType = useInputText(); 66 | 67 | return ( 68 | <> 69 | 70 | 71 | 72 | JSON 불러오기 73 | 74 | 75 | 76 | 77 | 82 | onClickOK(inputText)} 85 | > 86 | {inputStatus ? '지도 가져오기' : ' 잘못된 입력입니다'} 87 | 88 | 89 | 90 | ); 91 | } 92 | 93 | export default ImportModal; 94 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarModal/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from '../../../utils/styles/styled'; 2 | 3 | const ModalWrapper = styled.div` 4 | position: fixed; 5 | top: 50%; 6 | left: 50%; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | 12 | width: 500px; 13 | height: 500px; 14 | 15 | transform: translate(-50%, -50%); 16 | border: 0; 17 | border-radius: 8px; 18 | background-color: ${(props) => props.theme.WHITE}; 19 | box-shadow: 0 0 10px ${(props) => props.theme.GREY}; 20 | z-index: 30; 21 | overflow: hidden; 22 | `; 23 | 24 | const ModalHeader = styled.div` 25 | position: relative; 26 | display: flex; 27 | align-self: center; 28 | justify-content: space-between; 29 | width: 100%; 30 | padding: 15px; 31 | height: 5rem; 32 | `; 33 | 34 | const ModalTitle = styled.h2` 35 | width: 100%; 36 | height: 50px; 37 | text-align: center; 38 | font-size: 2rem; 39 | font-weight: 600; 40 | flex: 0 0 content; 41 | `; 42 | 43 | const ModalCloseButton = styled.button` 44 | position: absolute; 45 | top: 10px; 46 | right: 7px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | align-self: center; 51 | width: 30px; 52 | height: 30px; 53 | padding: 5px 0; 54 | 55 | border-radius: 5px; 56 | background-color: ${(props) => props.theme.WHITE}; 57 | text-align: center; 58 | border: none; 59 | 60 | &:hover { 61 | color: ${(props) => props.theme.GREEN}; 62 | } 63 | `; 64 | 65 | export { ModalWrapper, ModalHeader, ModalTitle, ModalCloseButton }; 66 | -------------------------------------------------------------------------------- /src/components/common/Overlay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from '../../utils/styles/styled'; 3 | 4 | interface OverlayProps { 5 | color?: string; 6 | toggleHandler: () => void; 7 | } 8 | 9 | interface OverlayDivProps { 10 | color?: string; 11 | } 12 | 13 | const OverlayDiv = styled.div` 14 | position: fixed; 15 | top: -100vh; 16 | left: -100vw; 17 | width: 200vw; 18 | height: 200vh; 19 | 20 | background-color: ${(props) => (props.color ? props.color : 'transparent')}; 21 | opacity: 0.6; 22 | z-index: 10; 23 | `; 24 | 25 | function Overlay({ color, toggleHandler }: OverlayProps): React.ReactElement { 26 | const [open, setOpen] = useState(true); 27 | 28 | const clickHandler = () => { 29 | toggleHandler(); 30 | setOpen(false); 31 | }; 32 | 33 | if (!open) return <>; 34 | return ; 35 | } 36 | 37 | export default Overlay; 38 | -------------------------------------------------------------------------------- /src/hooks/common/useInputRange.ts: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { StyleDefaultKeyType } from '../../store/common/type'; 3 | 4 | interface UseInputRangeProps { 5 | range: string | number; 6 | onStyleChange: (key: StyleDefaultKeyType, value: string | number) => void; 7 | } 8 | 9 | interface InputRangeHookType { 10 | curRange: string | number; 11 | rangeChangeHandler: (e: React.ChangeEvent) => void; 12 | rangeMouseUpHandler: (key: StyleDefaultKeyType) => void; 13 | initStyle: (key: StyleDefaultKeyType) => void; 14 | } 15 | 16 | function useInputRange({ 17 | range, 18 | onStyleChange, 19 | }: UseInputRangeProps): InputRangeHookType { 20 | const [curRange, setCurRange] = useState(range); 21 | 22 | useEffect(() => { 23 | setCurRange(range); 24 | }, [range]); 25 | 26 | const rangeChangeHandler = (e: React.ChangeEvent) => { 27 | const value = Number.isNaN(Number(e.target.value)) 28 | ? e.target.value 29 | : Number(e.target.value); 30 | setCurRange(value); 31 | }; 32 | 33 | const rangeMouseUpHandler = (key: StyleDefaultKeyType) => { 34 | onStyleChange(key, curRange === 'transparent' ? '#000000' : curRange); 35 | }; 36 | 37 | const initStyle = (key: StyleDefaultKeyType) => { 38 | onStyleChange(key, 'transparent'); 39 | }; 40 | 41 | return { 42 | curRange, 43 | rangeChangeHandler, 44 | rangeMouseUpHandler, 45 | initStyle, 46 | }; 47 | } 48 | 49 | export default useInputRange; 50 | -------------------------------------------------------------------------------- /src/hooks/common/useInputText.ts: -------------------------------------------------------------------------------- 1 | import React, { useState, ChangeEvent } from 'react'; 2 | 3 | type InputType = HTMLInputElement | HTMLTextAreaElement; 4 | export interface InputTextHookType { 5 | inputText: string; 6 | onInputChange: (e: ChangeEvent) => void; 7 | } 8 | 9 | function useInputText(): InputTextHookType { 10 | const [inputText, setInputText] = useState(''); 11 | const onInputChange = (e: React.ChangeEvent) => { 12 | const newInputText = e.target.value; 13 | setInputText(newInputText); 14 | }; 15 | 16 | return { 17 | inputText, 18 | onInputChange, 19 | }; 20 | } 21 | 22 | export default useInputText; 23 | -------------------------------------------------------------------------------- /src/hooks/common/useWholeStyle.ts: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { useEffect, useState } from 'react'; 3 | import mapboxgl from 'mapbox-gl'; 4 | 5 | // Redux 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { RootState } from '../../store'; 8 | import { setWholeStyle, replaceWholeStyle } from '../../store/style/action'; 9 | import { addLog } from '../../store/history/action'; 10 | 11 | // Util 12 | import setFeatureStyle from '../../utils/setFeatureStyle'; 13 | 14 | // Type 15 | import { 16 | WholeStyleActionPayload, 17 | FeatureState, 18 | FeatureNameType, 19 | StyleStoreType, 20 | ReplaceType, 21 | } from '../../store/common/type'; 22 | 23 | type LogInfoType = { changedKey: ReplaceType; changedValue?: string }; 24 | type FlagType = LogInfoType | boolean; 25 | interface WholeStyleHook { 26 | flag: LogInfoType | boolean; 27 | getWholeStyle: () => StyleStoreType; 28 | changeStyle: ( 29 | inputStyle: WholeStyleActionPayload, 30 | logInfo?: LogInfoType 31 | ) => void; 32 | replaceStyle: (inputStyle: StyleStoreType) => void; 33 | } 34 | 35 | function useWholeStyle(): WholeStyleHook { 36 | const [flag, setFlag] = useState(false); 37 | const dispatch = useDispatch(); 38 | 39 | const map = useSelector((state) => state.map.map) as mapboxgl.Map; 40 | const { poi, landscape, administrative, road, transit, water } = useSelector< 41 | RootState 42 | >((state) => state) as StyleStoreType; 43 | 44 | useEffect(() => { 45 | if (!flag || !map) return; 46 | const stores: StyleStoreType = { 47 | poi, 48 | landscape, 49 | administrative, 50 | road, 51 | transit, 52 | water, 53 | }; 54 | 55 | const features = Object.keys(stores) as FeatureNameType[]; 56 | // eslint-disable-next-line no-restricted-syntax 57 | for (const feature of features) { 58 | setFeatureStyle({ 59 | map, 60 | feature: feature as FeatureNameType, 61 | featureState: stores[feature] as FeatureState, 62 | }); 63 | } 64 | if (flag !== true) { 65 | dispatch(addLog({ ...flag, wholeStyle: stores })); 66 | } 67 | setFlag(false); 68 | }, [flag]); 69 | 70 | const getWholeStyle = () => { 71 | return { 72 | poi, 73 | landscape, 74 | administrative, 75 | road, 76 | transit, 77 | water, 78 | }; 79 | }; 80 | 81 | const changeStyle = ( 82 | inputStyle: WholeStyleActionPayload, 83 | logInfo?: LogInfoType 84 | ): void => { 85 | dispatch(setWholeStyle(inputStyle)); 86 | setFlag(logInfo || true); 87 | }; 88 | 89 | const replaceStyle = (inputStyle: StyleStoreType): void => { 90 | dispatch(replaceWholeStyle(inputStyle)); 91 | setFlag(true); 92 | }; 93 | 94 | return { 95 | flag, 96 | getWholeStyle, 97 | changeStyle, 98 | replaceStyle, 99 | }; 100 | } 101 | 102 | export default useWholeStyle; 103 | -------------------------------------------------------------------------------- /src/hooks/map/getCompareMap.js: -------------------------------------------------------------------------------- 1 | import Compare from 'mapbox-gl-compare'; 2 | 3 | const getCompareMap = (beforeMap, afterMap, comparisonMapRef) => { 4 | return new Compare(beforeMap, afterMap, comparisonMapRef, { 5 | // mousemove: true, 6 | orientation: 'vertical', 7 | }); 8 | }; 9 | 10 | export default getCompareMap; 11 | -------------------------------------------------------------------------------- /src/hooks/map/useCompareFeature.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable guard-for-in */ 3 | // Dependencies 4 | import { useState, RefObject, useEffect } from 'react'; 5 | import mapboxgl from 'mapbox-gl'; 6 | import getCompareMap from './getCompareMap'; 7 | 8 | // Redux 9 | import { useSelector } from 'react-redux'; 10 | import { RootState } from '../../store'; 11 | 12 | // Type 13 | import { 14 | HistoryState, 15 | FeatureNameType, 16 | FeatureState, 17 | } from '../../store/common/type'; 18 | 19 | // Util 20 | import initLayers from '../../utils/rendering-data/layers/init'; 21 | import setFeatureStyle from '../../utils/setFeatureStyle'; 22 | 23 | export interface mapProps { 24 | container: string | HTMLElement; 25 | [key: string]: string | HTMLElement; 26 | } 27 | 28 | export interface useComparisonButtonType { 29 | logId: string | undefined; 30 | setLogId: (id: string | undefined) => void; 31 | comparisonButtonClickHandler: (id: string) => void; 32 | } 33 | 34 | export interface useCompareFeatureProps { 35 | containerRef: RefObject; 36 | beforeMapRef: RefObject; 37 | } 38 | interface ReduxStateType extends HistoryState { 39 | map: mapboxgl.Map; 40 | } 41 | 42 | function useCompareFeature({ 43 | containerRef, 44 | beforeMapRef, 45 | }: useCompareFeatureProps): useComparisonButtonType { 46 | const [logId, setLogId] = useState(); 47 | const [beforeMap, setBeforeMap] = useState(null); 48 | const { map, log } = useSelector((state) => ({ 49 | map: state.map.map, 50 | log: state.history.log, 51 | })) as ReduxStateType; 52 | 53 | useEffect(() => { 54 | if (!map) return; 55 | 56 | const newMap = new mapboxgl.Map({ 57 | container: beforeMapRef.current as HTMLDivElement, 58 | style: initLayers as mapboxgl.Style, 59 | center: [map.getCenter().lng, map.getCenter().lat], 60 | zoom: map.getZoom(), 61 | }); 62 | 63 | setBeforeMap(newMap); 64 | }, [map]); 65 | 66 | useEffect(() => { 67 | if (!map || !logId || !beforeMap || !log) return; 68 | 69 | const [item] = log?.filter((val) => val.id === logId) || []; 70 | const { wholeStyle } = item; 71 | 72 | for (const feature in wholeStyle) { 73 | setFeatureStyle({ 74 | map: beforeMap as mapboxgl.Map, 75 | feature: feature as FeatureNameType, 76 | featureState: item.wholeStyle[ 77 | feature as FeatureNameType 78 | ] as FeatureState, 79 | }); 80 | } 81 | const compare = getCompareMap(beforeMap, map, containerRef.current); 82 | 83 | // eslint-disable-next-line consistent-return 84 | return () => { 85 | compare.remove(); 86 | }; 87 | }, [logId]); 88 | 89 | const comparisonButtonClickHandler = (newLogId: string) => { 90 | if (newLogId === logId) { 91 | setLogId(undefined); 92 | return; 93 | } 94 | setLogId(newLogId); 95 | }; 96 | 97 | return { 98 | logId, 99 | setLogId, 100 | comparisonButtonClickHandler, 101 | }; 102 | } 103 | 104 | export default useCompareFeature; 105 | -------------------------------------------------------------------------------- /src/hooks/map/useHistoryFeature.ts: -------------------------------------------------------------------------------- 1 | // Redux 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { RootState } from '../../store/index'; 4 | import { resetHistory } from '../../store/history/action'; 5 | import { initDepthTheme } from '../../store/depth-theme/action'; 6 | 7 | // Type 8 | import { HistoryState, HistoryInfoPropsType } from '../../store/common/type'; 9 | 10 | // Hook 11 | import useWholeStyle from '../common/useWholeStyle'; 12 | 13 | export interface useHistoryFeatureType { 14 | log?: HistoryInfoPropsType[]; 15 | currentIdx: number | null; 16 | resetHistoryAndStyle: () => void; 17 | } 18 | 19 | function useHistoryFeature(): useHistoryFeatureType { 20 | const { log, currentIdx } = useSelector( 21 | (state) => state.history 22 | ) as HistoryState; 23 | const { changeStyle } = useWholeStyle(); 24 | 25 | const dispatch = useDispatch(); 26 | 27 | const resetHistoryAndStyle = () => { 28 | dispatch(resetHistory()); 29 | dispatch(initDepthTheme()); 30 | changeStyle({}); 31 | }; 32 | 33 | return { log, currentIdx, resetHistoryAndStyle }; 34 | } 35 | 36 | export default useHistoryFeature; 37 | -------------------------------------------------------------------------------- /src/hooks/map/useHistoryMap.ts: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { useState, useEffect } from 'react'; 3 | 4 | // Redux 5 | import { useSelector } from 'react-redux'; 6 | import { RootState } from '../../store/index'; 7 | 8 | // Type 9 | import { HistoryState } from '../../store/common/type'; 10 | 11 | export interface useHistoryMapType { 12 | isHistoryOpen: boolean; 13 | historyBtnHandler: () => void; 14 | } 15 | 16 | const LOCALSTORAGE_MAX_LOG_COUNT = 50; 17 | 18 | function useHistoryMap(): useHistoryMapType { 19 | const [isHistoryOpen, setIsHistoryOpen] = useState(false); 20 | const { log } = useSelector( 21 | (state) => state.history 22 | ) as HistoryState; 23 | 24 | const historyBtnHandler = () => { 25 | setIsHistoryOpen(!isHistoryOpen); 26 | }; 27 | 28 | useEffect(() => { 29 | window.onbeforeunload = function setLog(): void { 30 | if (log !== undefined) { 31 | const fiftyLog = 32 | log.length <= LOCALSTORAGE_MAX_LOG_COUNT 33 | ? log 34 | : log.splice(log.length - LOCALSTORAGE_MAX_LOG_COUNT); 35 | localStorage.setItem('log', JSON.stringify(fiftyLog)); 36 | } 37 | }; 38 | 39 | return () => { 40 | window.onbeforeunload = null; 41 | }; 42 | }, [log]); 43 | 44 | return { 45 | isHistoryOpen, 46 | historyBtnHandler, 47 | }; 48 | } 49 | 50 | export default useHistoryMap; 51 | -------------------------------------------------------------------------------- /src/hooks/map/useLowerButtons.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { RootState } from '../../store'; 3 | 4 | export interface LowerButtonsHookType { 5 | plusZoom: () => void; 6 | minusZoom: () => void; 7 | } 8 | 9 | function useLowerButtons(): LowerButtonsHookType { 10 | const map = useSelector((state: RootState) => state.map.map); 11 | 12 | const plusZoom = () => { 13 | if (!map) return; 14 | const zoom = map?.getZoom() as number; 15 | map?.flyTo({ zoom: zoom + 1 }); 16 | }; 17 | 18 | const minusZoom = () => { 19 | if (!map) return; 20 | const zoom = map?.getZoom() as number; 21 | map?.flyTo({ zoom: zoom - 1 }); 22 | }; 23 | 24 | return { 25 | plusZoom, 26 | minusZoom, 27 | }; 28 | } 29 | 30 | export default useLowerButtons; 31 | -------------------------------------------------------------------------------- /src/hooks/map/useMap.ts: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { RefObject, useRef, useEffect, useState } from 'react'; 3 | 4 | // Redux 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { initMarker, MarkerState } from '../../store/marker/action'; 7 | import { initHistory } from '../../store/history/action'; 8 | import { RootState } from '../../store/index'; 9 | import { initMap } from '../../store/map/action'; 10 | 11 | // Util 12 | import { getInitialMarkersFromLocalStorage } from '../../utils/updateMarkerStorage'; 13 | import { urlToJson } from '../../utils/urlParsing'; 14 | import validateStyle from '../../utils/validateStyle'; 15 | 16 | // Hook 17 | import useMarkerRegister from './useMarkerRegister'; 18 | import useWholeStyle from '../common/useWholeStyle'; 19 | 20 | // Type 21 | import { 22 | WholeStyleActionPayload, 23 | HistoryState, 24 | LocationType, 25 | URLPathNameType, 26 | } from '../../store/common/type'; 27 | 28 | export interface MapHookType { 29 | containerRef: RefObject; 30 | afterMapRef: RefObject; 31 | beforeMapRef: RefObject; 32 | } 33 | 34 | interface ReduxStateType { 35 | history: HistoryState; 36 | marker: MarkerState; 37 | } 38 | 39 | function useMap(): MapHookType { 40 | const dispatch = useDispatch(); 41 | const containerRef = useRef(null); 42 | const afterMapRef = useRef(null); 43 | const beforeMapRef = useRef(null); 44 | const { 45 | history: { log, currentIdx }, 46 | marker, 47 | } = useSelector((state) => ({ 48 | history: state.history, 49 | marker: state.marker, 50 | })) as ReduxStateType; 51 | 52 | const { changeStyle, replaceStyle } = useWholeStyle(); 53 | const { registerMarker } = useMarkerRegister(); 54 | const [flag, setFlag] = useState(false); 55 | const { pathname } = window.location; 56 | 57 | const initializeMap = (map: mapboxgl.Map): void => { 58 | const { filteredStyle, mapCoordinate } = urlToJson(); 59 | 60 | if (validateStyle(filteredStyle as WholeStyleActionPayload)) { 61 | changeStyle(filteredStyle as WholeStyleActionPayload); 62 | } else { 63 | // eslint-disable-next-line no-alert 64 | alert('URL에 잘못된 속성이 포함되어 있습니다.'); 65 | } 66 | 67 | if (mapCoordinate) { 68 | const { zoom, lng, lat } = mapCoordinate as LocationType; 69 | if (zoom && lng && lat) { 70 | map.setCenter({ lng, lat }); 71 | map.setZoom(zoom); 72 | } 73 | } 74 | 75 | if (pathname !== URLPathNameType.show) { 76 | dispatch(initHistory()); 77 | } 78 | 79 | const storedMarkers = getInitialMarkersFromLocalStorage(); 80 | dispatch(initMarker(storedMarkers)); 81 | 82 | setFlag(true); 83 | }; 84 | 85 | const printMarker = (): void => { 86 | if (!marker) return; 87 | marker.markers.forEach((item) => { 88 | registerMarker({ 89 | id: item.id, 90 | text: item.text, 91 | lngLat: { lng: item.lng, lat: item.lat }, 92 | instance: item.instance as mapboxgl.Marker, 93 | }); 94 | }); 95 | }; 96 | 97 | useEffect(() => { 98 | if (!flag) return; 99 | if (log && log.length) { 100 | const { wholeStyle } = log[currentIdx as number]; 101 | if (wholeStyle) replaceStyle(wholeStyle); 102 | } 103 | 104 | if (marker && marker.markers.length) { 105 | printMarker(); 106 | } 107 | setFlag(false); 108 | }, [flag]); 109 | 110 | useEffect(() => { 111 | dispatch(initMap(afterMapRef, initializeMap)); 112 | }, []); 113 | 114 | return { 115 | containerRef, 116 | afterMapRef, 117 | beforeMapRef, 118 | }; 119 | } 120 | 121 | export default useMap; 122 | -------------------------------------------------------------------------------- /src/hooks/map/useMapTheme.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { RootState } from '../../store'; 3 | import { 4 | DepthThemeState, 5 | setThemeProperties, 6 | } from '../../store/depth-theme/action'; 7 | 8 | export interface MapThemeHookType { 9 | themeIdx?: number; 10 | checkHandler: (index: number) => void; 11 | } 12 | 13 | function useMapTheme(): MapThemeHookType { 14 | const { themeIdx } = useSelector( 15 | (state) => state.depthTheme 16 | ) as DepthThemeState; 17 | 18 | const dispatch = useDispatch(); 19 | 20 | const checkHandler = (index: number) => { 21 | dispatch(setThemeProperties({ themeIdx: index })); 22 | }; 23 | 24 | return { themeIdx, checkHandler }; 25 | } 26 | 27 | export default useMapTheme; 28 | -------------------------------------------------------------------------------- /src/hooks/map/useMarkerPopUp.ts: -------------------------------------------------------------------------------- 1 | import useMarkerRegister from './useMarkerRegister'; 2 | import { MarkerLngLatType } from './useMarkerPosition'; 3 | 4 | interface MarkerPopUpHookType { 5 | onClickButton: () => void; 6 | } 7 | 8 | interface MarkerPopUpPropsType { 9 | inputText: string; 10 | markerLngLat: MarkerLngLatType; 11 | resetMarkerPos: () => void; 12 | } 13 | 14 | function useMarkerPopUp({ 15 | inputText, 16 | markerLngLat, 17 | resetMarkerPos, 18 | }: MarkerPopUpPropsType): MarkerPopUpHookType { 19 | const { registerMarker } = useMarkerRegister(); 20 | const onClickButton = () => { 21 | if (inputText.length > 10) return; 22 | registerMarker({ text: inputText, lngLat: markerLngLat }); 23 | resetMarkerPos(); 24 | }; 25 | 26 | return { 27 | onClickButton, 28 | }; 29 | } 30 | 31 | export default useMarkerPopUp; 32 | -------------------------------------------------------------------------------- /src/hooks/map/useMarkerPosition.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '../../store'; 4 | import { URLPathNameType } from '../../store/common/type'; 5 | 6 | interface MarkerPosType { 7 | x: number | null; 8 | y: number | null; 9 | } 10 | 11 | export interface MarkerLngLatType { 12 | lng: number | null; 13 | lat: number | null; 14 | } 15 | export interface MarkerHookType { 16 | markerPosition: MarkerPosType; 17 | resetMarkerPos: () => void; 18 | markerLngLat: MarkerLngLatType; 19 | } 20 | 21 | const initMarkerStateXY = { 22 | x: null, 23 | y: null, 24 | }; 25 | const initMarkerStateLngLat = { 26 | lng: null, 27 | lat: null, 28 | }; 29 | const { pathname } = window.location; 30 | 31 | function useMarkerFeature(): MarkerHookType { 32 | const map = useSelector((state) => state.map.map) as mapboxgl.Map; 33 | 34 | const [markerPosition, setMarkerPos] = useState({ 35 | ...initMarkerStateXY, 36 | }); 37 | const [markerLngLat, setMarkerLngLat] = useState({ 38 | ...initMarkerStateLngLat, 39 | }); 40 | 41 | const resetMarkerPos = () => { 42 | setMarkerPos({ ...initMarkerStateXY }); 43 | setMarkerLngLat({ ...initMarkerStateLngLat }); 44 | }; 45 | 46 | useEffect(() => { 47 | if (!map) return; 48 | if (pathname !== URLPathNameType.show) { 49 | map.on('contextmenu', (e) => { 50 | e.preventDefault(); 51 | setMarkerPos({ ...e.point }); 52 | setMarkerLngLat({ ...e.lngLat }); 53 | }); 54 | } 55 | }, [map]); 56 | 57 | return { 58 | markerPosition, 59 | resetMarkerPos, 60 | markerLngLat, 61 | }; 62 | } 63 | 64 | export default useMarkerFeature; 65 | -------------------------------------------------------------------------------- /src/hooks/map/useMarkerRegister.ts: -------------------------------------------------------------------------------- 1 | // Redux 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { RootState } from '../../store'; 4 | import { 5 | addMarker, 6 | updateMarker, 7 | removeMarker, 8 | MarkerInstanceType, 9 | } from '../../store/marker/action'; 10 | 11 | // Util 12 | import { 13 | deleteMarkerOfLocalStorage, 14 | updateMarkerOfLocalStorage, 15 | setNewMarkerToLocalStorage, 16 | } from '../../utils/updateMarkerStorage'; 17 | import getRandomId from '../../utils/getRandomId'; 18 | 19 | // Hook 20 | import { MarkerLngLatType } from './useMarkerPosition'; 21 | 22 | // Type 23 | import mapboxgl from 'mapbox-gl'; 24 | import { ReduxStateType, URLPathNameType } from '../../store/common/type'; 25 | 26 | export interface RegisterMarkerType { 27 | id?: string; 28 | text: string; 29 | lngLat?: MarkerLngLatType; 30 | instance?: mapboxgl.Marker; 31 | } 32 | 33 | export interface MarkerRegisterHookType { 34 | marker: MarkerInstanceType[]; 35 | addInstanceToMap: ({ id, text, instance }: AddInstanceToMapProps) => void; 36 | registerMarker: ({ text, lngLat }: RegisterMarkerType) => void; 37 | } 38 | 39 | const LIMIT_MARKER_NUMBER = 30; 40 | const { pathname } = window.location; 41 | 42 | interface AddInstanceToMapProps { 43 | id: string; 44 | text: string; 45 | instance: mapboxgl.Marker; 46 | } 47 | 48 | function useMarkerRegister(): MarkerRegisterHookType { 49 | const dispatch = useDispatch(); 50 | const { map, marker } = useSelector((state) => ({ 51 | map: state.map.map, 52 | marker: state.marker.markers, 53 | })) as ReduxStateType; 54 | 55 | const addInstanceToMap = ({ id, text, instance }: AddInstanceToMapProps) => { 56 | instance.getElement().addEventListener('contextmenu', (e) => { 57 | e.stopPropagation(); 58 | e.preventDefault(); 59 | instance.remove(); 60 | dispatch(removeMarker(id)); 61 | deleteMarkerOfLocalStorage(id); 62 | }); 63 | instance.addTo(map); 64 | instance.on('dragend', () => { 65 | if (pathname === URLPathNameType.show) return; 66 | const { lng, lat } = instance.getLngLat(); 67 | const changedData = { id, text, lng, lat }; 68 | dispatch(updateMarker(changedData)); 69 | updateMarkerOfLocalStorage(changedData); 70 | }); 71 | }; 72 | 73 | const registerMarker = ({ 74 | id = getRandomId(8), 75 | text, 76 | lngLat, 77 | instance, 78 | }: RegisterMarkerType): void => { 79 | if (!map || !marker) return; 80 | if (!lngLat?.lng || !lngLat?.lat) return; 81 | const { lng, lat } = lngLat; 82 | 83 | /** 새로고침 할 때 */ 84 | if (instance) { 85 | addInstanceToMap({ instance, id, text }); 86 | return; 87 | } 88 | 89 | if (marker.length >= LIMIT_MARKER_NUMBER) { 90 | // eslint-disable-next-line no-alert 91 | alert(`최대 ${LIMIT_MARKER_NUMBER}개의 marker만 등록할 수 있습니다.`); 92 | return; 93 | } 94 | 95 | const newMarker = new mapboxgl.Marker({ draggable: true }) 96 | .setLngLat([lng, lat]) 97 | .setPopup(new mapboxgl.Popup().setHTML(`

${text}

`)) 98 | .addTo(map); 99 | 100 | addInstanceToMap({ id, text, instance: newMarker }); 101 | 102 | // 새로운 마커 추가 103 | const newMarkerInstance: MarkerInstanceType = { 104 | id, 105 | text, 106 | lng, 107 | lat, 108 | instance: newMarker, 109 | }; 110 | 111 | dispatch(addMarker(newMarkerInstance)); 112 | if (pathname !== URLPathNameType.show) 113 | setNewMarkerToLocalStorage(newMarkerInstance); 114 | }; 115 | 116 | return { 117 | marker, 118 | addInstanceToMap, 119 | registerMarker, 120 | }; 121 | } 122 | 123 | export default useMarkerRegister; 124 | -------------------------------------------------------------------------------- /src/hooks/map/useUpperButtons.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, RefObject } from 'react'; 2 | 3 | export interface useUpperButtonsType { 4 | fullScreenButtonClickHandler?: () => void; 5 | smallScreenButtonClickHandler?: () => void; 6 | isFullscreen: boolean; 7 | } 8 | 9 | interface useUpperButtonsProps { 10 | mapRef: RefObject; 11 | } 12 | 13 | function useUpperButtons({ 14 | mapRef, 15 | }: useUpperButtonsProps): useUpperButtonsType { 16 | const [isFullscreen, setIsFullscreen] = useState(false); 17 | 18 | useEffect(() => { 19 | window.document.onfullscreenchange = () => { 20 | if (!window.document.fullscreenElement) { 21 | setIsFullscreen(false); 22 | } 23 | }; 24 | 25 | return () => { 26 | window.document.onfullscreenchange = null; 27 | }; 28 | }, []); 29 | 30 | const fullScreenButtonClickHandler = () => { 31 | if (mapRef.current) { 32 | mapRef.current.requestFullscreen(); 33 | } 34 | setIsFullscreen(!isFullscreen); 35 | }; 36 | 37 | const smallScreenButtonClickHandler = () => { 38 | if (window.document.fullscreenElement) { 39 | window.document.exitFullscreen(); 40 | } 41 | setIsFullscreen(!isFullscreen); 42 | }; 43 | 44 | return { 45 | isFullscreen, 46 | fullScreenButtonClickHandler, 47 | smallScreenButtonClickHandler, 48 | }; 49 | } 50 | 51 | export default useUpperButtons; 52 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useCopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | interface CopyToClipboardType { 4 | url: string; 5 | json: string; 6 | completeUrlCopy: boolean; 7 | completeJsonCopy: boolean; 8 | updateUrl: (newUrl: string) => void; 9 | updateJson: (newJson: string) => void; 10 | setCompleteUrlCopy: (newCompleteCopy: boolean) => void; 11 | setCompleteJsonCopy: (newCompleteCopy: boolean) => void; 12 | copyToClipboard: ({ newJson, newUrl }: CopyButtonProps) => void; 13 | } 14 | 15 | interface CopyButtonProps { 16 | newJson?: string; 17 | newUrl?: string; 18 | } 19 | 20 | function useCopyToClipboard(): CopyToClipboardType { 21 | const [url, setUrl] = useState(''); 22 | const [json, setJson] = useState(''); 23 | const [completeUrlCopy, setCompleteUrlCopy] = useState(false); 24 | const [completeJsonCopy, setCompleteJsonCopy] = useState(false); 25 | const updateUrl = (newUrl: string) => { 26 | setUrl(newUrl); 27 | }; 28 | const updateJson = (newJson: string) => { 29 | setJson(newJson); 30 | }; 31 | 32 | const copyToClipboard = ({ newUrl, newJson }: CopyButtonProps) => { 33 | if (newUrl) updateUrl(newUrl); 34 | if (newJson) updateJson(newJson); 35 | 36 | const textareaEl = document.createElement('textarea'); 37 | textareaEl.value = (newUrl as string) || (newJson as string); 38 | textareaEl.setAttribute('readonly', ''); 39 | textareaEl.style.position = 'absolute'; 40 | textareaEl.style.left = '-9999px'; 41 | document.body.appendChild(textareaEl); 42 | textareaEl.select(); 43 | 44 | const returnValue = document.execCommand('copy'); 45 | document.body.removeChild(textareaEl); 46 | 47 | if (!returnValue) return; 48 | if (returnValue && newJson) setCompleteJsonCopy(true); 49 | if (returnValue && newUrl) setCompleteUrlCopy(true); 50 | }; 51 | 52 | return { 53 | url, 54 | json, 55 | completeUrlCopy, 56 | completeJsonCopy, 57 | updateUrl, 58 | updateJson, 59 | setCompleteUrlCopy, 60 | setCompleteJsonCopy, 61 | copyToClipboard, 62 | }; 63 | } 64 | 65 | export default useCopyToClipboard; 66 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useDetailType.ts: -------------------------------------------------------------------------------- 1 | // Reudx 2 | import { useSelector } from 'react-redux'; 3 | import { RootState } from '../../store'; 4 | 5 | // Type 6 | import { 7 | FeatureNameType, 8 | ElementNameType, 9 | SubElementNameType, 10 | FeatureType, 11 | SidebarState, 12 | } from '../../store/common/type'; 13 | 14 | interface UseDetailTypeProps { 15 | sidebarTypeClickHandler: (name: FeatureNameType | ElementNameType) => void; 16 | sidebarSubTypeClickHandler: (name: string) => void; 17 | } 18 | 19 | export interface UseDetailHookType { 20 | detail: FeatureType; 21 | styleClickHandler: ( 22 | elementName: ElementNameType, 23 | subElementName?: SubElementNameType 24 | ) => void; 25 | checkIsSelected: ( 26 | elementName: ElementNameType, 27 | subElementName?: SubElementNameType 28 | ) => boolean; 29 | } 30 | 31 | const dummyDetail = { 32 | section: null, 33 | label: null, 34 | }; 35 | 36 | function useDetailType({ 37 | sidebarTypeClickHandler, 38 | sidebarSubTypeClickHandler, 39 | }: UseDetailTypeProps): UseDetailHookType { 40 | const { feature, subFeature, element, subElement } = useSelector( 41 | (state) => state.sidebar 42 | ) as SidebarState; 43 | 44 | const detail = useSelector((state) => { 45 | if (!feature || !subFeature) { 46 | return dummyDetail; 47 | } 48 | 49 | return state[feature][subFeature]; 50 | }) as FeatureType; 51 | 52 | const styleClickHandler = ( 53 | elementName: ElementNameType, 54 | subElementName?: SubElementNameType 55 | ) => { 56 | sidebarTypeClickHandler(elementName); 57 | if (subElementName) sidebarSubTypeClickHandler(subElementName); 58 | }; 59 | 60 | const checkIsSelected = ( 61 | elementName: ElementNameType, 62 | subElementName?: SubElementNameType 63 | ) => { 64 | if (!subElementName) return elementName === element; 65 | return elementName === element && subElementName === subElement; 66 | }; 67 | 68 | return { 69 | detail, 70 | styleClickHandler, 71 | checkIsSelected, 72 | }; 73 | } 74 | 75 | export default useDetailType; 76 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useFeatureTypeItem.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { RootState } from '../../store'; 3 | import { 4 | FeatureState, 5 | FeatureNameType, 6 | SidebarState, 7 | } from '../../store/common/type'; 8 | 9 | interface useFeatureTypeItemType { 10 | featureList: FeatureState; 11 | feature: FeatureNameType | null; 12 | subFeature: string | null; 13 | } 14 | 15 | export interface useFeatureTypeItemProps { 16 | featureName: FeatureNameType; 17 | } 18 | 19 | function useFeatureTypeItem({ 20 | featureName, 21 | }: useFeatureTypeItemProps): useFeatureTypeItemType { 22 | const { feature, subFeature } = useSelector( 23 | (state) => state.sidebar 24 | ) as SidebarState; 25 | const featureList = useSelector( 26 | (state) => state[featureName] 27 | ) as FeatureState; 28 | 29 | return { 30 | featureList, 31 | feature, 32 | subFeature, 33 | }; 34 | } 35 | 36 | export default useFeatureTypeItem; 37 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useImportModalStatus.ts: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { useState, useEffect } from 'react'; 3 | 4 | // Redux 5 | import { useDispatch } from 'react-redux'; 6 | import { initMarker, MarkerInstanceType } from '../../store/marker/action'; 7 | 8 | // Hook 9 | import useMarkerRegister from '../map/useMarkerRegister'; 10 | import useWholeStyle from '../common/useWholeStyle'; 11 | 12 | // Utils 13 | import { 14 | initMarkerInstances, 15 | setMarkersToLocalStorage, 16 | } from '../../utils/updateMarkerStorage'; 17 | import validateStyle from '../../utils/validateStyle'; 18 | 19 | // Type 20 | import { ReplaceType } from '../../store/common/type'; 21 | 22 | export interface useModalStatusProps { 23 | importModalToggleHandler: () => void; 24 | } 25 | 26 | export interface useModalStatusType { 27 | inputStatus: boolean; 28 | onClickClose: () => void; 29 | onClickOK: (inputText: string) => void; 30 | } 31 | 32 | const Delay = 2000; 33 | 34 | function useModalStatus({ 35 | importModalToggleHandler, 36 | }: useModalStatusProps): useModalStatusType { 37 | const [inputStatus, setInputStatus] = useState(true); 38 | const { flag: isImporting, changeStyle } = useWholeStyle(); 39 | const { marker, addInstanceToMap } = useMarkerRegister(); 40 | const dispatch = useDispatch(); 41 | 42 | useEffect(() => { 43 | if (inputStatus) return; 44 | const timer = setTimeout(() => { 45 | setInputStatus(true); 46 | }, Delay); 47 | 48 | // eslint-disable-next-line consistent-return 49 | return () => { 50 | clearTimeout(timer); 51 | }; 52 | }, [inputStatus]); 53 | 54 | useEffect(() => { 55 | if (!isImporting) return; 56 | importModalToggleHandler(); 57 | }, [isImporting]); 58 | 59 | const onClickClose = () => { 60 | if (!isImporting) importModalToggleHandler(); 61 | }; 62 | 63 | const onClickOK = (inputText: string) => { 64 | try { 65 | if (!inputStatus) return; 66 | const { markers: importedMarker, ...input } = JSON.parse(inputText); 67 | if ( 68 | importedMarker && 69 | (!Array.isArray(importedMarker) || importedMarker.length > 30) 70 | ) 71 | throw new Error('InvalidStyle'); 72 | if (!validateStyle(input)) throw new Error('InvalidStyle'); 73 | 74 | changeStyle(input, { changedKey: ReplaceType.import }); 75 | marker.forEach(({ instance }) => { 76 | if (instance) instance.remove(); 77 | }); 78 | 79 | const newMarkers = initMarkerInstances(importedMarker); 80 | dispatch(initMarker(newMarkers)); 81 | 82 | const updateStorage: MarkerInstanceType[] = []; 83 | newMarkers.forEach(({ id, text, lng, lat, instance }) => { 84 | if (instance) addInstanceToMap({ id, text, instance }); 85 | updateStorage.push({ id, text, lng, lat }); 86 | }); 87 | setMarkersToLocalStorage(updateStorage); 88 | } catch { 89 | setInputStatus(false); 90 | } 91 | }; 92 | 93 | return { 94 | inputStatus, 95 | onClickClose, 96 | onClickOK, 97 | }; 98 | } 99 | 100 | export default useModalStatus; 101 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useInitAllColor.ts: -------------------------------------------------------------------------------- 1 | // Redux 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { RootState } from '../../store'; 4 | import { initColors } from '../../store/style/action'; 5 | import { getDefaultStyle } from '../../store/style/properties'; 6 | 7 | // Util 8 | import * as mapStyling from '../../utils/map-styling'; 9 | 10 | // Type 11 | import { 12 | FeatureNameType, 13 | ElementNameType, 14 | SubElementNameType, 15 | StyleType, 16 | StyleKeyType, 17 | StyleStoreType, 18 | } from '../../store/common/type'; 19 | 20 | interface InitAllColorProps { 21 | features: StyleStoreType; 22 | feature: FeatureNameType; 23 | element: ElementNameType; 24 | subElement: SubElementNameType; 25 | style: StyleType; 26 | key: StyleKeyType; 27 | } 28 | 29 | interface useInitAllColorHookType { 30 | initAllColor: (props: InitAllColorProps) => void; 31 | } 32 | 33 | function useInitAllColor(): useInitAllColorHookType { 34 | const dispatch = useDispatch(); 35 | const map = useSelector((state) => state.map.map) as mapboxgl.Map; 36 | 37 | const initAllColor = ({ 38 | features, 39 | feature, 40 | element, 41 | subElement, 42 | style, 43 | key, 44 | }: InitAllColorProps) => { 45 | dispatch(initColors(feature, element, subElement)); 46 | Object.keys(features[feature]).forEach((subFeatureName) => { 47 | const defaulStyle = getDefaultStyle({ 48 | feature, 49 | subFeature: subFeatureName, 50 | element, 51 | subElement, 52 | }); 53 | 54 | mapStyling[feature]({ 55 | map, 56 | subFeature: subFeatureName, 57 | key, 58 | element, 59 | subElement, 60 | style: { 61 | ...style, 62 | color: defaulStyle.color, 63 | }, 64 | }); 65 | }); 66 | }; 67 | 68 | return { 69 | initAllColor, 70 | }; 71 | } 72 | 73 | export default useInitAllColor; 74 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useSidebarDropdown.ts: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { useState } from 'react'; 3 | 4 | // Redux 5 | import { useDispatch } from 'react-redux'; 6 | import { ReplaceType } from '../../store/common/type'; 7 | import { initDepthTheme } from '../../store/depth-theme/action'; 8 | 9 | // Hook 10 | import useWholeStyle from '../common/useWholeStyle'; 11 | 12 | interface SidebarDropdownProps { 13 | isOpened: boolean; 14 | dropdownToggleHandler: () => void; 15 | } 16 | 17 | export interface useSidebarDropdownType { 18 | resetClickHandler: (e: React.MouseEvent) => void; 19 | importModalToggleHandler: () => void; 20 | isModalOpened: boolean; 21 | } 22 | 23 | function useSidebarDropdown({ 24 | isOpened, 25 | dropdownToggleHandler, 26 | }: SidebarDropdownProps): useSidebarDropdownType { 27 | const dispatch = useDispatch(); 28 | const [isModalOpened, setIsModalOpened] = useState(false); 29 | const { changeStyle } = useWholeStyle(); 30 | 31 | const importModalToggleHandler = () => { 32 | if (isOpened) dropdownToggleHandler(); 33 | setIsModalOpened(!isModalOpened); 34 | }; 35 | 36 | const resetClickHandler = () => { 37 | if (isOpened) dropdownToggleHandler(); 38 | changeStyle({}, { changedKey: ReplaceType.init }); 39 | dispatch(initDepthTheme()); 40 | }; 41 | 42 | return { 43 | importModalToggleHandler, 44 | resetClickHandler, 45 | isModalOpened, 46 | }; 47 | } 48 | 49 | export default useSidebarDropdown; 50 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useSidebarFooter.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useExportStyle, { ExportType } from './useExportStyle'; 3 | 4 | interface SidebarFooterHook { 5 | isOpen: boolean; 6 | onClickExport: () => void; 7 | onCloseModal: () => void; 8 | style: ExportType; 9 | } 10 | 11 | function useSidebarFooter(): SidebarFooterHook { 12 | const [isOpen, setIsOpen] = useState(false); 13 | const [style, setStyle] = useState({}); 14 | 15 | const { exportStyle } = useExportStyle(); 16 | 17 | const onClickExport = () => { 18 | const data = exportStyle(); 19 | setStyle(data); 20 | setIsOpen(true); 21 | }; 22 | 23 | const onCloseModal = () => { 24 | setIsOpen(false); 25 | }; 26 | 27 | return { isOpen, onClickExport, onCloseModal, style }; 28 | } 29 | 30 | export default useSidebarFooter; 31 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useSidebarHeader.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export interface useSidebarHeaderType { 4 | isOpened: boolean; 5 | dropdownToggleHandler: () => void; 6 | } 7 | 8 | function useSidebarHeader(): useSidebarHeaderType { 9 | const [isOpened, setIsOpened] = useState(false); 10 | const dropdownToggleHandler = () => { 11 | setIsOpened(!isOpened); 12 | }; 13 | 14 | return { 15 | isOpened, 16 | dropdownToggleHandler, 17 | }; 18 | } 19 | 20 | export default useSidebarHeader; 21 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useSidebarType.ts: -------------------------------------------------------------------------------- 1 | // Redux 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { 4 | setSidebarProperties, 5 | initSidebarProperties, 6 | } from '../../store/sidebar/action'; 7 | import { RootState } from '../../store/index'; 8 | 9 | // Type 10 | import { 11 | FeatureNameType, 12 | ElementNameType, 13 | SubElementNameType, 14 | SidebarState, 15 | SidebarProperties, 16 | } from '../../store/common/type'; 17 | 18 | export interface SidebarHookType { 19 | sidebarTypeClickHandler: (name: FeatureNameType | ElementNameType) => void; 20 | sidebarSubTypeClickHandler: (name: string) => void; 21 | feature: FeatureNameType | null; 22 | subFeature: string | null; 23 | element: ElementNameType | null; 24 | subElement: SubElementNameType | null; 25 | } 26 | 27 | function useSidebarType(): SidebarHookType { 28 | const dispatch = useDispatch(); 29 | const sidebarStates = useSelector( 30 | (state) => state.sidebar 31 | ) as SidebarState; 32 | const { feature, subFeature, element, subElement } = sidebarStates; 33 | 34 | const sidebarTypeClickHandler = (name: FeatureNameType | ElementNameType) => { 35 | if ([feature, element].includes(name)) return; 36 | if (Object.keys(ElementNameType).includes(name)) { 37 | dispatch( 38 | setSidebarProperties({ 39 | ...sidebarStates, 40 | element: name as ElementNameType, 41 | key: SidebarProperties.element, 42 | }) 43 | ); 44 | } else { 45 | dispatch( 46 | initSidebarProperties({ 47 | ...sidebarStates, 48 | feature: name as FeatureNameType, 49 | key: SidebarProperties.feature, 50 | }) 51 | ); 52 | } 53 | }; 54 | 55 | const sidebarSubTypeClickHandler = (name: string) => { 56 | if (Object.keys(SubElementNameType).includes(name)) { 57 | dispatch( 58 | setSidebarProperties({ 59 | ...sidebarStates, 60 | subElement: name as SubElementNameType, 61 | key: SidebarProperties.subElement, 62 | }) 63 | ); 64 | } else { 65 | dispatch( 66 | setSidebarProperties({ 67 | ...sidebarStates, 68 | subFeature: name, 69 | key: SidebarProperties.subFeature, 70 | }) 71 | ); 72 | } 73 | }; 74 | 75 | return { 76 | sidebarTypeClickHandler, 77 | sidebarSubTypeClickHandler, 78 | feature, 79 | subFeature, 80 | element, 81 | subElement, 82 | }; 83 | } 84 | 85 | export default useSidebarType; 86 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { objType, ReplaceType } from '../../store/common/type'; 2 | import useWholeStyle from '../../hooks/common/useWholeStyle'; 3 | 4 | interface UseThemeType { 5 | applyTheme: (theme: objType) => void; 6 | } 7 | 8 | interface UseThemeProps { 9 | clickHandler: () => void; 10 | } 11 | 12 | const standardTheme = '표준'; 13 | 14 | function useTheme({ clickHandler }: UseThemeProps): UseThemeType { 15 | const { changeStyle } = useWholeStyle(); 16 | 17 | const applyTheme = (data: objType) => { 18 | clickHandler(); 19 | if (!data.theme) 20 | changeStyle( 21 | {}, 22 | { changedKey: ReplaceType.theme, changedValue: standardTheme } 23 | ); 24 | else 25 | changeStyle(data.theme, { 26 | changedKey: ReplaceType.theme, 27 | changedValue: data.name, 28 | }); 29 | }; 30 | 31 | return { 32 | applyTheme, 33 | }; 34 | } 35 | 36 | export default useTheme; 37 | -------------------------------------------------------------------------------- /src/hooks/sidebar/useToggleStatus.ts: -------------------------------------------------------------------------------- 1 | import { useState, Dispatch, SetStateAction } from 'react'; 2 | 3 | export interface ToggleStatusHook { 4 | isActive: boolean; 5 | setIsActive: Dispatch>; 6 | } 7 | 8 | function useToggleStatus(): ToggleStatusHook { 9 | const [isActive, setIsActive] = useState(false); 10 | 11 | return { 12 | isActive, 13 | setIsActive, 14 | }; 15 | } 16 | 17 | export default useToggleStatus; 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { ThemeProvider } from 'emotion-theming'; 5 | import App from './App'; 6 | 7 | import GlobalStyle from './utils/styles/globalStyle'; 8 | import { theme } from './utils/styles/styled'; 9 | 10 | import { Provider } from 'react-redux'; 11 | import store from './store'; 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | -------------------------------------------------------------------------------- /src/pages/Entry.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Canvas from '../components/Canvas'; 4 | import styled from '../utils/styles/styled'; 5 | 6 | const BackGround = styled.div` 7 | width: 100vw; 8 | height: 100vh; 9 | 10 | background-color: ${(props) => props.theme.DEEPGREY}; 11 | `; 12 | 13 | const Container = styled.div` 14 | position: absolute; 15 | top: 50%; 16 | left: 50%; 17 | transform: translate(-50%, -50%); 18 | height: 60%; 19 | 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: space-between; 24 | `; 25 | 26 | const StartButton = styled.button` 27 | width: 250px; 28 | height: 60px; 29 | 30 | border: none; 31 | border-radius: 40px; 32 | font-size: 2.8rem; 33 | font-weight: bold; 34 | 35 | color: ${(props) => props.theme.GREEN}; 36 | background-color: ${(props) => props.theme.WHITE}; 37 | &:hover { 38 | background-color: ${(props) => props.theme.GREEN}; 39 | color: ${(props) => props.theme.WHITE}; 40 | } 41 | `; 42 | 43 | function Entry(): React.ReactElement { 44 | return ( 45 | 46 | 47 | 48 | 49 | 시작하기 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | export default Entry; 57 | -------------------------------------------------------------------------------- /src/pages/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '../utils/styles/styled'; 3 | import Sidebar from '../components/Sidebar/Sidebar'; 4 | import Map from '../components/Map/Map'; 5 | import { URLPathNameType } from '../store/common/type'; 6 | 7 | interface MainProps { 8 | location: { 9 | pathname?: string; 10 | search?: string; 11 | hash?: string; 12 | state?: string; 13 | }; 14 | } 15 | 16 | const MainWrapper = styled.div` 17 | display: flex; 18 | flex-direction: row; 19 | justify-content: flex-start; 20 | align-items: center; 21 | width: 100%; 22 | height: 100%; 23 | min-width: 500px; 24 | `; 25 | 26 | function Main({ location: { pathname } }: MainProps): React.ReactElement { 27 | return ( 28 | 29 | {pathname === URLPathNameType.map && } 30 | 31 | 32 | ); 33 | } 34 | 35 | export default Main; 36 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/store/depth-theme/action.ts: -------------------------------------------------------------------------------- 1 | import { DepthItemKeyTypes } from '../../hooks/sidebar/useSidebarDepthItem'; 2 | 3 | export const SET_SHOW_DEPTH_PROPERTIES = 'SET_SHOW_DEPTH_PROPERTIES' as const; 4 | export const SET_THEME_PROPERTIES = 'SET_THEME_PROPERTIES' as const; 5 | export const INIT_DEPTH_THEME = 'INIT_DEPTH_THEME'; 6 | 7 | export interface DepthPropsType { 8 | selectedFeature: DepthItemKeyTypes; 9 | selectedDepth: number; 10 | } 11 | 12 | export interface ThemePropsType { 13 | themeIdx: number; 14 | } 15 | 16 | export interface DepthThemeState extends ThemePropsType { 17 | roadDepth: number; 18 | administrativeDepth: number; 19 | } 20 | 21 | export interface DepthThemeActionType { 22 | type: 23 | | typeof SET_SHOW_DEPTH_PROPERTIES 24 | | typeof SET_THEME_PROPERTIES 25 | | typeof INIT_DEPTH_THEME; 26 | payload: DepthPropsType | ThemePropsType; 27 | } 28 | 29 | export const setShowDepthProperties = ({ 30 | selectedFeature, 31 | selectedDepth, 32 | }: DepthPropsType): DepthThemeActionType => ({ 33 | type: SET_SHOW_DEPTH_PROPERTIES, 34 | payload: { selectedFeature, selectedDepth }, 35 | }); 36 | 37 | export const setThemeProperties = ({ 38 | themeIdx, 39 | }: ThemePropsType): DepthThemeActionType => ({ 40 | type: SET_THEME_PROPERTIES, 41 | payload: { themeIdx }, 42 | }); 43 | 44 | export const initDepthTheme = (): { type: typeof INIT_DEPTH_THEME } => ({ 45 | type: INIT_DEPTH_THEME, 46 | }); 47 | -------------------------------------------------------------------------------- /src/store/depth-theme/reducer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | import { 3 | DepthThemeActionType, 4 | DepthThemeState, 5 | DepthPropsType, 6 | ThemePropsType, 7 | SET_SHOW_DEPTH_PROPERTIES, 8 | SET_THEME_PROPERTIES, 9 | INIT_DEPTH_THEME, 10 | } from './action'; 11 | 12 | const initialState: DepthThemeState = { 13 | roadDepth: 3, 14 | administrativeDepth: 3, 15 | themeIdx: 0, 16 | }; 17 | 18 | function depthThemeReducer( 19 | state: DepthThemeState = initialState, 20 | action: DepthThemeActionType 21 | ): DepthThemeState { 22 | const { type, payload } = action; 23 | 24 | switch (type) { 25 | case SET_SHOW_DEPTH_PROPERTIES: 26 | const { selectedFeature, selectedDepth } = payload as DepthPropsType; 27 | 28 | return { 29 | ...state, 30 | [selectedFeature]: selectedDepth, 31 | }; 32 | 33 | case SET_THEME_PROPERTIES: 34 | const { themeIdx } = payload as ThemePropsType; 35 | return { 36 | ...state, 37 | themeIdx, 38 | }; 39 | 40 | case INIT_DEPTH_THEME: 41 | return initialState; 42 | 43 | default: 44 | return state; 45 | } 46 | } 47 | 48 | export default depthThemeReducer; 49 | -------------------------------------------------------------------------------- /src/store/history/action.ts: -------------------------------------------------------------------------------- 1 | import { HistoryActionType, HistoryInfoPropsType } from '../common/type'; 2 | 3 | export const INIT_HISTORY = 'INIT_HISTORY' as const; 4 | export const ADD_LOG = 'ADD_LOG' as const; 5 | export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX' as const; 6 | export const RESET_HISTORY = 'RESET_HISTORY' as const; 7 | 8 | export const initHistory = (): HistoryActionType => ({ 9 | type: INIT_HISTORY, 10 | payload: null, 11 | }); 12 | 13 | export const addLog = (info: HistoryInfoPropsType): HistoryActionType => ({ 14 | type: ADD_LOG, 15 | payload: info, 16 | }); 17 | 18 | export const setCurrentIndex = (currentIndex: number): HistoryActionType => ({ 19 | type: SET_CURRENT_INDEX, 20 | payload: { currentIndex }, 21 | }); 22 | 23 | export const resetHistory = (): HistoryActionType => ({ 24 | type: RESET_HISTORY, 25 | payload: null, 26 | }); 27 | -------------------------------------------------------------------------------- /src/store/history/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_LOG, 3 | INIT_HISTORY, 4 | SET_CURRENT_INDEX, 5 | RESET_HISTORY, 6 | } from './action'; 7 | import { 8 | HistoryState, 9 | HistoryActionType, 10 | HistoryInfoPropsType, 11 | SetIndexPayload, 12 | } from '../common/type'; 13 | import getRandomId from '../../utils/getRandomId'; 14 | 15 | const logKey = 'log' as const; 16 | 17 | const initialState: HistoryState = { 18 | log: [], 19 | currentIdx: null, 20 | }; 21 | 22 | function historyReducer( 23 | state: HistoryState = initialState, 24 | action: HistoryActionType 25 | ): HistoryState { 26 | switch (action.type) { 27 | case INIT_HISTORY: { 28 | const localStorageLog = JSON.parse( 29 | localStorage.getItem(logKey) as string 30 | ); 31 | 32 | const log = localStorageLog 33 | ? (localStorageLog as HistoryInfoPropsType[]) 34 | : []; 35 | 36 | const currentIdx = log.length ? log.length - 1 : null; 37 | 38 | return { 39 | log, 40 | currentIdx, 41 | }; 42 | } 43 | case ADD_LOG: { 44 | const id = getRandomId(8); 45 | const newState = JSON.parse(JSON.stringify(state)); 46 | if (newState.log.length !== newState.currentIdx + 1) { 47 | newState.log.splice(newState.currentIdx + 1); 48 | } 49 | 50 | newState.log.push({ id, ...action.payload }); 51 | newState.currentIdx = newState.log.length - 1; 52 | 53 | return newState as HistoryState; 54 | } 55 | 56 | case SET_CURRENT_INDEX: { 57 | const newState = JSON.parse(JSON.stringify(state)); 58 | newState.currentIdx = (action.payload as SetIndexPayload).currentIndex; 59 | return newState; 60 | } 61 | 62 | case RESET_HISTORY: { 63 | return { ...state, log: [], currentIdx: null }; 64 | } 65 | default: 66 | return state; 67 | } 68 | } 69 | 70 | export default historyReducer; 71 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from 'redux'; 2 | import map from './map/reducer'; 3 | import { 4 | poiReducer as poi, 5 | transitReducer as transit, 6 | landscapeReducer as landscape, 7 | administrativeReducer as administrative, 8 | roadReducer as road, 9 | waterReducer as water, 10 | } from './style/reducer'; 11 | import sidebar from './sidebar/reducer'; 12 | import history from './history/reducer'; 13 | import depthTheme from './depth-theme/reducer'; 14 | import marker from './marker/reducer'; 15 | 16 | const rootReducer = combineReducers({ 17 | map, 18 | poi, 19 | landscape, 20 | administrative, 21 | road, 22 | transit, 23 | water, 24 | sidebar, 25 | history, 26 | depthTheme, 27 | marker, 28 | }); 29 | 30 | const store = createStore(rootReducer); 31 | 32 | export type RootState = ReturnType; 33 | 34 | export default store; 35 | -------------------------------------------------------------------------------- /src/store/map/action.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | export const INIT_MAP = 'INIT_MAP' as const; 4 | 5 | export interface InitActionType { 6 | type: typeof INIT_MAP; 7 | payload: { 8 | mapRef: RefObject; 9 | initializeMap: (map: mapboxgl.Map) => void; 10 | }; 11 | } 12 | 13 | export const initMap = ( 14 | mapRef: RefObject, 15 | initializeMap: (map: mapboxgl.Map) => void 16 | ): InitActionType => ({ 17 | type: INIT_MAP, 18 | payload: { mapRef, initializeMap }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/store/map/geocoder.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@mapbox/mapbox-gl-geocoder'; 2 | -------------------------------------------------------------------------------- /src/store/map/initializeMap.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import mapboxgl from 'mapbox-gl'; 3 | import 'mapbox-gl/dist/mapbox-gl.css'; 4 | import dotenv from 'dotenv'; 5 | import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; 6 | import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; 7 | 8 | import initLayers from '../../utils/rendering-data/layers/init'; 9 | 10 | const LNG = 126.978; 11 | const LAT = 37.5656; 12 | const ZOOM = 15.5; 13 | 14 | dotenv.config(); 15 | mapboxgl.accessToken = process.env.REACT_APP_ACCESS_TOKEN as string; 16 | 17 | interface InitializeMapProps { 18 | mapRef: RefObject; 19 | initializeMap: (map: mapboxgl.Map) => void; 20 | } 21 | 22 | function initializingMap({ 23 | mapRef, 24 | initializeMap, 25 | }: InitializeMapProps): mapboxgl.Map { 26 | const map = new mapboxgl.Map({ 27 | container: mapRef.current as HTMLDivElement, 28 | style: initLayers as mapboxgl.Style, 29 | center: [LNG, LAT], 30 | zoom: ZOOM, 31 | }); 32 | 33 | const geocoder = new MapboxGeocoder({ 34 | accessToken: mapboxgl.accessToken, 35 | placeholder: '검색할 장소를 입력하세요', 36 | mapboxgl, 37 | }); 38 | 39 | map.addControl( 40 | new mapboxgl.GeolocateControl({ 41 | positionOptions: { 42 | enableHighAccuracy: true, 43 | }, 44 | trackUserLocation: true, 45 | }), 46 | 'bottom-right' 47 | ); 48 | 49 | map.on('load', () => { 50 | initializeMap(map); 51 | document.getElementById('search-bar')?.appendChild(geocoder.onAdd(map)); 52 | }); 53 | 54 | return map; 55 | } 56 | 57 | export default initializingMap; 58 | -------------------------------------------------------------------------------- /src/store/map/reducer.ts: -------------------------------------------------------------------------------- 1 | import { InitActionType, INIT_MAP } from './action'; 2 | import mapboxgl from 'mapbox-gl'; 3 | import initializingMap from './initializeMap'; 4 | 5 | export interface MapState { 6 | map: mapboxgl.Map | null; 7 | } 8 | 9 | const initialState: MapState = { 10 | map: null, 11 | }; 12 | 13 | function mapReducer( 14 | state: MapState = initialState, 15 | action: InitActionType 16 | ): MapState { 17 | switch (action.type) { 18 | case INIT_MAP: { 19 | const map = initializingMap({ 20 | mapRef: action.payload.mapRef, 21 | initializeMap: action.payload.initializeMap, 22 | }); 23 | return { 24 | ...state, 25 | map, 26 | }; 27 | } 28 | default: 29 | return state; 30 | } 31 | } 32 | 33 | export default mapReducer; 34 | -------------------------------------------------------------------------------- /src/store/marker/action.ts: -------------------------------------------------------------------------------- 1 | export const INIT_MARKER = 'INIT_MARKER' as const; 2 | export const ADD_MARKER = 'ADD_MARKER' as const; 3 | export const UPDATE_MARKER = 'UPDATE_MARKER' as const; 4 | export const REMOVE_MARKER = 'REMOVE_MARKER' as const; 5 | 6 | export const MARKER = 'marker' as const; 7 | 8 | export enum MarkerKeyType { 9 | id = 'id', 10 | lng = 'lng', 11 | lat = 'lat', 12 | text = 'text', 13 | instance = 'instance', 14 | } 15 | 16 | export interface MarkerType { 17 | lng: number; 18 | lat: number; 19 | text: string; 20 | } 21 | 22 | export interface MarkerInstanceType extends MarkerType { 23 | id: string; 24 | instance?: mapboxgl.Marker; 25 | } 26 | 27 | export interface MarkerUpdateType { 28 | id: string; 29 | lng?: number; 30 | lat?: number; 31 | } 32 | 33 | export interface MarkerState { 34 | markers: MarkerInstanceType[]; 35 | } 36 | 37 | export interface MarkerActionType { 38 | type: 39 | | typeof INIT_MARKER 40 | | typeof ADD_MARKER 41 | | typeof UPDATE_MARKER 42 | | typeof REMOVE_MARKER; 43 | payload: 44 | | MarkerInstanceType[] 45 | | MarkerInstanceType 46 | | MarkerUpdateType 47 | | string; 48 | } 49 | 50 | export const initMarker = ( 51 | inputPayload: MarkerInstanceType[] 52 | ): MarkerActionType => ({ 53 | type: INIT_MARKER, 54 | payload: [...inputPayload], 55 | }); 56 | 57 | export const addMarker = ( 58 | inputPayload: MarkerInstanceType 59 | ): MarkerActionType => ({ 60 | type: ADD_MARKER, 61 | payload: { ...inputPayload }, 62 | }); 63 | 64 | export const updateMarker = ( 65 | inputPayload: MarkerUpdateType 66 | ): MarkerActionType => ({ 67 | type: UPDATE_MARKER, 68 | payload: { ...inputPayload }, 69 | }); 70 | 71 | export const removeMarker = (id: string): MarkerActionType => ({ 72 | type: REMOVE_MARKER, 73 | payload: id, 74 | }); 75 | -------------------------------------------------------------------------------- /src/store/marker/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MarkerInstanceType, 3 | MarkerActionType, 4 | MarkerState, 5 | INIT_MARKER, 6 | ADD_MARKER, 7 | UPDATE_MARKER, 8 | REMOVE_MARKER, 9 | } from './action'; 10 | import produce from 'immer'; 11 | 12 | const initialState = { markers: [] }; 13 | 14 | function markerReducer( 15 | state: MarkerState = initialState, 16 | action: MarkerActionType 17 | ): MarkerState { 18 | switch (action.type) { 19 | case INIT_MARKER: 20 | return { markers: [...(action.payload as MarkerInstanceType[])] }; 21 | 22 | case ADD_MARKER: { 23 | const { 24 | id, 25 | lng, 26 | lat, 27 | text, 28 | instance, 29 | } = action.payload as MarkerInstanceType; 30 | const nextState = produce(state, (draftState) => { 31 | draftState.markers.push({ id, lng, lat, text, instance }); 32 | }); 33 | return nextState; 34 | } 35 | 36 | case UPDATE_MARKER: { 37 | const { id, lng, lat } = action.payload as MarkerInstanceType; 38 | 39 | const { markers } = state; 40 | const target = markers.find((item) => item.id === id); 41 | 42 | const changedTarget = { 43 | ...target, 44 | lng: lng ?? target?.lng, 45 | lat: lat ?? target?.lat, 46 | } as MarkerInstanceType; 47 | 48 | const newState: MarkerInstanceType[] = [...markers] 49 | .filter((marker) => marker.id !== id) 50 | .concat([changedTarget]); 51 | 52 | return { markers: newState }; 53 | } 54 | 55 | case REMOVE_MARKER: { 56 | const newMarkers = state.markers.filter( 57 | (item) => item.id !== action.payload 58 | ); 59 | return { markers: newMarkers }; 60 | } 61 | 62 | default: 63 | return state; 64 | } 65 | } 66 | 67 | export default markerReducer; 68 | -------------------------------------------------------------------------------- /src/store/sidebar/action.ts: -------------------------------------------------------------------------------- 1 | import { SidebarState } from '../common/type'; 2 | 3 | export const SET_SIDEBAR_PROPERTIES = 'SET_SIDEBAR_PROPERTIES' as const; 4 | export const INIT_SIDEBAR_PROPERTIES = 'INIT_SIDEBAR_PROPERTIES' as const; 5 | 6 | export interface SidebarActionType { 7 | type: typeof SET_SIDEBAR_PROPERTIES | typeof INIT_SIDEBAR_PROPERTIES; 8 | payload: SidebarState; 9 | } 10 | 11 | export const setSidebarProperties = ({ 12 | key, 13 | feature, 14 | subFeature, 15 | element, 16 | subElement, 17 | }: SidebarState): SidebarActionType => ({ 18 | type: SET_SIDEBAR_PROPERTIES, 19 | payload: { key, feature, subFeature, element, subElement }, 20 | }); 21 | 22 | export const initSidebarProperties = ({ 23 | key, 24 | feature, 25 | subFeature, 26 | element, 27 | subElement, 28 | }: SidebarState): SidebarActionType => ({ 29 | type: INIT_SIDEBAR_PROPERTIES, 30 | payload: { key, feature, subFeature, element, subElement }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/store/sidebar/reducer.ts: -------------------------------------------------------------------------------- 1 | import { SidebarState, SidebarActionType } from '../common/type'; 2 | import { SET_SIDEBAR_PROPERTIES, INIT_SIDEBAR_PROPERTIES } from './action'; 3 | 4 | const initialState: SidebarState = { 5 | key: 'feature', 6 | feature: null, 7 | subFeature: null, 8 | element: null, 9 | subElement: null, 10 | }; 11 | 12 | function sidebarReducer( 13 | state: SidebarState = initialState, 14 | action: SidebarActionType 15 | ): SidebarState { 16 | switch (action.type) { 17 | case SET_SIDEBAR_PROPERTIES: { 18 | return { 19 | ...state, 20 | [action.payload.key]: action.payload[action.payload.key], 21 | }; 22 | } 23 | case INIT_SIDEBAR_PROPERTIES: { 24 | return { 25 | ...initialState, 26 | [action.payload.key]: action.payload[action.payload.key], 27 | }; 28 | } 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | export default sidebarReducer; 35 | -------------------------------------------------------------------------------- /src/store/style/action.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FeatureNameType, 3 | ActionPayload, 4 | WholeStyleActionPayload, 5 | StyleStoreType, 6 | ElementNameType, 7 | SubElementNameType, 8 | } from '../common/type'; 9 | 10 | export const INIT = 'INIT' as const; 11 | export const SET = 'SET' as const; 12 | export const SET_WHOLE = 'SET_WHOLE' as const; 13 | export const REPLACE_FEATURE = 'REPLACE_FEATURE' as const; 14 | export const REPLACE_WHOLE = 'REPLACE_WHOLE' as const; 15 | export const INIT_COLORS = 'INIT_COLORS' as const; 16 | 17 | export interface SetType { 18 | type: typeof SET; 19 | payload: ActionPayload; 20 | } 21 | 22 | export interface SetWholeType { 23 | type: typeof SET_WHOLE; 24 | payload: WholeStyleActionPayload; 25 | } 26 | 27 | export interface ReplaceFeatureType { 28 | type: typeof REPLACE_FEATURE; 29 | payload: WholeStyleActionPayload; 30 | } 31 | 32 | export interface ReplaceWholeType { 33 | type: typeof REPLACE_WHOLE; 34 | payload: StyleStoreType; 35 | } 36 | 37 | export interface InitColorsType { 38 | type: typeof INIT_COLORS; 39 | payload: { 40 | feature: FeatureNameType; 41 | element: ElementNameType; 42 | subElement: SubElementNameType; 43 | }; 44 | } 45 | 46 | export const init = (): { type: typeof INIT } => ({ 47 | type: INIT, 48 | }); 49 | 50 | export const setStyle = ({ 51 | feature, 52 | subFeature, 53 | element, 54 | subElement, 55 | style, 56 | }: ActionPayload): SetType => ({ 57 | type: SET, 58 | payload: { feature, subFeature, element, subElement, style }, 59 | }); 60 | 61 | export const setWholeStyle = ( 62 | wholeStyle: WholeStyleActionPayload 63 | ): SetWholeType => ({ 64 | type: SET_WHOLE, 65 | payload: wholeStyle, 66 | }); 67 | 68 | export const replaceFeatureStyle = ( 69 | wholeStyle: WholeStyleActionPayload 70 | ): ReplaceFeatureType => ({ 71 | type: REPLACE_FEATURE, 72 | payload: wholeStyle, 73 | }); 74 | 75 | export const replaceWholeStyle = ( 76 | wholeStyle: StyleStoreType 77 | ): ReplaceWholeType => ({ 78 | type: REPLACE_WHOLE, 79 | payload: wholeStyle, 80 | }); 81 | 82 | export const initColors = ( 83 | feature: FeatureNameType, 84 | element: ElementNameType, 85 | subElement: SubElementNameType 86 | ): InitColorsType => ({ 87 | type: INIT_COLORS, 88 | payload: { feature, subElement, element }, 89 | }); 90 | -------------------------------------------------------------------------------- /src/store/style/compareStyle.ts: -------------------------------------------------------------------------------- 1 | import { StyleType, StyleKeyType, objType } from '../common/type'; 2 | 3 | interface checkStyleIsChangedProps { 4 | defaultStyle: StyleType; 5 | style: StyleType; 6 | } 7 | 8 | export function checkStyleIsChanged({ 9 | defaultStyle, 10 | style, 11 | }: checkStyleIsChangedProps): boolean { 12 | const keys = Object.keys(defaultStyle) as StyleKeyType[]; 13 | 14 | const filteredKeys = keys.filter( 15 | (key) => key === StyleKeyType.isChanged || defaultStyle[key] === style[key] 16 | ); 17 | return keys.length !== filteredKeys.length; 18 | } 19 | 20 | export function checkFeatureIsChanged(targetFeature: objType): boolean { 21 | if (!targetFeature) { 22 | return false; 23 | } 24 | const keys = Object.keys(targetFeature); 25 | for (let i = 0; i < keys.length; i += 1) { 26 | if ( 27 | typeof targetFeature[keys[i]] === 'object' && 28 | checkFeatureIsChanged(targetFeature[keys[i]]) === true 29 | ) { 30 | return true; 31 | } 32 | 33 | if (Object.values(targetFeature).includes(true)) return true; 34 | } 35 | return false; 36 | } 37 | -------------------------------------------------------------------------------- /src/store/style/manageCategories.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SubElementActionPayload, 3 | ElementActionPayload, 4 | StyleActionPayload, 5 | FeatureType, 6 | SubElementType, 7 | StyleType, 8 | SubElementNameType, 9 | ElementNameType, 10 | } from '../common/type'; 11 | import { checkStyleIsChanged } from './compareStyle'; 12 | 13 | interface combineElementProps { 14 | elementStyle: ElementActionPayload; 15 | initialElementStyle: FeatureType; 16 | } 17 | 18 | export const combineElement = ({ 19 | elementStyle, 20 | initialElementStyle, 21 | }: combineElementProps): FeatureType => { 22 | const update = initialElementStyle; 23 | const elements = Object.keys(elementStyle) as ElementNameType[]; 24 | elements.forEach((element) => { 25 | if (update[element]) { 26 | switch (element) { 27 | case ElementNameType.labelIcon: { 28 | update[element] = combineStyle({ 29 | style: elementStyle[element] as StyleActionPayload, 30 | defaultStyle: update[element] as StyleType, 31 | }); 32 | break; 33 | } 34 | case ElementNameType.section: 35 | case ElementNameType.labelText: 36 | update[element] = combineSubElement({ 37 | subElementStyle: elementStyle[element] as SubElementActionPayload, 38 | initialSubElementStyle: update[element] as SubElementType, 39 | }); 40 | break; 41 | default: 42 | break; 43 | } 44 | } 45 | }); 46 | return update; 47 | }; 48 | 49 | interface combineSubElementProps { 50 | subElementStyle: SubElementActionPayload; 51 | initialSubElementStyle: SubElementType; 52 | } 53 | 54 | const combineSubElement = ({ 55 | subElementStyle, 56 | initialSubElementStyle, 57 | }: combineSubElementProps): SubElementType => { 58 | const update = initialSubElementStyle; 59 | const subElements = Object.keys(subElementStyle) as SubElementNameType[]; 60 | subElements.forEach((subElement) => { 61 | update[subElement] = combineStyle({ 62 | style: subElementStyle[subElement] as StyleActionPayload, 63 | defaultStyle: update[subElement], 64 | }); 65 | }); 66 | return update as SubElementType; 67 | }; 68 | 69 | interface combineStyleProps { 70 | style: StyleActionPayload; 71 | defaultStyle: StyleType; 72 | } 73 | 74 | const combineStyle = ({ 75 | style, 76 | defaultStyle, 77 | }: combineStyleProps): StyleType => { 78 | const update: StyleType = { 79 | ...defaultStyle, 80 | ...style, 81 | }; 82 | return { 83 | ...update, 84 | isChanged: checkStyleIsChanged({ defaultStyle, style: update }), 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/store/style/properties.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StyleType, 3 | ElementNameType, 4 | FeatureType, 5 | SubElementNameType, 6 | FeaturePropsType, 7 | ElementPropsType, 8 | DefaultElementType, 9 | DefaultStyleType, 10 | } from '../common/type'; 11 | import { hslToHEX } from '../../utils/colorFormat'; 12 | import defaultStyle from '../../utils/rendering-data/defaultStyle'; 13 | 14 | const style: StyleType = { 15 | isChanged: false, 16 | visibility: 'inherit', 17 | color: '#000000', 18 | weight: 0, 19 | }; 20 | 21 | export const getDefaultStyle = ({ 22 | feature, 23 | subFeature, 24 | element, 25 | subElement, 26 | }: ElementPropsType): StyleType => { 27 | const defaultState = subElement 28 | ? ((defaultStyle[feature][subFeature][element] as DefaultElementType)[ 29 | subElement 30 | ] as DefaultStyleType) 31 | : (defaultStyle[feature][subFeature][element] as DefaultStyleType); 32 | 33 | return { 34 | ...JSON.parse(JSON.stringify(style)), 35 | visibility: subFeature === 'all' ? 'visible' : 'inherit', 36 | color: hslToHEX(defaultState?.color as string), 37 | weight: defaultState?.weight || 0, 38 | }; 39 | }; 40 | 41 | export const getDefaultFeature = ({ 42 | feature, 43 | subFeature, 44 | }: FeaturePropsType): FeatureType => { 45 | return { 46 | isChanged: false, 47 | section: defaultStyle[feature][subFeature][ElementNameType.section] 48 | ? { 49 | [SubElementNameType.fill]: getDefaultStyle({ 50 | feature, 51 | subFeature, 52 | element: ElementNameType.section, 53 | subElement: SubElementNameType.fill, 54 | }), 55 | [SubElementNameType.stroke]: getDefaultStyle({ 56 | feature, 57 | subFeature, 58 | element: ElementNameType.section, 59 | subElement: SubElementNameType.stroke, 60 | }), 61 | } 62 | : null, 63 | labelText: defaultStyle[feature][subFeature][ElementNameType.labelText] 64 | ? { 65 | [SubElementNameType.fill]: getDefaultStyle({ 66 | feature, 67 | subFeature, 68 | element: ElementNameType.labelText, 69 | subElement: SubElementNameType.fill, 70 | }), 71 | [SubElementNameType.stroke]: getDefaultStyle({ 72 | feature, 73 | subFeature, 74 | element: ElementNameType.labelText, 75 | subElement: SubElementNameType.stroke, 76 | }), 77 | } 78 | : null, 79 | labelIcon: defaultStyle[feature][subFeature][ElementNameType.labelIcon] 80 | ? getDefaultStyle({ 81 | feature, 82 | subFeature, 83 | element: ElementNameType.labelIcon, 84 | }) 85 | : null, 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/store/style/reducer.ts: -------------------------------------------------------------------------------- 1 | import getReducer from './getReducer'; 2 | 3 | const INDEX = { 4 | POI: 0, 5 | ROAD: 1, 6 | ADMINISTRATIVE: 2, 7 | LANDSCAPE: 3, 8 | TRANSIT: 4, 9 | WATER: 5, 10 | }; 11 | 12 | export const poiReducer = getReducer(INDEX.POI); 13 | 14 | export const roadReducer = getReducer(INDEX.ROAD); 15 | 16 | export const administrativeReducer = getReducer(INDEX.ADMINISTRATIVE); 17 | 18 | export const landscapeReducer = getReducer(INDEX.LANDSCAPE); 19 | 20 | export const transitReducer = getReducer(INDEX.TRANSIT); 21 | 22 | export const waterReducer = getReducer(INDEX.WATER); 23 | -------------------------------------------------------------------------------- /src/utils/applyStyle.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | /* eslint-disable no-case-declarations */ 3 | import mapboxgl from 'mapbox-gl'; 4 | import { hexToHSL } from './colorFormat'; 5 | import { WeightTemplateProperty } from './map-styling/macgyver/weightTemplate'; 6 | 7 | export enum ColorType { 8 | fill = 'fill-color', 9 | line = 'line-color', 10 | text = 'text-color', 11 | background = 'background-color', 12 | textHalo = 'text-halo-color', 13 | icon = 'icon-opacity', 14 | } 15 | 16 | export enum WeightType { 17 | line = 'line-width', 18 | textHalo = 'text-halo-width', 19 | } 20 | 21 | interface ApplyProps { 22 | map: mapboxgl.Map; 23 | layerNames: string[]; 24 | color?: string; 25 | type?: StyleTypes; 26 | saturation?: number; 27 | lightness?: number; 28 | weight?: number | WeightTemplateProperty[]; 29 | visibility?: string; 30 | } 31 | 32 | export enum VisibilityType { 33 | visible = 'visible', 34 | inherit = 'inherit', 35 | none = 'none', 36 | } 37 | 38 | export type StyleTypes = VisibilityType | ColorType | WeightType; 39 | 40 | export function applyColor({ 41 | map, 42 | layerNames, 43 | color, 44 | type, 45 | }: // saturation, 46 | // lightness, 47 | ApplyProps): void { 48 | if (!type || !color) return; 49 | const { h, s, l } = hexToHSL(color); 50 | 51 | // if (saturation) { 52 | // return layerNames.forEach((layerName) => { 53 | // map.setPaintProperty(layerName, type, `hsl(${h}, ${saturation}%, ${l}%)`); 54 | // }); 55 | // } 56 | 57 | // if (lightness) { 58 | // return layerNames.forEach((layerName) => { 59 | // map.setPaintProperty(layerName, type, `hsl(${h}, ${s}%, ${lightness}%)`); 60 | // }); 61 | // } 62 | 63 | return layerNames.forEach((layerName) => { 64 | map.setPaintProperty(layerName, type, `hsl(${h}, ${s}%, ${l}%)`); 65 | }); 66 | } 67 | 68 | export function applyVisibility({ 69 | map, 70 | layerNames, 71 | visibility, 72 | }: ApplyProps): void { 73 | layerNames.forEach((layerName) => { 74 | if (visibility === 'inherit') { 75 | map.setLayoutProperty(layerName, 'visibility', 'visible'); 76 | } else map.setLayoutProperty(layerName, 'visibility', visibility); 77 | }); 78 | } 79 | 80 | export function applyWeight({ 81 | map, 82 | layerNames, 83 | type, 84 | weight = 1, 85 | }: ApplyProps): void { 86 | if (!type) return; 87 | const weightValue = weight === 0 ? 0 : weight; 88 | layerNames.forEach((layerName) => { 89 | map.setPaintProperty(layerName, type, weightValue); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/colorFormat.ts: -------------------------------------------------------------------------------- 1 | /** https://css-tricks.com/converting-color-spaces-in-javascript/ */ 2 | interface HexToHSLType { 3 | h: number; 4 | s: number; 5 | l: number; 6 | } 7 | 8 | export function hexToHSL(color: string): HexToHSLType { 9 | if (color === 'transparent') { 10 | return { 11 | h: 0, 12 | s: 0, 13 | l: 0, 14 | }; 15 | } 16 | let r = parseInt(color.slice(1, 3), 16); 17 | let g = parseInt(color.slice(3, 5), 16); 18 | let b = parseInt(color.slice(5), 16); 19 | 20 | r /= 255; 21 | g /= 255; 22 | b /= 255; 23 | const cmin = Math.min(r, g, b); 24 | const cmax = Math.max(r, g, b); 25 | const delta = cmax - cmin; 26 | let h = 0; 27 | let s = 0; 28 | let l = 0; 29 | 30 | if (delta === 0) h = 0; 31 | else if (cmax === r) h = ((g - b) / delta) % 6; 32 | else if (cmax === g) h = (b - r) / delta + 2; 33 | else h = (r - g) / delta + 4; 34 | 35 | h = Math.round(h * 60); 36 | 37 | if (h < 0) h += 360; 38 | 39 | l = (cmax + cmin) / 2; 40 | s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); 41 | s = +(s * 100).toFixed(1); 42 | l = +(l * 100).toFixed(1); 43 | 44 | return { 45 | h: Math.round(h), 46 | s: Math.round(s), 47 | l: Math.round(l), 48 | }; 49 | } 50 | 51 | export function hslToHEX(color: string): string { 52 | if (color === 'transparent' || !color) return 'transparent'; 53 | const hsl = color.match(/(\d*\.?\d+)/g)?.map((c) => Number(c)) as number[]; 54 | 55 | const h: number = hsl[0]; 56 | const s: number = hsl[1] / 100; 57 | const l: number = hsl[2] / 100; 58 | 59 | const c = (1 - Math.abs(2 * l - 1)) * s; 60 | const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); 61 | const m = l - c / 2; 62 | let r = 0; 63 | let g = 0; 64 | let b = 0; 65 | 66 | if (h >= 0 && h < 60) { 67 | r = c; 68 | g = x; 69 | b = 0; 70 | } else if (h >= 60 && h < 120) { 71 | r = x; 72 | g = c; 73 | b = 0; 74 | } else if (h >= 120 && h < 180) { 75 | r = 0; 76 | g = c; 77 | b = x; 78 | } else if (h >= 180 && h < 240) { 79 | r = 0; 80 | g = x; 81 | b = c; 82 | } else if (h >= 240 && h < 300) { 83 | r = x; 84 | g = 0; 85 | b = c; 86 | } else if (h >= 300 && h < 360) { 87 | r = c; 88 | g = 0; 89 | b = x; 90 | } 91 | // Having obtained RGB, convert channels to hex 92 | let stringR: string = Math.round((r + m) * 255).toString(16); 93 | let stringG: string = Math.round((g + m) * 255).toString(16); 94 | let stringB: string = Math.round((b + m) * 255).toString(16); 95 | 96 | // Prepend 0s, if necessary 97 | if (stringR.length === 1) stringR = `0${stringR}`; 98 | if (stringG.length === 1) stringG = `0${stringG}`; 99 | if (stringB.length === 1) stringB = `0${stringB}`; 100 | 101 | return `#${stringR}${stringG}${stringB}`; 102 | } 103 | -------------------------------------------------------------------------------- /src/utils/deepCopy.ts: -------------------------------------------------------------------------------- 1 | import { objType } from '../store/common/type'; 2 | 3 | function deepCopy(obj: objType): objType { 4 | if (obj === null || typeof obj !== 'object') { 5 | return obj; 6 | } 7 | 8 | const result: objType = Array.isArray(obj) ? [] : {}; 9 | Object.keys(obj).forEach((key) => { 10 | result[key] = deepCopy(obj[key]); 11 | }); 12 | 13 | return result; 14 | } 15 | 16 | export default deepCopy; 17 | -------------------------------------------------------------------------------- /src/utils/getRandomId.ts: -------------------------------------------------------------------------------- 1 | function getRandomId(length: number): string { 2 | let result = ''; 3 | const characters = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | 6 | const charactersLength = characters.length; 7 | // eslint-disable-next-line no-plusplus 8 | for (let i = 0; i < length; i++) { 9 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 10 | } 11 | return result; 12 | } 13 | 14 | export default getRandomId; 15 | -------------------------------------------------------------------------------- /src/utils/getTypeName.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElementNameType, 3 | SubElementNameType, 4 | StyleKeyType, 5 | ColorSubStyleType, 6 | ReplaceType, 7 | objType, 8 | } from '../store/common/type'; 9 | import featureTypeData from './rendering-data/featureTypeData'; 10 | 11 | const featureName = featureTypeData.reduce( 12 | (pre, cur) => { 13 | const name = pre; 14 | name.feature[cur.typeKey] = cur.typeName; 15 | name.subFeature[cur.typeKey] = { all: '전체' }; 16 | cur.subFeatures.forEach((sub) => { 17 | name.subFeature[cur.typeKey][sub.key] = sub.name; 18 | }); 19 | return name; 20 | }, 21 | { feature: {}, subFeature: {} } as objType 22 | ); 23 | 24 | const elementName = { 25 | element: { 26 | [ElementNameType.section]: '구역', 27 | [ElementNameType.labelText]: '라벨 > 텍스트', 28 | [ElementNameType.labelIcon]: '라벨 > 아이콘', 29 | }, 30 | subElement: { 31 | [SubElementNameType.fill]: '채우기', 32 | [SubElementNameType.stroke]: '테두리', 33 | }, 34 | style: { 35 | [StyleKeyType.visibility]: '가시성', 36 | [StyleKeyType.color]: '색상', 37 | [StyleKeyType.weight]: '굵기', 38 | [ColorSubStyleType.saturation]: '채도', 39 | [ColorSubStyleType.lightness]: '밝기', 40 | [StyleKeyType.isChanged]: '', 41 | }, 42 | }; 43 | 44 | const replaceName = { 45 | [ReplaceType.init]: '초기화', 46 | [ReplaceType.import]: '가져오기', 47 | [ReplaceType.theme]: '테마', 48 | [ReplaceType.depth]: '표기 단계 조절', 49 | }; 50 | 51 | const depthName = ['하', '중', '상']; 52 | 53 | export { featureName, elementName, replaceName, depthName }; 54 | -------------------------------------------------------------------------------- /src/utils/map-styling/administrative.ts: -------------------------------------------------------------------------------- 1 | import { stylingProps } from '.'; 2 | import { ElementNameType, StyleKeyType } from '../../store/common/type'; 3 | import { VisibilityType, WeightType } from '../applyStyle'; 4 | import seperatedLayers from './macgyver/seperatedLayers'; 5 | import { getIdsFromLayersArr, INVISIBLE, VISIBLE } from './macgyver/utils'; 6 | import { administrativeMappingToFunc } from './macgyver/mappingDetailToFunc'; 7 | 8 | function administrativeStyling({ 9 | map, 10 | subFeature, 11 | element, 12 | subElement, 13 | key, 14 | style, 15 | }: stylingProps): void { 16 | if (key === 'isChanged' || element === ElementNameType.labelIcon) return; 17 | 18 | const { typeName: type, funcName: func } = administrativeMappingToFunc[ 19 | element 20 | ][subElement][key]; 21 | if (!type || !func) return; 22 | 23 | /** get LayerNames */ 24 | let layerNames: string[] = []; 25 | if (element === ElementNameType.labelText) { 26 | layerNames = getIdsFromLayersArr( 27 | seperatedLayers.administrative[subFeature].labelText 28 | ); 29 | } else { 30 | if (!seperatedLayers.administrative[subFeature][element]) return; 31 | layerNames = getIdsFromLayersArr( 32 | seperatedLayers.administrative[subFeature][element][subElement] 33 | ); 34 | } 35 | 36 | /** styling */ 37 | if (key === StyleKeyType.visibility && type === WeightType.textHalo) { 38 | func({ 39 | map, 40 | layerNames, 41 | type, 42 | weight: style.visibility === VisibilityType.none ? INVISIBLE : VISIBLE, 43 | }); 44 | return; 45 | } 46 | 47 | func({ 48 | map, 49 | layerNames, 50 | type, 51 | color: style.color, 52 | [key]: style[key as StyleKeyType], 53 | }); 54 | } 55 | 56 | export default administrativeStyling; 57 | -------------------------------------------------------------------------------- /src/utils/map-styling/index.ts: -------------------------------------------------------------------------------- 1 | import mapboxgl from 'mapbox-gl'; 2 | import { 3 | ElementNameType, 4 | SubElementNameType, 5 | StyleKeyType, 6 | StyleType, 7 | } from '../../store/common/type'; 8 | 9 | export interface stylingProps { 10 | map: mapboxgl.Map; 11 | subFeature: string; 12 | element: ElementNameType; 13 | subElement: SubElementNameType; 14 | key: StyleKeyType; 15 | style: StyleType; 16 | } 17 | 18 | export { default as poi } from './poi'; 19 | export { default as road } from './road'; 20 | export { default as water } from './water'; 21 | export { default as administrative } from './administrative'; 22 | export { default as transit } from './transit'; 23 | export { default as landscape } from './landscape'; 24 | -------------------------------------------------------------------------------- /src/utils/map-styling/landscape.ts: -------------------------------------------------------------------------------- 1 | import { stylingProps } from './index'; 2 | import { WeightType, ColorType, VisibilityType } from '../applyStyle'; 3 | import { StyleKeyType, ElementNameType } from '../../store/common/type'; 4 | import seperatedLayers from './macgyver/seperatedLayers'; 5 | import { 6 | getIdsFromLayersArr, 7 | getIdsFromLayersArrWithType, 8 | getIdsFromLayersArrWithoutType, 9 | INVISIBLE, 10 | VISIBLE, 11 | } from './macgyver/utils'; 12 | import { landscapeMappingToFunc } from './macgyver/mappingDetailToFunc'; 13 | 14 | const BACKGROUND_TYPE = 'background'; 15 | 16 | function landscapeStyling({ 17 | map, 18 | subFeature, 19 | element, 20 | subElement, 21 | key, 22 | style, 23 | }: stylingProps): void { 24 | if (key === 'isChanged' || element === ElementNameType.labelIcon) return; 25 | 26 | const { typeName: type, funcName: func } = landscapeMappingToFunc[element][ 27 | subElement 28 | ][key]; 29 | if (!type || !func) return; 30 | 31 | /** get LayerNames */ 32 | let layerNames: string[] = []; 33 | let outsideLayerNames: string[] = []; 34 | if (element === ElementNameType.labelText) { 35 | layerNames = getIdsFromLayersArr( 36 | seperatedLayers.landscape[subFeature].labelText 37 | ); 38 | } else { 39 | layerNames = getIdsFromLayersArrWithoutType( 40 | seperatedLayers.landscape[subFeature][element][subElement], 41 | BACKGROUND_TYPE 42 | ); 43 | outsideLayerNames = getIdsFromLayersArrWithType( 44 | seperatedLayers.landscape[subFeature][element][subElement], 45 | BACKGROUND_TYPE 46 | ); 47 | } 48 | 49 | /** styling */ 50 | if (key === StyleKeyType.visibility && type === WeightType.textHalo) { 51 | func({ 52 | map, 53 | layerNames, 54 | type, 55 | weight: style.visibility === VisibilityType.none ? INVISIBLE : VISIBLE, 56 | }); 57 | return; 58 | } 59 | 60 | if (outsideLayerNames.length) { 61 | func({ 62 | map, 63 | layerNames: outsideLayerNames, 64 | type: ColorType.background, 65 | color: style.color, 66 | [key]: style[key as StyleKeyType], 67 | }); 68 | } 69 | 70 | func({ 71 | map, 72 | layerNames, 73 | type, 74 | color: style.color, 75 | [key]: style[key as StyleKeyType], 76 | }); 77 | } 78 | 79 | export default landscapeStyling; 80 | -------------------------------------------------------------------------------- /src/utils/map-styling/macgyver/seperatedLayers.ts: -------------------------------------------------------------------------------- 1 | import initLayers from '../../rendering-data/layers/init'; 2 | 3 | const seperatedLayers: any = {}; 4 | 5 | function isNumber(value: string | number): boolean { 6 | return !Number.isNaN(Number(value)); 7 | } 8 | 9 | /** feature / subFeature / element / subElement / value */ 10 | initLayers.layers.forEach((layer) => { 11 | const layerId = layer.id; 12 | const layerInfo = { id: layerId, type: layer.type }; 13 | const [feature, subFeature, element, subElement] = layerInfo.id.split('-'); 14 | if (feature && !seperatedLayers[feature]) { 15 | seperatedLayers[feature] = { all: {} }; 16 | } 17 | if (subFeature && !seperatedLayers[feature][subFeature]) { 18 | seperatedLayers[feature][subFeature] = {}; 19 | } 20 | if (element && seperatedLayers[feature][subFeature][element] === undefined) { 21 | if (subElement && !isNumber(subElement) && subElement !== undefined) { 22 | seperatedLayers[feature][subFeature][element] = {}; 23 | seperatedLayers[feature].all[element] = { 24 | ...seperatedLayers[feature].all[element], 25 | }; 26 | } else { 27 | seperatedLayers[feature][subFeature][element] = []; 28 | seperatedLayers[feature].all[element] = [].concat( 29 | seperatedLayers[feature].all[element] || [] 30 | ); 31 | } 32 | } 33 | if ( 34 | !isNumber(subElement) && 35 | subElement !== undefined && 36 | seperatedLayers[feature][subFeature][element][subElement] === undefined 37 | ) { 38 | seperatedLayers[feature][subFeature][element][subElement] = []; 39 | seperatedLayers[feature].all[element][subElement] = [].concat( 40 | seperatedLayers[feature].all[element][subElement] || [] 41 | ); 42 | } 43 | if (!isNumber(subElement) && subElement !== undefined) { 44 | seperatedLayers[feature][subFeature][element][subElement].push(layerInfo); 45 | seperatedLayers[feature].all[element][subElement].push(layerInfo); 46 | } else { 47 | seperatedLayers[feature][subFeature][element].push(layerInfo); 48 | seperatedLayers[feature].all[element].push(layerInfo); 49 | } 50 | }); 51 | 52 | export default seperatedLayers; 53 | -------------------------------------------------------------------------------- /src/utils/map-styling/macgyver/utils.ts: -------------------------------------------------------------------------------- 1 | import { objType } from '../../../store/common/type'; 2 | 3 | export const VISIBLE = 1; 4 | export const INVISIBLE = 0; 5 | 6 | export function getIdsFromLayersArr(seperatedLayers: objType): string[] { 7 | if (!seperatedLayers) return []; 8 | return seperatedLayers.map((layer: { id: string; type: string }) => layer.id); 9 | } 10 | 11 | export function getIdsFromLayersArrWithType( 12 | seperatedLayers: objType, 13 | type: string 14 | ): string[] { 15 | if (!seperatedLayers) return []; 16 | return seperatedLayers 17 | .filter((layer: { id: string; type: string }) => type === layer.type) 18 | .map((layer: { id: string; type: string }) => layer.id); 19 | } 20 | 21 | export function getIdsFromLayersArrWithoutType( 22 | seperatedLayers: objType, 23 | type: string 24 | ): string[] { 25 | if (!seperatedLayers) return []; 26 | return seperatedLayers 27 | .filter((layer: { id: string; type: string }) => type !== layer.type) 28 | .map((layer: { id: string; type: string }) => layer.id); 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/map-styling/macgyver/weightTemplate.ts: -------------------------------------------------------------------------------- 1 | export type WeightTemplateProperty = string | number | (string | number)[]; 2 | interface WeightTemplate { 3 | [subFeature: string]: { 4 | [element: string]: (weight: number) => WeightTemplateProperty[]; 5 | }; 6 | } 7 | 8 | const weightTemplate: WeightTemplate = { 9 | all: { 10 | fill: (weight: number): WeightTemplateProperty[] => [ 11 | 'interpolate', 12 | ['exponential', 1.5], 13 | ['zoom'], 14 | 12, 15 | weight, 16 | 14, 17 | weight * 2 + 2, 18 | 18, 19 | weight * 10 + 5, 20 | ], 21 | stroke: (weight: number): WeightTemplateProperty[] => [ 22 | 'interpolate', 23 | ['exponential', 1.5], 24 | ['zoom'], 25 | 12, 26 | weight, 27 | 20, 28 | weight * 1.5 + 1, 29 | ], 30 | }, 31 | highway: { 32 | fill: (weight: number): WeightTemplateProperty[] => [ 33 | 'interpolate', 34 | ['exponential', 1.5], 35 | ['zoom'], 36 | 5, 37 | weight, 38 | 18, 39 | weight * 5 + 25, 40 | ], 41 | stroke: (weight: number): WeightTemplateProperty[] => [ 42 | 'interpolate', 43 | ['exponential', 1.5], 44 | ['zoom'], 45 | 12, 46 | weight, 47 | 14, 48 | weight * 2, 49 | 18, 50 | weight * 2 + 10, 51 | ], 52 | }, 53 | arterial: { 54 | fill: (weight: number): WeightTemplateProperty[] => [ 55 | 'interpolate', 56 | ['exponential', 1.5], 57 | ['zoom'], 58 | 5, 59 | weight, 60 | 18, 61 | weight * 5 + 25, 62 | ], 63 | stroke: (weight: number): WeightTemplateProperty[] => [ 64 | 'interpolate', 65 | ['exponential', 1.5], 66 | ['zoom'], 67 | 10, 68 | weight, 69 | 18, 70 | weight * 1.5 + 1, 71 | ], 72 | }, 73 | local: { 74 | fill: (weight: number): WeightTemplateProperty[] => [ 75 | 'interpolate', 76 | ['exponential', 1.5], 77 | ['zoom'], 78 | 12, 79 | weight, 80 | 14, 81 | weight * 2 + 2, 82 | 18, 83 | weight * 20 + 5, 84 | ], 85 | stroke: (weight: number): WeightTemplateProperty[] => [ 86 | 'interpolate', 87 | ['exponential', 1.5], 88 | ['zoom'], 89 | 12, 90 | weight, 91 | 20, 92 | weight * 1.5 + 1, 93 | ], 94 | }, 95 | sidewalk: { 96 | fill: (weight: number): WeightTemplateProperty[] => [ 97 | 'interpolate', 98 | ['exponential', 1.5], 99 | ['zoom'], 100 | 14, 101 | weight, 102 | 18, 103 | weight * 15 + 5, 104 | ], 105 | stroke: (weight: number): WeightTemplateProperty[] => [ 106 | 'interpolate', 107 | ['exponential', 1.5], 108 | ['zoom'], 109 | 14, 110 | weight, 111 | 18, 112 | weight * 5 + 5, 113 | ], 114 | }, 115 | }; 116 | 117 | export default weightTemplate; 118 | -------------------------------------------------------------------------------- /src/utils/map-styling/poi.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | import { stylingProps } from './index'; 3 | import { 4 | WeightType, 5 | ColorType, 6 | StyleTypes, 7 | VisibilityType, 8 | } from '../applyStyle'; 9 | import { 10 | StyleKeyType, 11 | ElementNameType, 12 | PoiNameType, 13 | } from '../../store/common/type'; 14 | import seperatedLayers from './macgyver/seperatedLayers'; 15 | import { getIdsFromLayersArr, INVISIBLE, VISIBLE } from './macgyver/utils'; 16 | import { poiMappingToFunc } from './macgyver/mappingDetailToFunc'; 17 | 18 | function poiStyling({ 19 | map, 20 | subFeature, 21 | element, 22 | subElement, 23 | style, 24 | key, 25 | }: stylingProps): void { 26 | let type = null; 27 | let func = null; 28 | 29 | if (element === ElementNameType.section) return; 30 | if (element === ElementNameType.labelText && key !== 'isChanged') { 31 | const { typeName, funcName } = poiMappingToFunc[element][subElement][key]; 32 | type = typeName; 33 | func = funcName; 34 | } else if (element === ElementNameType.labelIcon && key === 'visibility') { 35 | const { typeName, funcName } = poiMappingToFunc[element][key]; 36 | type = typeName; 37 | func = funcName; 38 | } 39 | 40 | /** get LayerNames */ 41 | if (!type || !func) return; 42 | const layerNames = getIdsFromLayersArr( 43 | seperatedLayers.poi[subFeature as PoiNameType].labelText 44 | ); 45 | 46 | /** styling */ 47 | if ( 48 | key === StyleKeyType.visibility && 49 | (type === ColorType.icon || type === WeightType.textHalo) 50 | ) { 51 | func({ 52 | map, 53 | layerNames, 54 | type, 55 | weight: style.visibility === VisibilityType.none ? INVISIBLE : VISIBLE, 56 | }); 57 | return; 58 | } 59 | 60 | func({ 61 | map, 62 | layerNames, 63 | type: type as StyleTypes, 64 | color: style.color, 65 | [key]: style[key as StyleKeyType], 66 | }); 67 | } 68 | 69 | export default poiStyling; 70 | -------------------------------------------------------------------------------- /src/utils/map-styling/road.ts: -------------------------------------------------------------------------------- 1 | import { stylingProps } from '.'; 2 | import { ColorType } from '../applyStyle'; 3 | import { 4 | StyleKeyType, 5 | ElementNameType, 6 | SubElementNameType, 7 | } from '../../store/common/type'; 8 | import seperatedLayers from './macgyver/seperatedLayers'; 9 | import { 10 | getIdsFromLayersArr, 11 | getIdsFromLayersArrWithType, 12 | getIdsFromLayersArrWithoutType, 13 | } from './macgyver/utils'; 14 | import weightTemplate from './macgyver/weightTemplate'; 15 | import { roadMappingToFunc } from './macgyver/mappingDetailToFunc'; 16 | 17 | function roadStyling({ 18 | map, 19 | subFeature, 20 | element, 21 | subElement, 22 | key, 23 | style, 24 | }: stylingProps): void { 25 | if (key === 'isChanged') return; 26 | let type = null; 27 | let func = null; 28 | 29 | if (element === ElementNameType.labelIcon) { 30 | if (key !== 'visibility') return; 31 | const { typeName, funcName } = roadMappingToFunc[element][key]; 32 | type = typeName; 33 | func = funcName; 34 | } else { 35 | const { typeName, funcName } = roadMappingToFunc[element][subElement][key]; 36 | type = typeName; 37 | func = funcName; 38 | } 39 | 40 | /** get LayerNames */ 41 | if (!type || !func) return; 42 | let layerNames: string[] = []; 43 | let outsideLayerNames: string[] = []; 44 | if ( 45 | element === ElementNameType.labelText || 46 | element === ElementNameType.labelIcon 47 | ) { 48 | layerNames = getIdsFromLayersArr(seperatedLayers.road[subFeature][element]); 49 | } else { 50 | layerNames = getIdsFromLayersArrWithoutType( 51 | seperatedLayers.road[subFeature][element][subElement], 52 | SubElementNameType.fill 53 | ); 54 | outsideLayerNames = getIdsFromLayersArrWithType( 55 | seperatedLayers.road[subFeature][element][subElement], 56 | SubElementNameType.fill 57 | ); 58 | } 59 | 60 | /** set weight with weightTemplate */ 61 | let zoomWeight = null; 62 | if (key === StyleKeyType.weight) { 63 | zoomWeight = weightTemplate[subFeature][subElement](style.weight); 64 | } 65 | 66 | /** styling */ 67 | // fill일 때만 조건문 추가 68 | if (outsideLayerNames.length && key !== StyleKeyType.weight) { 69 | func({ 70 | map, 71 | layerNames: outsideLayerNames, 72 | type: ColorType.fill, 73 | color: style.color, 74 | [key]: style[key as StyleKeyType], 75 | }); 76 | } 77 | 78 | func({ 79 | map, 80 | layerNames, 81 | type, 82 | color: style.color, 83 | [key]: zoomWeight || style[key as StyleKeyType], 84 | }); 85 | } 86 | 87 | export default roadStyling; 88 | -------------------------------------------------------------------------------- /src/utils/map-styling/transit.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | /* eslint-disable no-case-declarations */ 3 | import { stylingProps } from '.'; 4 | import { StyleKeyType, ElementNameType } from '../../store/common/type'; 5 | import { VisibilityType, WeightType } from '../../utils/applyStyle'; 6 | import seperatedLayers from './macgyver/seperatedLayers'; 7 | import { getIdsFromLayersArr, INVISIBLE, VISIBLE } from './macgyver/utils'; 8 | import { transitMappingToFunc } from './macgyver/mappingDetailToFunc'; 9 | 10 | function transitStyling({ 11 | map, 12 | subFeature, 13 | key, 14 | element, 15 | subElement, 16 | style, 17 | }: stylingProps): void { 18 | if (key === 'isChanged' || element === ElementNameType.labelIcon) return; 19 | 20 | const { typeName: type, funcName: func } = transitMappingToFunc[element][ 21 | subElement 22 | ][key]; 23 | if (!type || !func) return; 24 | 25 | /** get LayerNames */ 26 | let layerNames: string[] = []; 27 | if (element === ElementNameType.labelText) { 28 | layerNames = getIdsFromLayersArr( 29 | seperatedLayers.transit[subFeature].labelText 30 | ); 31 | } else { 32 | if (!seperatedLayers.transit[subFeature][element]) return; 33 | layerNames = getIdsFromLayersArr( 34 | seperatedLayers.transit[subFeature][element][subElement] 35 | ); 36 | } 37 | 38 | /** styling */ 39 | if (key === StyleKeyType.visibility && type === WeightType.textHalo) { 40 | func({ 41 | map, 42 | layerNames, 43 | type, 44 | weight: style.visibility === VisibilityType.none ? INVISIBLE : VISIBLE, 45 | }); 46 | return; 47 | } 48 | 49 | func({ 50 | map, 51 | layerNames, 52 | type, 53 | color: style.color, 54 | [key]: style[key as StyleKeyType], 55 | }); 56 | } 57 | 58 | export default transitStyling; 59 | -------------------------------------------------------------------------------- /src/utils/map-styling/water.ts: -------------------------------------------------------------------------------- 1 | import { stylingProps } from '.'; 2 | import { ElementNameType, StyleKeyType } from '../../store/common/type'; 3 | import { VisibilityType, WeightType } from '../applyStyle'; 4 | import seperatedLayers from './macgyver/seperatedLayers'; 5 | import { getIdsFromLayersArr, INVISIBLE, VISIBLE } from './macgyver/utils'; 6 | import { waterMappingToFunc } from './macgyver/mappingDetailToFunc'; 7 | 8 | function waterStyling({ 9 | map, 10 | element, 11 | subElement, 12 | key, 13 | style, 14 | }: stylingProps): void { 15 | if (key === 'isChanged' || element === ElementNameType.labelIcon) return; 16 | 17 | const { typeName: type, funcName: func } = waterMappingToFunc[element][ 18 | subElement 19 | ][key]; 20 | if (!type || !func) return; 21 | 22 | /** get LayerNames */ 23 | let layerNames: string[] = []; 24 | if (element === ElementNameType.labelText) { 25 | layerNames = getIdsFromLayersArr(seperatedLayers.water.all.labelText); 26 | } else { 27 | layerNames = getIdsFromLayersArr( 28 | seperatedLayers.water.all[element][subElement] 29 | ); 30 | } 31 | 32 | /** styling */ 33 | if (key === StyleKeyType.visibility && type === WeightType.textHalo) { 34 | func({ 35 | map, 36 | layerNames, 37 | type, 38 | weight: style.visibility === VisibilityType.none ? INVISIBLE : VISIBLE, 39 | }); 40 | return; 41 | } 42 | 43 | func({ 44 | map, 45 | layerNames, 46 | type, 47 | color: style.color, 48 | [key]: style[key as StyleKeyType], 49 | }); 50 | } 51 | 52 | export default waterStyling; 53 | -------------------------------------------------------------------------------- /src/utils/removeNullFromObject.ts: -------------------------------------------------------------------------------- 1 | import { objType } from '../store/common/type'; 2 | 3 | function removeNullFromObject(object: objType): objType | undefined { 4 | if (typeof object !== 'object') return; 5 | 6 | Object.keys(object).forEach((key) => { 7 | if (object[key] === null) { 8 | // eslint-disable-next-line no-param-reassign 9 | delete object[key]; 10 | return; 11 | } 12 | removeNullFromObject(object[key]); 13 | }); 14 | 15 | // eslint-disable-next-line consistent-return 16 | return object; 17 | } 18 | 19 | export default removeNullFromObject; 20 | -------------------------------------------------------------------------------- /src/utils/rendering-data/featureTypeData.ts: -------------------------------------------------------------------------------- 1 | import { FeatureNameType } from '../../store/common/type'; 2 | 3 | export interface FeaturesType { 4 | key: string; 5 | name: string; 6 | } 7 | export interface DataType { 8 | typeKey: FeatureNameType; 9 | typeName: string; 10 | subFeatures: FeaturesType[]; 11 | } 12 | 13 | const data: DataType[] = [ 14 | { 15 | typeKey: FeatureNameType.poi, 16 | typeName: 'POI', 17 | subFeatures: [ 18 | { key: 'landmark', name: '랜드마크' }, 19 | { key: 'business', name: '상업시설' }, 20 | { key: 'government', name: '공공시설' }, 21 | { key: 'medical', name: '의료' }, 22 | { key: 'park', name: '공원' }, 23 | { key: 'worship', name: '종교시설' }, 24 | { key: 'school', name: '교육기관' }, 25 | { key: 'sports', name: '체육시설' }, 26 | { key: 'etc', name: '기타시설' }, 27 | ], 28 | }, 29 | { 30 | typeKey: FeatureNameType.road, 31 | typeName: '도로', 32 | subFeatures: [ 33 | { key: 'highway', name: '중심도로' }, 34 | { key: 'arterial', name: '주요도로' }, 35 | { key: 'local', name: '일반도로' }, 36 | { key: 'sidewalk', name: '인도' }, 37 | ], 38 | }, 39 | { 40 | typeKey: FeatureNameType.administrative, 41 | typeName: '행정구역', 42 | subFeatures: [ 43 | { key: 'country', name: '국가' }, 44 | { key: 'state', name: '도/주' }, 45 | { key: 'locality', name: '그외' }, 46 | ], 47 | }, 48 | { 49 | typeKey: FeatureNameType.landscape, 50 | typeName: '경관', 51 | subFeatures: [ 52 | { key: 'humanmade', name: '인공물' }, 53 | { key: 'building', name: '건물' }, 54 | { key: 'natural', name: '자연물' }, 55 | { key: 'landcover', name: '평지' }, 56 | { key: 'mountain', name: '산지' }, 57 | ], 58 | }, 59 | { 60 | typeKey: FeatureNameType.transit, 61 | typeName: '교통', 62 | subFeatures: [ 63 | { key: 'airport', name: '공항' }, 64 | { key: 'bus', name: '버스' }, 65 | { key: 'rail', name: '철도' }, 66 | { key: 'subway', name: '지하철' }, 67 | ], 68 | }, 69 | { typeKey: FeatureNameType.water, typeName: '물', subFeatures: [] }, 70 | ]; 71 | 72 | export default data; 73 | -------------------------------------------------------------------------------- /src/utils/rendering-data/sidebarThemeData.ts: -------------------------------------------------------------------------------- 1 | import christmas from './theme/christmas.json'; 2 | import blueprint from './theme/blueprint.json'; 3 | import dark from './theme/dark.json'; 4 | import sketch from './theme/sketch.json'; 5 | import monochrome from './theme/monochrome.json'; 6 | 7 | const data = [ 8 | { 9 | src: '/images/default.png', 10 | name: '표준', 11 | }, 12 | { 13 | src: '/images/monochrome.png', 14 | name: '모노크롬', 15 | theme: monochrome, 16 | }, 17 | { 18 | src: '/images/sketch.png', 19 | name: '스케치', 20 | theme: sketch, 21 | }, 22 | { 23 | src: '/images/dark.png', 24 | name: '밤', 25 | theme: dark, 26 | }, 27 | { 28 | src: '/images/christamas.png', 29 | name: '크리스마스', 30 | theme: christmas, 31 | }, 32 | { 33 | src: '/images/blueprint.png', 34 | name: '청사진', 35 | theme: blueprint, 36 | }, 37 | ]; 38 | 39 | export default data; 40 | -------------------------------------------------------------------------------- /src/utils/rendering-data/theme/blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "poi": { "all": { "labelText": { "fill": { "visibility": "none" } } } }, 3 | "landscape": { 4 | "all": { 5 | "section": { 6 | "fill": { "color": "#405CB0", "saturation": 47, "lightness": 47 } 7 | }, 8 | "labelText": { 9 | "fill": { 10 | "visibility": "none", 11 | "color": "#405CB0", 12 | "saturation": 40, 13 | "lightness": 49 14 | } 15 | } 16 | }, 17 | "building": { 18 | "section": { 19 | "stroke": { 20 | "color": "#ffffff", 21 | "weight": 2.5, 22 | "saturation": 0, 23 | "lightness": 100 24 | } 25 | } 26 | } 27 | }, 28 | "administrative": { 29 | "all": { 30 | "section": { "stroke": { "color": "#ffffff" } }, 31 | "labelText": { 32 | "fill": { "color": "#405CB0", "saturation": 46.7, "lightness": 47.1 }, 33 | "stroke": { "color": "#ffffff", "lightness": 100 } 34 | } 35 | } 36 | }, 37 | "road": { 38 | "all": { 39 | "section": { 40 | "fill": { "color": "#405CB0", "saturation": 47, "lightness": 47 }, 41 | "stroke": { "color": "#fafafa", "weight": 1.5, "lightness": 98 } 42 | }, 43 | "labelText": { 44 | "fill": { "color": "#405cb0", "saturation": 47, "lightness": 47 }, 45 | "stroke": { "visibility": "none", "color": "#fafafa", "lightness": 98 } 46 | }, 47 | "labelIcon": { "visibility": "none" } 48 | }, 49 | "local": { 50 | "section": { 51 | "stroke": { 52 | "color": "#ffffff", 53 | "weight": 3.5, 54 | "saturation": 0, 55 | "lightness": 100 56 | } 57 | } 58 | } 59 | }, 60 | "transit": { 61 | "all": { 62 | "section": { "stroke": { "color": "#ffffff", "lightness": 100 } }, 63 | "labelText": { 64 | "fill": { "color": "#405cb0", "saturation": 46.7, "lightness": 47.1 }, 65 | "stroke": { "color": "#ffffff", "lightness": 100 } 66 | } 67 | }, 68 | "airport": { 69 | "section": { 70 | "fill": { "color": "#405CB0" }, 71 | "stroke": { "color": "#ffffff" } 72 | } 73 | } 74 | }, 75 | "water": { 76 | "all": { 77 | "section": { "fill": { "color": "#405CB0" } }, 78 | "labelText": { 79 | "fill": { "color": "#405cb0", "saturation": 47, "lightness": 47 }, 80 | "stroke": { "color": "#ffffff", "lightness": 100 } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/rendering-data/theme/christmas.json: -------------------------------------------------------------------------------- 1 | { 2 | "poi": { 3 | "all": { 4 | "labelText": { 5 | "fill": { 6 | "visibility": "none", 7 | "color": "#d96459", 8 | "saturation": 63, 9 | "lightness": 60 10 | } 11 | } 12 | } 13 | }, 14 | "landscape": { 15 | "all": { 16 | "section": { 17 | "fill": { 18 | "color": "#588c73", 19 | "saturation": 23, 20 | "lightness": 45 21 | } 22 | }, 23 | "labelText": { 24 | "fill": { 25 | "color": "#eea36e", 26 | "saturation": 79, 27 | "lightness": 68 28 | } 29 | } 30 | }, 31 | "humanmade": { 32 | "section": { 33 | "fill": { 34 | "color": "#d96459", 35 | "saturation": 63, 36 | "lightness": 60 37 | } 38 | } 39 | }, 40 | "building": { 41 | "section": { 42 | "fill": { 43 | "color": "#d96459", 44 | "saturation": 63, 45 | "lightness": 60 46 | } 47 | }, 48 | "labelText": { 49 | "fill": { 50 | "visibility": "none" 51 | } 52 | } 53 | } 54 | }, 55 | "administrative": { 56 | "all": { 57 | "section": { 58 | "stroke": { 59 | "color": "#d96459", 60 | "weight": 2, 61 | "saturation": 63, 62 | "lightness": 60 63 | } 64 | }, 65 | "labelText": { 66 | "fill": { 67 | "color": "#d96459", 68 | "saturation": 63, 69 | "lightness": 60 70 | }, 71 | "stroke": { 72 | "color": "#eea36e", 73 | "saturation": 79, 74 | "lightness": 68 75 | } 76 | } 77 | } 78 | }, 79 | "road": { 80 | "all": { 81 | "section": { 82 | "fill": { 83 | "visibility": "none", 84 | "saturation": 47, 85 | "lightness": 47 86 | }, 87 | "stroke": { 88 | "visibility": "visible", 89 | "color": "#fafafa", 90 | "weight": 0.5, 91 | "lightness": 98 92 | } 93 | }, 94 | "labelText": { 95 | "fill": { 96 | "color": "#f7f8fd", 97 | "saturation": 60, 98 | "lightness": 98 99 | }, 100 | "stroke": { 101 | "visibility": "none", 102 | "color": "#fafafa", 103 | "lightness": 98 104 | } 105 | }, 106 | "labelIcon": { 107 | "visibility": "none" 108 | } 109 | } 110 | }, 111 | "transit": { 112 | "all": { 113 | "section": { 114 | "stroke": { 115 | "color": "#ffffff", 116 | "lightness": 100 117 | } 118 | }, 119 | "labelText": { 120 | "fill": { 121 | "color": "#eea36e", 122 | "saturation": 79, 123 | "lightness": 68 124 | }, 125 | "stroke": { 126 | "color": "#ffffff", 127 | "lightness": 100 128 | } 129 | } 130 | }, 131 | "airport": { 132 | "section": { 133 | "fill": { 134 | "color": "#ffffff" 135 | }, 136 | "stroke": { 137 | "color": "#ffffff" 138 | } 139 | }, 140 | "labelText": { 141 | "fill": { 142 | "color": "#eea36e", 143 | "saturation": 79, 144 | "lightness": 68 145 | } 146 | } 147 | } 148 | }, 149 | "water": { 150 | "all": { 151 | "section": { 152 | "fill": { 153 | "color": "#fafafa", 154 | "saturation": 0, 155 | "lightness": 98 156 | } 157 | }, 158 | "labelText": { 159 | "fill": { 160 | "visibility": "none" 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/utils/rendering-data/theme/dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "poi": { 3 | "all": { 4 | "labelText": { 5 | "fill": { "visibility": "none" } 6 | }, 7 | "labelIcon": { "visibility": "none" } 8 | } 9 | }, 10 | "landscape": { 11 | "all": { 12 | "section": { 13 | "fill": { "color": "#162131" } 14 | }, 15 | "labelText": { 16 | "fill": { "color": "#162131" }, 17 | "stroke": { 18 | "color": "#081217", 19 | 20 | "lightness": 6 21 | } 22 | } 23 | }, 24 | "building": { 25 | "labelText": { "fill": { "visibility": "none" } } 26 | }, 27 | "humanmade": { "section": { "fill": { "visibility": "none" } } }, 28 | "natural": { 29 | "fill": { "color": "#122623" } 30 | } 31 | }, 32 | "administrative": { 33 | "all": { 34 | "section": { "stroke": { "color": "#ffffff" } }, 35 | "labelText": { 36 | "fill": { "color": "#ef8521" }, 37 | "stroke": { "color": "#000000" } 38 | } 39 | } 40 | }, 41 | "road": { 42 | "all": { 43 | "section": { 44 | "fill": { "visibility": "none" }, 45 | "stroke": { 46 | "color": "#556577", 47 | "weight": 0.5 48 | } 49 | }, 50 | "labelText": { 51 | "fill": { "color": "#d9d86d" }, 52 | "stroke": { "color": "#000000" } 53 | }, 54 | "labelIcon": { "visibility": "none" } 55 | } 56 | }, 57 | "transit": { 58 | "all": { 59 | "section": { "stroke": { "color": "#ffffff", "lightness": 100 } }, 60 | "labelText": { 61 | "fill": { "color": "#162131", "saturation": 38, "lightness": 14 }, 62 | "stroke": { "color": "#ffffff", "lightness": 100 } 63 | } 64 | }, 65 | "airport": { 66 | "section": { 67 | "fill": { "color": "#ffffff" }, 68 | "stroke": { "color": "#000000" } 69 | }, 70 | "labelText": { 71 | "fill": { "color": "#ffffff" }, 72 | "stroke": { "color": "#000000" } 73 | } 74 | } 75 | }, 76 | "water": { 77 | "all": { 78 | "section": { 79 | "fill": { "color": "#081217", "saturation": 48, "lightness": 6 } 80 | }, 81 | "labelText": { 82 | "fill": { 83 | "visibility": "none" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/rendering-data/theme/monochrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "poi": { 3 | "all": { 4 | "labelText": { 5 | "fill": { 6 | "color": "#9cfcd2" 7 | }, 8 | "stroke": { 9 | "visibility": "visible", 10 | "color": "#070808" 11 | } 12 | } 13 | } 14 | }, 15 | "road": { 16 | "all": { 17 | "section": { 18 | "fill": { 19 | "color": "#4aa57e" 20 | } 21 | } 22 | } 23 | }, 24 | "administrative": { 25 | "all": { 26 | "section": { 27 | "stroke": { 28 | "color": "#ffffff" 29 | } 30 | }, 31 | "labelText": { 32 | "fill": { 33 | "color": "#9cfcd2" 34 | }, 35 | "stroke": { 36 | "color": "#070808" 37 | } 38 | } 39 | } 40 | }, 41 | "landscape": { 42 | "all": { 43 | "section": { 44 | "fill": { 45 | "color": "#070808" 46 | } 47 | }, 48 | "labelText": { 49 | "fill": { 50 | "color": "#9cfcd2" 51 | }, 52 | "stroke": { 53 | "color": "#070808" 54 | } 55 | } 56 | }, 57 | "building": { 58 | "section": { 59 | "fill": { 60 | "visibility": "visible", 61 | "color": "#87cfb0" 62 | }, 63 | "stroke": { 64 | "color": "#070808" 65 | } 66 | }, 67 | "labelText": { 68 | "fill": { 69 | "visibility": "visible" 70 | }, 71 | "stroke": { 72 | "color": "#070808" 73 | } 74 | } 75 | }, 76 | "landcover": { 77 | "section": { 78 | "fill": { 79 | "color": "#070808" 80 | } 81 | } 82 | } 83 | }, 84 | "transit": { 85 | "all": { 86 | "section": { 87 | "stroke": { 88 | "color": "#ffffff" 89 | } 90 | }, 91 | "labelText": { 92 | "fill": { 93 | "color": "#9cfcd2" 94 | }, 95 | "stroke": { 96 | "color": "#070808" 97 | } 98 | } 99 | }, 100 | "airport": { 101 | "section": { 102 | "fill": { 103 | "color": "#9cfcd2" 104 | }, 105 | "stroke": { 106 | "color": "#070808" 107 | } 108 | } 109 | } 110 | }, 111 | "water": { 112 | "all": { 113 | "section": { 114 | "fill": { 115 | "color": "#182f25" 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/utils/rendering-data/theme/sketch.json: -------------------------------------------------------------------------------- 1 | { 2 | "poi": { 3 | "all": { 4 | "labelText": { 5 | "fill": { "color": "#1A1A1A", "weight": 0 }, 6 | "stroke": { "color": "#E6E6E6", "weight": 2 } 7 | }, 8 | "labelIcon": { "visibility": "none" } 9 | } 10 | }, 11 | "road": { 12 | "all": { 13 | "section": { 14 | "fill": { "color": "#E6E6E6", "weight": 2 }, 15 | "stroke": { "color": "#1A1A1A", "weight": 1 } 16 | }, 17 | "labelText": { 18 | "fill": { "color": "#1A1A1A", "weight": 0 }, 19 | "stroke": { "color": "#E6E6E6", "weight": 2 } 20 | }, 21 | "labelIcon": { "visibility": "none" } 22 | } 23 | }, 24 | "administrative": { 25 | "all": { 26 | "section": { 27 | "fill": { "color": "#E6E6E6", "weight": 0 }, 28 | "stroke": { "color": "#1A1A1A", "weight": 1 } 29 | }, 30 | "labelText": { 31 | "fill": { "color": "#1A1A1A", "weight": 0 }, 32 | "stroke": { "color": "#E6E6E6", "weight": 2 } 33 | } 34 | } 35 | }, 36 | "landscape": { 37 | "all": { 38 | "section": { 39 | "fill": { "color": "#E6E6E6", "weight": 0 }, 40 | "stroke": { "color": "#1A1A1A", "weight": 1 } 41 | }, 42 | "labelText": { 43 | "fill": { "color": "#1A1A1A", "weight": 0 }, 44 | "stroke": { "color": "#E6E6E6", "weight": 2 } 45 | } 46 | } 47 | }, 48 | "transit": { 49 | "all": { 50 | "section": { 51 | "fill": { "color": "#E6E6E6", "weight": 0 }, 52 | "stroke": { "color": "#1A1A1A", "weight": 1 } 53 | }, 54 | "labelText": { 55 | "fill": { "color": "#1A1A1A", "weight": 0 }, 56 | "stroke": { "color": "#E6E6E6", "weight": 2 } 57 | } 58 | } 59 | }, 60 | "water": { 61 | "all": { 62 | "section": { 63 | "fill": { "color": "#E6E6E6", "weight": 0 }, 64 | "stroke": { "color": "#1A1A1A", "weight": 1 } 65 | }, 66 | "labelText": { 67 | "fill": { "color": "#1A1A1A", "weight": 0 }, 68 | "stroke": { "color": "#E6E6E6", "weight": 2 } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/setFeatureStyle.ts: -------------------------------------------------------------------------------- 1 | import mapboxgl from 'mapbox-gl'; 2 | import { 3 | FeatureState, 4 | ActionPayload, 5 | SubElementType, 6 | StyleType, 7 | FeatureNameType, 8 | ElementNameType, 9 | SubElementNameType, 10 | StyleKeyType, 11 | } from '../store/common/type'; 12 | import * as mapStyling from './map-styling'; 13 | 14 | interface setWholeStyleProps { 15 | map: mapboxgl.Map; 16 | feature: FeatureNameType; 17 | featureState: FeatureState; 18 | } 19 | 20 | /* eslint-disable no-restricted-syntax */ 21 | function setFeatureStyle({ 22 | map, 23 | feature, 24 | featureState, 25 | }: setWholeStyleProps): void { 26 | const subFeatures = Object.keys(featureState); 27 | const sortedFeatures = subFeatures.sort((x, y) => { 28 | if (featureState[x].isChanged || !featureState[y].isChanged) return 1; 29 | return -1; 30 | }); 31 | const isInit = 32 | subFeatures.findIndex( 33 | (subFeature) => featureState[subFeature].isChanged 34 | ) === -1; 35 | 36 | for (const subFeature of sortedFeatures) { 37 | const elements = Object.keys(featureState[subFeature]) as ElementNameType[]; 38 | for (const element of elements) { 39 | const elementStyle = featureState[subFeature][element]; 40 | if (elementStyle) { 41 | switch (element) { 42 | case ElementNameType.labelIcon: 43 | setElementStyle({ 44 | map, 45 | feature, 46 | subFeature, 47 | element, 48 | style: elementStyle as StyleType, 49 | isInit, 50 | }); 51 | break; 52 | case ElementNameType.labelText: 53 | case ElementNameType.section: 54 | (Object.keys( 55 | elementStyle as SubElementType 56 | ) as SubElementNameType[]).forEach((subElement) => { 57 | setElementStyle({ 58 | map, 59 | feature, 60 | subFeature, 61 | element, 62 | subElement, 63 | style: (elementStyle as SubElementType)[ 64 | subElement 65 | ] as StyleType, 66 | isInit, 67 | }); 68 | }); 69 | break; 70 | default: 71 | break; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | interface setElementStyleProps extends ActionPayload { 79 | map: mapboxgl.Map; 80 | isInit: boolean; 81 | } 82 | 83 | function setElementStyle({ 84 | map, 85 | feature, 86 | subFeature, 87 | element, 88 | subElement, 89 | style, 90 | isInit, 91 | }: setElementStyleProps): void { 92 | if (!style.isChanged && !isInit) return; 93 | const keys = Object.keys(style) as StyleKeyType[]; 94 | keys.forEach((key) => { 95 | if (key === 'color' && style[key] === 'transparent') { 96 | return; 97 | } 98 | mapStyling[feature]({ 99 | map, 100 | subFeature, 101 | key, 102 | element, 103 | subElement: subElement as SubElementNameType, 104 | style, 105 | }); 106 | }); 107 | } 108 | 109 | export default setFeatureStyle; 110 | -------------------------------------------------------------------------------- /src/utils/styles/globalStyle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import emotionReset from 'emotion-reset'; 3 | import { Global, css } from '@emotion/core'; 4 | 5 | function GlobalStyle(): React.ReactElement { 6 | return ( 7 | 35 | ); 36 | } 37 | 38 | export default GlobalStyle; 39 | -------------------------------------------------------------------------------- /src/utils/styles/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { CreateStyled } from '@emotion/styled'; 2 | 3 | interface ThemeType { 4 | [color: string]: string; 5 | } 6 | 7 | export const theme: ThemeType = { 8 | BLACK: '#000000', 9 | GREEN: '#3ECF5C', 10 | LIGHTGREY: '#E0E0E0', 11 | GREY: '#9E9E9E', 12 | DARKGREY: '#616161', 13 | DEEPGREY: '#434343', 14 | WHITE: '#FFFFFF', 15 | GOOGLE_GREY: '#f5f5f5', 16 | RED: '#cb2431', 17 | }; 18 | 19 | export default styled as CreateStyled; 20 | -------------------------------------------------------------------------------- /src/utils/updateMarkerStorage.ts: -------------------------------------------------------------------------------- 1 | import mapboxgl from 'mapbox-gl'; 2 | import { MarkerInstanceType, MARKER } from '../store/marker/action'; 3 | import getRandomId from './getRandomId'; 4 | 5 | const initialLocalStorageMarkers: MarkerInstanceType[] = []; 6 | 7 | const getMarkersFromLocalStorage = (): MarkerInstanceType[] => { 8 | return JSON.parse(localStorage.getItem(MARKER) as string); 9 | }; 10 | 11 | export const setMarkersToLocalStorage = ( 12 | markers: MarkerInstanceType[] 13 | ): void => { 14 | localStorage.setItem(MARKER, JSON.stringify(markers)); 15 | }; 16 | 17 | export function initMarkerInstances( 18 | markers: MarkerInstanceType[] 19 | ): MarkerInstanceType[] { 20 | const newMarkers = markers.reduce( 21 | ( 22 | acc: MarkerInstanceType[], 23 | marker: MarkerInstanceType 24 | ): MarkerInstanceType[] => { 25 | const newMarker = new mapboxgl.Marker({ 26 | draggable: true, 27 | }) 28 | .setLngLat([marker.lng, marker.lat]) 29 | .setPopup(new mapboxgl.Popup().setHTML(`

${marker.text}

`)); 30 | const newMarkerId = marker.id || getRandomId(8); 31 | 32 | acc.push({ ...marker, id: newMarkerId, instance: newMarker }); 33 | return acc; 34 | }, 35 | [] 36 | ); 37 | return newMarkers; 38 | } 39 | 40 | export function getInitialMarkersFromLocalStorage(): MarkerInstanceType[] { 41 | const storedMarkers = getMarkersFromLocalStorage(); 42 | if (!storedMarkers) return initialLocalStorageMarkers; 43 | const newState = initMarkerInstances(storedMarkers); 44 | return newState; 45 | } 46 | 47 | export function setNewMarkerToLocalStorage({ 48 | id, 49 | text, 50 | lng, 51 | lat, 52 | }: MarkerInstanceType): void { 53 | const storedMarker = getMarkersFromLocalStorage(); 54 | const markerArray = storedMarker ?? []; 55 | 56 | markerArray.push({ 57 | id, 58 | text, 59 | lng, 60 | lat, 61 | }); 62 | 63 | setMarkersToLocalStorage(markerArray); 64 | } 65 | 66 | export function updateMarkerOfLocalStorage({ 67 | id, 68 | lng, 69 | lat, 70 | }: MarkerInstanceType): void { 71 | const storedMarkers = getMarkersFromLocalStorage(); 72 | const changedMarkers = storedMarkers.map((marker) => { 73 | const targetMarker = 74 | marker.id === id 75 | ? { ...marker, lng: lng ?? marker.lng, lat: lat ?? marker.lat } 76 | : marker; 77 | return targetMarker; 78 | }); 79 | 80 | setMarkersToLocalStorage(changedMarkers); 81 | } 82 | 83 | export function deleteMarkerOfLocalStorage(id: string): void { 84 | const storedMarkers = getMarkersFromLocalStorage(); 85 | const targetMarker = storedMarkers.find((marker) => marker.id === id); 86 | targetMarker?.instance?.remove(); 87 | const changedMarkers = storedMarkers.filter((marker) => marker.id !== id); 88 | setMarkersToLocalStorage(changedMarkers); 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/validateStyle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WholeStyleActionPayload, 3 | SubFeatureActionPayload, 4 | StyleActionPayload, 5 | ElementActionPayload, 6 | SubElementActionPayload, 7 | StyleElementPropsType, 8 | FeatureNameType, 9 | ElementNameType, 10 | SubElementNameType, 11 | VisibilityValueType, 12 | StyleKeyType, 13 | } from '../store/common/type'; 14 | import initialLayers from './rendering-data/defaultStyle'; 15 | 16 | /* eslint-disable no-restricted-syntax */ 17 | export default function validateStyle( 18 | inputStyle: WholeStyleActionPayload 19 | ): boolean { 20 | const features = Object.keys(inputStyle); 21 | 22 | for (const feature of features) { 23 | if (!(feature in FeatureNameType)) return false; 24 | 25 | const inputFeatureStyle = inputStyle[ 26 | feature as FeatureNameType 27 | ] as SubFeatureActionPayload; 28 | const subFeatures = Object.keys(inputFeatureStyle); 29 | 30 | for (const subFeature of subFeatures) { 31 | const subFeatureStyle = inputFeatureStyle[subFeature]; 32 | const initialSubFeatureStyle = 33 | initialLayers[feature as FeatureNameType][subFeature]; 34 | 35 | if ( 36 | !initialSubFeatureStyle || 37 | typeof subFeatureStyle !== 'object' || 38 | !checkElement({ 39 | subFeatureStyle, 40 | initialSubFeatureStyle: initialSubFeatureStyle as StyleElementPropsType, 41 | }) 42 | ) { 43 | return false; 44 | } 45 | } 46 | } 47 | return true; 48 | } 49 | 50 | interface checkElementProps { 51 | subFeatureStyle: ElementActionPayload; 52 | initialSubFeatureStyle: StyleElementPropsType; 53 | } 54 | 55 | function checkElement({ 56 | subFeatureStyle, 57 | initialSubFeatureStyle, 58 | }: checkElementProps): boolean { 59 | const elements = Object.keys(subFeatureStyle) as ElementNameType[]; 60 | 61 | for (const element of elements) { 62 | const elementSytle = subFeatureStyle[element]; 63 | if (!initialSubFeatureStyle[element] || typeof elementSytle !== 'object') 64 | return false; 65 | 66 | if ( 67 | element === ElementNameType.labelIcon && 68 | !checkStyle(elementSytle as StyleActionPayload) 69 | ) { 70 | return false; 71 | } 72 | 73 | if ( 74 | (element === ElementNameType.labelText || 75 | element === ElementNameType.section) && 76 | !checkSubElement(elementSytle as SubElementActionPayload) 77 | ) { 78 | return false; 79 | } 80 | } 81 | 82 | return true; 83 | } 84 | 85 | function checkSubElement(input: SubElementActionPayload): boolean { 86 | const subElements = Object.keys(input) as SubElementNameType[]; 87 | 88 | for (const subElement of subElements) { 89 | const subElementSytle = (input as SubElementActionPayload)[subElement]; 90 | 91 | if ( 92 | !(subElement in SubElementNameType) || 93 | typeof subElementSytle !== 'object' || 94 | !checkStyle(subElementSytle as StyleActionPayload) 95 | ) { 96 | return false; 97 | } 98 | } 99 | 100 | return true; 101 | } 102 | 103 | function checkStyle(input: StyleActionPayload): boolean { 104 | const keys = Object.keys(input) as StyleKeyType[]; 105 | for (const key of keys) { 106 | if (!(key in StyleKeyType)) return false; 107 | const style = input[key]; 108 | switch (key) { 109 | case StyleKeyType.color: 110 | if (!checkColor(style)) return false; 111 | break; 112 | case StyleKeyType.weight: 113 | if (!checkRange(style, 0, 8)) return false; 114 | break; 115 | case StyleKeyType.visibility: 116 | if (!((input.visibility as VisibilityValueType) in VisibilityValueType)) 117 | return false; 118 | break; 119 | default: 120 | return false; 121 | } 122 | } 123 | 124 | return true; 125 | } 126 | 127 | function checkColor(inputColor: T): boolean { 128 | if (typeof inputColor !== 'string') return false; 129 | const color = inputColor.replaceAll(' ', ''); 130 | const hexReg = /^#[0-9A-F]{6}$/i; 131 | if (!color.match(hexReg)) return false; 132 | return true; 133 | } 134 | 135 | function checkRange(input: T, min: number, max: number): boolean { 136 | if (typeof input !== 'number') return false; 137 | if (input < min || input > max) return false; 138 | return true; 139 | } 140 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------