├── .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 |
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 |
25 |
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 |
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 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Icon/Compare.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default (): React.ReactElement => {
4 | return (
5 |
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 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/Icon/FullScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default (): React.ReactElement => {
4 | return (
5 |
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 |
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 |
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 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Icon/SmallScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default (): React.ReactElement => {
4 | return (
5 |
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 |
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 |
--------------------------------------------------------------------------------