├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── README.md
├── craco.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── analytics.js
├── background.html
├── favicon.ico
├── images
│ └── logo
│ │ ├── group-5.svg
│ │ ├── logo128.png
│ │ ├── logo16.png
│ │ ├── logo24.png
│ │ ├── logo32.png
│ │ └── logo64.png
├── index.html
├── manifest.json
├── popup.html
└── robots.txt
└── src
├── assets
├── css
│ ├── default.css
│ └── popup.css
├── images
│ ├── add.png
│ ├── alarm.png
│ ├── background1.jpg
│ ├── background2.jpg
│ ├── card-image.jpg
│ ├── check-false.png
│ ├── check-true.png
│ ├── close.svg
│ ├── group-10.png
│ ├── group-11.svg
│ ├── group-17.svg
│ ├── group-19.png
│ ├── group-19.svg
│ ├── group-25.svg
│ ├── group-26.svg
│ ├── group-27.svg
│ ├── group-5.svg
│ ├── history.png
│ ├── info-person.jpg
│ ├── link-copy.png
│ ├── link-copy.svg
│ ├── link-icon.png
│ ├── link-icon.svg
│ ├── link.svg
│ ├── linkListEmptyIcon.png
│ ├── link_icon.svg
│ ├── logo-google.png
│ ├── logo-urlink-box.png
│ ├── logo-urlink-full.png
│ ├── logo.png
│ ├── logo
│ │ ├── group-2.svg
│ │ ├── group-3.svg
│ │ ├── logo128.png
│ │ ├── logo16.png
│ │ ├── logo24.png
│ │ ├── logo32.png
│ │ ├── logo64.png
│ │ ├── profileImg.png
│ │ ├── profileImg@2x.png
│ │ └── profileImg@3x.png
│ ├── main1.png
│ ├── main1_back.png
│ ├── main2.png
│ ├── main2_back.png
│ ├── main3.png
│ ├── main3_back.png
│ ├── main4.png
│ ├── main5.png
│ ├── mainBackground.png
│ ├── mainBackground2.png
│ ├── mainLogo.png
│ ├── more.png
│ ├── more_3dot.svg
│ ├── move.png
│ ├── new-tab.svg
│ ├── open.png
│ ├── person.png
│ ├── plus.png
│ ├── plus_noborder.svg
│ ├── search.png
│ ├── search.svg
│ ├── star.svg
│ ├── star_fill.svg
│ ├── union.svg
│ └── white.svg
└── scss
│ ├── LoginSignup.scss
│ ├── font.scss
│ └── swiper.scss
├── background
└── index.js
├── hooks
├── useDebounce.js
├── useEventListener.js
└── useOutsideAlerter.js
├── main
├── App.js
├── components
│ ├── ExtendedFab
│ │ ├── index.jsx
│ │ └── style.js
│ ├── ScrollUpButton
│ │ ├── index.jsx
│ │ └── style.js
│ ├── SearchBar
│ │ ├── index.jsx
│ │ └── style.js
│ ├── SearchButton
│ │ ├── index.jsx
│ │ └── style.js
│ ├── Toast
│ │ └── index.jsx
│ ├── ValidationMessage
│ │ ├── index.jsx
│ │ └── style.js
│ └── modals
│ │ ├── AlertModal
│ │ ├── index.jsx
│ │ └── style.js
│ │ ├── TermsModal
│ │ ├── index.jsx
│ │ └── textInfo.js
│ │ ├── index.jsx
│ │ └── style.js
├── index.js
├── pages
│ ├── Home
│ │ ├── AppBar
│ │ │ ├── AlarmList
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── DraggableHistoryList
│ │ │ │ ├── History
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.js
│ │ │ │ ├── HistoryDateTitle
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.js
│ │ │ │ ├── HistoryDragBox
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.js
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── Profile
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── CategoryList
│ │ │ ├── AddCategoryModal
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── CategoryHeader
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── CategoryItem
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── CategoryItemWrapper
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── FirstCategoryDropZone
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── FirstFavoriteDropZone
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── UpdateCategoryModal
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── LinkDropZone
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── LinkList
│ │ │ ├── Header
│ │ │ │ ├── EditableCategoryTitle
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.js
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── Link
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── LinkSkeleton
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.js
│ │ │ ├── index.jsx
│ │ │ └── style.js
│ │ ├── index.jsx
│ │ └── style.js
│ ├── Login
│ │ ├── LoginForm
│ │ │ ├── GloginButton.jsx
│ │ │ ├── NloginForm.jsx
│ │ │ └── index.jsx
│ │ └── index.jsx
│ ├── Register
│ │ ├── RegisterForm
│ │ │ ├── GregisterButton.jsx
│ │ │ ├── NregisterForm.jsx
│ │ │ └── index.jsx
│ │ └── index.jsx
│ └── Start
│ │ ├── index.jsx
│ │ └── style.js
└── theme.js
├── modules
├── alarm
│ ├── api.js
│ ├── index.js
│ ├── queryInfoData.js
│ ├── reducer.js
│ └── saga.js
├── alarmNotice
│ ├── hooks
│ │ └── useAlarmNoticeConnection.js
│ ├── index.js
│ ├── queryInfoData.js
│ ├── reducer.js
│ ├── saga.js
│ └── ws.js
├── category
│ ├── api.js
│ ├── hooks
│ │ └── useCategories.js
│ ├── index.js
│ ├── queryInfoData.js
│ ├── reducer.js
│ └── saga.js
├── error
│ ├── index.js
│ └── reducer.js
├── helpers.js
├── historyLink
│ ├── hooks
│ │ └── useHistoryLinks.js
│ ├── index.js
│ ├── reducer.js
│ └── saga.js
├── index.js
├── link
│ ├── api.js
│ ├── hooks
│ │ └── useLinks.js
│ ├── index.js
│ ├── queryInfoData.js
│ ├── reducer.js
│ └── saga.js
├── pending
│ ├── constants.js
│ ├── index.js
│ └── reducer.js
├── token
│ ├── api.js
│ ├── index.js
│ └── queryInfoData.js
├── ui
│ ├── constants.js
│ ├── hooks
│ │ ├── useDialog.js
│ │ ├── useDrag.js
│ │ ├── useDropZone.js
│ │ └── useToast.js
│ ├── index.js
│ └── reducer.js
└── user
│ ├── api.js
│ ├── hooks
│ └── useUser.js
│ ├── index.js
│ ├── queryInfoData.js
│ ├── reducer.js
│ └── saga.js
├── popup
└── index.js
├── setting.js
└── utils
├── chromeApis
├── OAuth.js
├── bookmarks.js
├── history.js
├── onMessage.js
├── sendMessage.js
└── tab.js
├── copyLink.js
├── filter.js
├── ga.js
└── http
├── auth.js
├── client.js
├── queryFilter.js
└── ws.js
/.eslintignore:
--------------------------------------------------------------------------------
1 |
2 | /.vscode
3 | node_modules
4 | dist
5 | build
6 | eslintrc
7 | src/assets/*
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | root: true,
5 | extends: ['eslint:recommended', 'plugin:react/recommended', 'eslint-config-prettier', 'plugin:prettier/recommended'],
6 | plugins: ['react', 'react-hooks', 'import', 'prettier'],
7 | env: {
8 | browser: true,
9 | node: true,
10 | es6: true,
11 | },
12 | parserOptions: {
13 | ecmaVersion: 2020,
14 | sourceType: 'module',
15 | ecmaFeatures: {
16 | jsx: true,
17 | },
18 | },
19 | globals: {
20 | chrome: 'readonly',
21 | },
22 | settings: {
23 | 'import/resolver': {
24 | alias: {
25 | map: [
26 | ['@/*', path.resolve(__dirname, './src')],
27 | ['@background/*', path.resolve(__dirname, './src/background')],
28 | ['@main/*', path.resolve(__dirname, './src/main')],
29 | ['@popup/*', path.resolve(__dirname, './src/popup')],
30 | ['@assets/*', path.resolve(__dirname, './src/assets')],
31 | ['@hooks/*', path.resolve(__dirname, './src/hooks')],
32 | ['@modules/*', path.resolve(__dirname, './src/modules')],
33 | ['@utils/*', path.resolve(__dirname, './src/utils')],
34 | ],
35 | extensions: ['.js', '.jsx', '.json'],
36 | },
37 | },
38 | },
39 | rules: {
40 | 'react/prop-types': [0],
41 | 'no-unused-vars': 'off',
42 | 'react-hooks/rules-of-hooks': 'error',
43 | 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
44 | 'import/order': [
45 | 'error',
46 | {
47 | groups: ['builtin', 'external', 'internal'],
48 | pathGroups: [
49 | {
50 | pattern: 'react',
51 | group: 'external',
52 | position: 'before',
53 | },
54 | ],
55 | pathGroupsExcludedImportTypes: ['react'],
56 | 'newlines-between': 'always',
57 | alphabetize: {
58 | order: 'asc',
59 | caseInsensitive: true,
60 | },
61 | },
62 | ],
63 | 'prettier/prettier': [
64 | 'error',
65 | {
66 | endOfLine: 'auto',
67 | },
68 | ],
69 | },
70 | }
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | .idea
4 |
5 | # dependencies
6 | /node_modules
7 | /.pnp
8 | .pnp.js
9 | .eslintcache
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 | /public/popup
17 |
18 | # misc
19 | .DS_Store
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | rest.http
29 |
30 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "semi": false,
5 | "useTabs": false,
6 | "tabWidth": 2,
7 | "trailingComma": "es5"
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "javascript.format.enable": false,
3 | "editor.formatOnSave": false,
4 | "[javascript]": {
5 | "editor.formatOnSave": true
6 | },
7 | "[json]": {
8 | "editor.formatOnSave": true
9 | },
10 | "[html]": {
11 | "editor.formatOnSave": true
12 | },
13 | "eslint.options": {
14 | "extensions": [
15 | ".js",
16 | ".jsx",
17 | ],
18 | "validate": [
19 | "javascript",
20 | "javascriptreact",
21 | ]
22 | },
23 | "eslint.workingDirectories": [
24 | {
25 | "mode": "auto"
26 | }
27 | ],
28 | "eslint.alwaysShowStatus": true,
29 | "editor.codeActionsOnSave": {
30 | "source.organizeImports": false,
31 | "source.fixAll.eslint": true
32 | },
33 | "cSpell.words": [
34 | "favorited",
35 | "getcontentanchorel",
36 | "Glogin",
37 | "Gregister",
38 | "Nlogin",
39 | "Nregister",
40 | "pageview",
41 | "scrollmagic",
42 | "Spoqa"
43 | ],
44 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # urLink Chrome Extension
2 |
3 | 
4 |
5 |
6 |
7 |
8 |
9 | # urLink
10 |
11 |
12 | ### 🤔 내가 보관한 링크, 어디다 뒀더라..?
13 |
14 | 북마크, 메모장, 카카오톡 내게 쓰기, 노션 등.. 언젠가 보려고 여기저기 저장한 링크들, 찾으려고 하면 어디에 있는지 도저히 기억이 안나시나요? 이제 한 곳에서 한 번에 정리하고 사용하세요.
15 |
16 | 유어링크를 사용하면 쉽고 빠르게 웹사이트를 정리할 수 있음은 물론이고, 깔끔하게 정리된 링크들을 보며 자기유능감까지 느낄 수 있습니다!
17 |
18 | urlink(유어링크)는 인터넷에서 리서치할 때, 나중에 다시 보고 싶은 웹사이트를 발견했을 때, 쉽게 보관하고 정리하고 사용할 수 있도록 돕는 리서치 생산성 향상 서비스입니다.
19 |
20 | (유어링크는 PC 환경, 크롬 브라우저에서만 사용할 수 있는 '크롬 확장 프로그램'입니다.)
21 |
22 | ### 🔍 리서치에만 집중하세요. 발견한 정보는 유어링크가 정리해드릴게요.
23 |
24 | 정보를 검색하는 것 만으로도 충분히 바쁜데, 정리하는데 시간을 많이 쓸 필요는 없죠! 유어링크를 이용하면 리서치하다 발견한 유용한 정보를 쉽고 빠르게 보관, 정리할 수 있고 나중에 언제든지 꺼내볼 수 있어요.
25 |
26 | 대학생, 직장인, 마케터, 개발자, 디자이너 등 인터넷에서 자료를 찾고, 보관하고 정리하는데 어려움을 느끼시는 분이라면 누구나 유용하게 사용할 수 있어요!
27 |
28 | # 🎨 Features
29 |
30 | ### 1. 담고 싶은 웹사이트를 클릭 한 번으로 정리!
31 |
32 | - #### **유어링크 버튼을 활용해 링크 저장하기**
33 |
34 | 주소 표시줄 우측에 있는 **유어링크 버튼을 클릭**하면, 그 자리에서 내가 원하는 카테고리에 웹사이트 를 담을 수 있어요. 저장한 링크는 **[유어링크 열기]** 버튼을 눌러 확인할 수 있습니다.
35 |
36 | 
37 |
38 |
39 |
40 | - #### **방문기록을 드래그하여 링크 저장하기**
41 |
42 | 이전에 방문한 웹사이트를 쉽게 보관할 수 있어요. 우측 타이머 아이콘을 눌러 방문기록 리스트를 불러온 다음 원하는 링크를 왼쪽으로 드래그앤드랍하세요. 끌어다 놓기만 하면 내가 원하는 카테고리에 쉽게 링크를 보관할 수 있어요!
43 |
44 | 
45 |
46 | ### 2. 자주 보고 싶은 정보는 카드에 있는 페이보릿 버튼을 클릭하여 보관하세요.
47 |
48 | 카테고리 페이지 최상단에 카드가 배치 되어 내가 원하는 정보에 쉽게 접근할 수 있어요.
49 |
50 | 
51 |
52 | ### 3. 까먹어도 괜찮아요! 알람이 알려줄 거에요.
53 |
54 | 보고싶은 정보를 원하는 시간에 받아보세요. 알람 기능을 이용해 원하는 날짜와 시간을 입력하면, 그 시간에 내가 읽고싶은 정보를 보내드려요. 알람을 이용해 잊혀지던 정보를 효율적으로 사용하세요.
55 |
56 |
57 |
59 |
60 |
61 |
62 |
63 |
64 | # 🧑💻 Get start
65 |
66 | `development` 환경에서는 `chrome api` 기능을 사용 할 수 없어 `build`를 하고 크롬 익스텐션 개발 모드로 확인 가능합니다.
67 |
68 | ### development
69 |
70 | ```
71 | npm i
72 | npm run start
73 | ```
74 |
75 | ### production [build]
76 |
77 | ```
78 | npm i
79 | npm run build
80 | ```
81 |
82 | ### chrome development
83 |
84 | ```
85 | 1. 크롬 실행
86 | 2. 확장 프로그램
87 | 3. 압축해지된 확장 프로그램을 로드합니다[클릭]
88 | 4. build 된 폴더 선택
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "@/*": ["./*"],
6 | "@background/*": ["background/*"],
7 | "@main/*": ["main/*"],
8 | "@popup/*": ["popup/*"],
9 | "@assets/*": ["assets/*"],
10 | "@components/*": ["components/*"],
11 | "@hooks/*": ["hooks/*"],
12 | "@modules/*": ["modules/*"],
13 | "@utils/*": ["utils/*"]
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "urlink-chrome-extension",
3 | "version": "1.3.1",
4 | "private": true,
5 | "dependencies": {
6 | "@date-io/moment": "^1.3.13",
7 | "@emotion/react": "^11.8.2",
8 | "@emotion/styled": "^11.8.1",
9 | "@hookform/resolvers": "^1.2.0",
10 | "@mui/icons-material": "^5.5.1",
11 | "@mui/lab": "^5.0.0-alpha.73",
12 | "@mui/material": "^5.5.1",
13 | "@mui/styles": "^5.5.1",
14 | "@reduxjs/toolkit": "^1.5.0",
15 | "@testing-library/jest-dom": "^4.2.4",
16 | "@testing-library/react": "^9.3.2",
17 | "@testing-library/user-event": "^7.1.2",
18 | "axios": "^0.21.1",
19 | "clsx": "^1.1.0",
20 | "history": "^4.10.1",
21 | "immer": "^9.0.12",
22 | "moment": "^2.26.0",
23 | "node-sass": "^6.0.0",
24 | "re-resizable": "^6.9.9",
25 | "react": "^17.0.2",
26 | "react-chrome-extension-router": "^1.1.0",
27 | "react-dom": "^17.0.2",
28 | "react-ga": "^3.3.0",
29 | "react-hook-form": "^6.13.1",
30 | "react-id-swiper": "^4.0.0",
31 | "react-redux": "^7.2.2",
32 | "react-router-dom": "^5.2.0",
33 | "react-scripts": "^4.0.2",
34 | "redux": "^4.0.5",
35 | "redux-devtools-extension": "^2.13.8",
36 | "redux-saga": "^1.1.3",
37 | "redux-thunk": "^2.3.0",
38 | "swiper": "^6.5.1",
39 | "yup": "^0.32.8"
40 | },
41 | "scripts": {
42 | "start": "craco start",
43 | "build": "craco build",
44 | "test": "craco test",
45 | "lint": "eslint --cache . --ext .js,.jsx --fix",
46 | "eject": "react-scripts eject"
47 | },
48 | "browserslist": {
49 | "production": [
50 | ">0.2%",
51 | "not dead",
52 | "not op_mini all"
53 | ],
54 | "development": [
55 | "last 1 chrome version",
56 | "last 1 firefox version",
57 | "last 1 safari version"
58 | ]
59 | },
60 | "devDependencies": {
61 | "@craco/craco": "^6.4.3",
62 | "craco-alias": "^2.1.1",
63 | "craco-esbuild": "^0.5.0",
64 | "eslint-config-prettier": "^7.2.0",
65 | "eslint-import-resolver-alias": "^1.1.2",
66 | "eslint-plugin-import": "^2.22.1",
67 | "eslint-plugin-prettier": "^3.3.1",
68 | "prettier": "^2.2.1",
69 | "progress-bar-webpack-plugin": "^2.1.0",
70 | "react-error-overlay": "^6.0.9",
71 | "sass": "^1.50.1",
72 | "webpack-bundle-analyzer": "^4.4.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/public/analytics.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable no-unused-expressions */
3 |
4 | ;(function (i, s, o, g, r, a, m) {
5 | i['GoogleAnalyticsObject'] = r
6 | ;(i[r] =
7 | i[r] ||
8 | function () {
9 | ;(i[r].q = i[r].q || []).push(arguments)
10 | }),
11 | (i[r].l = 1 * new Date())
12 | ;(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0])
13 | a.async = 1
14 | a.src = g
15 | m.parentNode.insertBefore(a, m)
16 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga')
17 |
18 | ga('create', 'UA-207149982-2', 'auto')
19 | ga('set', 'checkProtocolTask', null)
20 | ga('send', 'pageview')
21 |
--------------------------------------------------------------------------------
/public/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UrLink-background
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/logo/logo128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/public/images/logo/logo128.png
--------------------------------------------------------------------------------
/public/images/logo/logo16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/public/images/logo/logo16.png
--------------------------------------------------------------------------------
/public/images/logo/logo24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/public/images/logo/logo24.png
--------------------------------------------------------------------------------
/public/images/logo/logo32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/public/images/logo/logo32.png
--------------------------------------------------------------------------------
/public/images/logo/logo64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/public/images/logo/logo64.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | UrLink
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "urLink",
3 | "version": "1.3.1",
4 | "manifest_version": 2,
5 | "description": "북마크보다 손쉽게 웹사이트를 보관하고 정리하세요.",
6 | "permissions": ["", "identity", "history", "notifications", "bookmarks"],
7 | "icons": {
8 | "16": "images/logo/logo16.png",
9 | "24": "images/logo/logo24.png",
10 | "32": "images/logo/logo32.png",
11 | "64": "images/logo/logo64.png",
12 | "128": "images/logo/logo128.png"
13 | },
14 | "web_accessible_resources": ["*.ttf", "*.woff2"],
15 | "browser_action": {
16 | "default_icon": "images/logo/logo16.png",
17 | "default_popup": "popup.html"
18 | },
19 | "background": {
20 | "page": "background.html",
21 | "persistent": true
22 | },
23 | "oauth2": {
24 | "client_id": "683415540436-aimi7aksc043mre0pdj2nejtr08fkchs.apps.googleusercontent.com",
25 | "scopes": ["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"]
26 | },
27 | "content_security_policy": "script-src 'self' https://www.google-analytics.com https://apis.google.com 'sha256-34M3HxxCd7web+UMuTvWUlRtVdx6QrTGItv3k4d183Y='; object-src 'self'"
28 | }
29 |
--------------------------------------------------------------------------------
/public/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | UrLink-popup
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 링크 저장하기
17 |
18 |
19 |
22 |
23 |
24 |

25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/assets/css/default.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --maincolor: #2083ff;
3 | --lightblue: #77caff;
4 | --powder-blue: #e3f0ff;
5 | --pale-sky-blue: #c2e8ff;
6 | --maintext: #212529;
7 | --battleship-grey: #737b84;
8 | --blue-grey: #868e96;
9 | --very-light-blue: #e9ecef;
10 | --pale-grey: #c4c4c4;
11 | --light-grey: #f4f4f4;
12 | --salmon: #ff6b6b;
13 | --green-blue: #00b381;
14 | }
15 |
16 | @import url(http://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-kr.css);
17 | @import url(http://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-jp.css);
18 | @import url(http://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css);
--------------------------------------------------------------------------------
/src/assets/images/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/add.png
--------------------------------------------------------------------------------
/src/assets/images/alarm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/alarm.png
--------------------------------------------------------------------------------
/src/assets/images/background1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/background1.jpg
--------------------------------------------------------------------------------
/src/assets/images/background2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/background2.jpg
--------------------------------------------------------------------------------
/src/assets/images/card-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/card-image.jpg
--------------------------------------------------------------------------------
/src/assets/images/check-false.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/check-false.png
--------------------------------------------------------------------------------
/src/assets/images/check-true.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/check-true.png
--------------------------------------------------------------------------------
/src/assets/images/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/group-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/group-10.png
--------------------------------------------------------------------------------
/src/assets/images/group-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/group-19.png
--------------------------------------------------------------------------------
/src/assets/images/group-27.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/images/history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/history.png
--------------------------------------------------------------------------------
/src/assets/images/info-person.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/info-person.jpg
--------------------------------------------------------------------------------
/src/assets/images/link-copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/link-copy.png
--------------------------------------------------------------------------------
/src/assets/images/link-copy.svg:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/src/assets/images/link-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/link-icon.png
--------------------------------------------------------------------------------
/src/assets/images/link-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
--------------------------------------------------------------------------------
/src/assets/images/link.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/images/linkListEmptyIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/linkListEmptyIcon.png
--------------------------------------------------------------------------------
/src/assets/images/link_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
--------------------------------------------------------------------------------
/src/assets/images/logo-google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo-google.png
--------------------------------------------------------------------------------
/src/assets/images/logo-urlink-box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo-urlink-box.png
--------------------------------------------------------------------------------
/src/assets/images/logo-urlink-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo-urlink-full.png
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/assets/images/logo/group-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/logo/logo128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/logo128.png
--------------------------------------------------------------------------------
/src/assets/images/logo/logo16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/logo16.png
--------------------------------------------------------------------------------
/src/assets/images/logo/logo24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/logo24.png
--------------------------------------------------------------------------------
/src/assets/images/logo/logo32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/logo32.png
--------------------------------------------------------------------------------
/src/assets/images/logo/logo64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/logo64.png
--------------------------------------------------------------------------------
/src/assets/images/logo/profileImg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/profileImg.png
--------------------------------------------------------------------------------
/src/assets/images/logo/profileImg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/profileImg@2x.png
--------------------------------------------------------------------------------
/src/assets/images/logo/profileImg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/logo/profileImg@3x.png
--------------------------------------------------------------------------------
/src/assets/images/main1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main1.png
--------------------------------------------------------------------------------
/src/assets/images/main1_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main1_back.png
--------------------------------------------------------------------------------
/src/assets/images/main2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main2.png
--------------------------------------------------------------------------------
/src/assets/images/main2_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main2_back.png
--------------------------------------------------------------------------------
/src/assets/images/main3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main3.png
--------------------------------------------------------------------------------
/src/assets/images/main3_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main3_back.png
--------------------------------------------------------------------------------
/src/assets/images/main4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main4.png
--------------------------------------------------------------------------------
/src/assets/images/main5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/main5.png
--------------------------------------------------------------------------------
/src/assets/images/mainBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/mainBackground.png
--------------------------------------------------------------------------------
/src/assets/images/mainBackground2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/mainBackground2.png
--------------------------------------------------------------------------------
/src/assets/images/mainLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/mainLogo.png
--------------------------------------------------------------------------------
/src/assets/images/more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/more.png
--------------------------------------------------------------------------------
/src/assets/images/more_3dot.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/move.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/move.png
--------------------------------------------------------------------------------
/src/assets/images/new-tab.svg:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/src/assets/images/open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/open.png
--------------------------------------------------------------------------------
/src/assets/images/person.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/person.png
--------------------------------------------------------------------------------
/src/assets/images/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/plus.png
--------------------------------------------------------------------------------
/src/assets/images/plus_noborder.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/urLink-DEV/urLink-frontend/14b63146fd68ecb8d49f38e7ece88ed78b1f676c/src/assets/images/search.png
--------------------------------------------------------------------------------
/src/assets/images/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/star_fill.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/union.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/white.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/scss/font.scss:
--------------------------------------------------------------------------------
1 | @import url(http://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-kr.css);
2 | @import url(http://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-jp.css);
3 | @import url(http://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css);
--------------------------------------------------------------------------------
/src/assets/scss/swiper.scss:
--------------------------------------------------------------------------------
1 | .swiper-container-c {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 700px !important;
6 | }
7 | .prev-btn {
8 | background: transparent;
9 | border: none;
10 | color: white;
11 | font-weight: bold;
12 | right: 10px;
13 | left: auto;
14 | top: auto;
15 | }
16 | .next-btn {
17 | background: transparent;
18 | border: none;
19 | color: white;
20 | font-weight: bold;
21 | right: auto;
22 | left: 10px;
23 | top: auto;
24 | }
25 | .swiper-container-horizontal > .swiper-pagination-bullets {
26 | bottom: 5px;
27 | }
28 | .swiper-pagination-bullet-active {
29 | background-color: white;
30 | }
--------------------------------------------------------------------------------
/src/background/index.js:
--------------------------------------------------------------------------------
1 | import { requestAlarmReadNotice } from '@/modules/alarmNotice'
2 | import { createTab } from '@/utils/chromeApis/tab'
3 | import { getAccessToken } from '@/utils/http/auth'
4 | import { onMessage, REMOVE_TOKEN, UPDATE_TOKEN } from '@utils/chromeApis/onMessage'
5 | import { alarmSocket } from '@utils/http/ws'
6 |
7 | function connectionWS() {
8 | try {
9 | if (alarmSocket.ws?.OPEN) alarmSocket.onClose()
10 | if (!getAccessToken()) return
11 | alarmSocket.onConnection().setOnmessage((event) => {
12 | const { message, status } = JSON.parse(event.data)
13 | if (status === 'alarm') {
14 | const notification = new Notification('urLink 알람이 도착했습니다.', {
15 | icon: message.url_favicon_path,
16 | body: message.url_title,
17 | })
18 | notification.onclick = () => {
19 | requestAlarmReadNotice({ alarm_id: message.id })
20 | createTab(message.url_path)
21 | }
22 | }
23 | })
24 | } catch (error) {
25 | console.dir('error', error)
26 | }
27 | }
28 |
29 | function connectionClose() {
30 | if (alarmSocket.ws?.OPEN) alarmSocket.onClose()
31 | }
32 |
33 | try {
34 | connectionWS()
35 | onMessage((request) => {
36 | switch (request.message) {
37 | case UPDATE_TOKEN:
38 | connectionWS()
39 | break
40 | case REMOVE_TOKEN:
41 | connectionClose()
42 | break
43 | default:
44 | break
45 | }
46 | })
47 | } catch (error) {
48 | console.dir('error', error)
49 | }
50 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | function useDebounce(value, delay) {
4 | // 디바운스 할 값을 관리하기위한 상태값과 setter함수
5 | const [debouncedValue, setDebouncedValue] = useState(value)
6 |
7 | useEffect(() => {
8 | // 딜레이 이후 값을 업데이트한다.
9 | const timer = setTimeout(() => {
10 | setDebouncedValue(value)
11 | }, delay)
12 |
13 | // 딜레이 기간중에 value혹은 delay값이 업데이트 되었다면 이(cleanup)함수를 실행한다.
14 | return () => {
15 | clearTimeout(timer)
16 | }
17 | }, [value, delay]) // delay값이나 value값이 업데이트 되었다면 다시 호출한다.
18 |
19 | return debouncedValue
20 | }
21 |
22 | export default useDebounce
23 |
--------------------------------------------------------------------------------
/src/hooks/useEventListener.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | export default function useEventListener(eventName, handler, element = window) {
4 | const savedHandler = useRef()
5 |
6 | useEffect(() => {
7 | savedHandler.current = handler
8 | }, [handler])
9 |
10 | useEffect(() => {
11 | const isSupported = element && element.addEventListener
12 | if (!isSupported) return
13 |
14 | const eventListener = (event) => savedHandler.current(event)
15 |
16 | element.addEventListener(eventName, eventListener)
17 |
18 | return () => {
19 | element.removeEventListener(eventName, eventListener)
20 | }
21 | }, [eventName, element])
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useOutsideAlerter.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | function useOutsideAlerter(ref, bool, callback) {
4 | useEffect(() => {
5 | function handleClickOutside(event) {
6 | if (ref.current && !ref.current?.contains(event.target)) {
7 | return callback()
8 | }
9 | }
10 |
11 | if (bool) document.addEventListener('mousedown', handleClickOutside)
12 | return () => document.removeEventListener('mousedown', handleClickOutside)
13 | }, [ref, bool, callback])
14 | }
15 |
16 | export default useOutsideAlerter
17 |
--------------------------------------------------------------------------------
/src/main/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import moment from 'moment'
4 | import { Router } from 'react-chrome-extension-router'
5 | import { useLocation } from 'react-router-dom'
6 |
7 | import Snackbar from '@main/components/Toast'
8 | import Home from '@main/pages/Home'
9 | import { useToast } from '@modules/ui'
10 | import { GAPageview, initGA } from '@utils/ga'
11 | import { getAccessToken } from '@utils/http/auth'
12 |
13 | import GetStartPage from './pages/Start'
14 | import 'moment/locale/ko'
15 |
16 | moment.locale('ko')
17 |
18 | function App() {
19 | const { pathname } = useLocation()
20 |
21 | useEffect(() => {
22 | initGA()
23 | }, [])
24 |
25 | useEffect(() => {
26 | GAPageview(pathname)
27 | }, [pathname])
28 |
29 | return (
30 | <>
31 | {getAccessToken() ? : }
32 |
33 | >
34 | )
35 | }
36 |
37 | function ToastContainer() {
38 | const { open, type, message, close } = useToast()
39 | return
40 | }
41 |
42 | export default App
43 |
--------------------------------------------------------------------------------
/src/main/components/ExtendedFab/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { StyledFab } from './style'
4 |
5 | function ExtendedFab(props) {
6 | const isExtended = React.Children.toArray(props.children).find((child) => typeof child === 'string')
7 |
8 | return
9 | }
10 |
11 | export default ExtendedFab
12 |
--------------------------------------------------------------------------------
/src/main/components/ExtendedFab/style.js:
--------------------------------------------------------------------------------
1 | import { Fab } from '@mui/material'
2 | import { withStyles } from '@mui/styles'
3 |
4 | export const StyledFab = withStyles((theme) => ({
5 | root: {
6 | position: 'absolute',
7 | bottom: 7,
8 | right: 5,
9 | width: 40,
10 | height: 40,
11 | margin: 0,
12 | borderRadius: '50%',
13 | },
14 | }))(Fab)
15 |
--------------------------------------------------------------------------------
/src/main/components/ScrollUpButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { ArrowUpward as ArrowUpwardIcon } from '@mui/icons-material'
4 | import clsx from 'clsx'
5 |
6 | import ExtendedFab from '@main/components/ExtendedFab'
7 | import { GAEvent } from '@utils/ga'
8 |
9 | import useStyles from './style'
10 |
11 | function ScrollUpButton({ targetRef, className, open }) {
12 | const classes = useStyles()
13 |
14 | const handleScrollUp = () => {
15 | GAEvent('스크롤 업 버튼', '스크롤 업 버튼 클릭')
16 | scrollTo(targetRef.current, 0, 500)
17 | }
18 |
19 | return (
20 |
27 |
28 |
29 | )
30 | }
31 |
32 | function scrollTo(element, to = 0, duration = 500, scrollToDone = null) {
33 | const start = element.scrollTop
34 | const change = to - start
35 | const increment = 20
36 | let currentTime = 0
37 |
38 | const easeInOutQuad = (currentTime, start, change, duration) => {
39 | currentTime /= duration / 2
40 | if (currentTime < 1) return (change / 2) * currentTime * currentTime + start
41 | currentTime--
42 | return (-change / 2) * (currentTime * (currentTime - 2) - 1) + start
43 | }
44 |
45 | const animateScroll = () => {
46 | currentTime += increment
47 | const val = easeInOutQuad(currentTime, start, change, duration)
48 | element.scrollTop = val
49 | if (currentTime < duration) setTimeout(animateScroll, increment)
50 | else if (scrollToDone) scrollToDone()
51 | }
52 |
53 | animateScroll()
54 | }
55 |
56 | export default ScrollUpButton
57 |
--------------------------------------------------------------------------------
/src/main/components/ScrollUpButton/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | root: {
5 | position: 'fixed',
6 | bottom: 14,
7 | right: 58,
8 |
9 | opacity: 0,
10 |
11 | transform: 'translateY(100px)',
12 | transition: 'all .5s ease',
13 | },
14 | showBtn: {
15 | opacity: 1,
16 |
17 | transform: 'translateY(0)',
18 | },
19 | }))
20 |
21 | export default useStyles
22 |
--------------------------------------------------------------------------------
/src/main/components/SearchBar/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | export const useStyles = makeStyles((theme) => ({
4 | searchBar: {
5 | display: 'flex',
6 | gap: 3,
7 | flexDirection: 'row',
8 | justifyContent: 'flex-start',
9 | alignItems: 'center',
10 |
11 | width: 380,
12 | height: 38,
13 | padding: '0px 40px 0px 16px',
14 | backgroundColor: '#F3F3F3',
15 | borderRadius: 8,
16 | },
17 | searchBarDisabled: {
18 | opacity: 0.4,
19 | },
20 | searchSelect: {
21 | paddingRight: '12px',
22 | fontSize: 14,
23 | },
24 | searchSelectPaper: {
25 | padding: 0,
26 | minWidth: 199,
27 | borderRadius: 8,
28 |
29 | '& > .MuiMenu-list': {
30 | padding: 0,
31 | display: 'flex',
32 | flexDirection: 'column',
33 | gap: 10,
34 | },
35 | },
36 | searchSelectItem: {
37 | display: 'flex',
38 | flexDirection: 'column',
39 | alignItems: 'flex-start',
40 | rowGap: 4,
41 | },
42 | searchInputBase: {
43 | width: '100%',
44 | },
45 | searchInput: {
46 | display: 'flex',
47 | alignItems: 'center',
48 |
49 | fontWeight: 400,
50 | fontSize: 14,
51 | lineHeight: 14,
52 |
53 | color: '#777777',
54 | },
55 | divider: {
56 | height: 31,
57 | },
58 | pickerBtn: {
59 | color: theme.palette.primary.main,
60 | fontWeight: 400,
61 | fontSize: 14,
62 |
63 | width: '100%',
64 | borderBottom: '1px solid',
65 | borderRadius: 0,
66 |
67 | '&:hover': {
68 | color: theme.palette.primary.main,
69 | },
70 | },
71 | datePicker: {
72 | width: 320,
73 | },
74 | searchIcon: {
75 | margin: '11px 10px',
76 | width: 16,
77 | height: 16,
78 | color: '#777777',
79 | },
80 | searchInputCancel: {
81 | position: 'relative',
82 | top: 0,
83 | right: -31,
84 |
85 | width: 24,
86 | height: 24,
87 | color: '#6b6b6f',
88 | },
89 | }))
90 |
91 | export default useStyles
92 |
--------------------------------------------------------------------------------
/src/main/components/SearchButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react'
2 |
3 | import Popover from '@mui/material/Popover'
4 | import ToggleButton from '@mui/material/ToggleButton'
5 |
6 | import SearchImg from '@assets/images/search.png'
7 |
8 | import useStyles, { StyledToggleButtonGroup } from './style'
9 |
10 | function SearchButton({ buttonProps, inputProps, searchFilterList, onSelectButton, selectedName }) {
11 | const classes = useStyles()
12 |
13 | const searchButtonRef = useRef(null)
14 | const [openSearchBox, setOpenSearchBox] = useState(false)
15 |
16 | return (
17 | <>
18 |
28 | setOpenSearchBox((open) => !open)}
32 | anchorEl={searchButtonRef.current}
33 | anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
34 | transformOrigin={{ vertical: 'top', horizontal: 'left' }}
35 | >
36 |
37 |
38 |

39 |
Search
40 |
41 |
42 |
43 |
44 | {searchFilterList && (
45 |
46 |
47 | {searchFilterList.map(({ search, name }) => (
48 |
55 | {name}
56 |
57 | ))}
58 |
59 |
60 | )}
61 |
62 |
63 | >
64 | )
65 | }
66 |
67 | export default SearchButton
68 |
--------------------------------------------------------------------------------
/src/main/components/SearchButton/style.js:
--------------------------------------------------------------------------------
1 | import ToggleButtonGroup from '@mui/lab/ToggleButtonGroup'
2 | import { makeStyles, withStyles } from '@mui/styles'
3 |
4 | export const StyledToggleButtonGroup = withStyles((theme) => ({
5 | grouped: {
6 | margin: theme.spacing(0.5),
7 | border: 'none',
8 | '&:not(:first-child)': {
9 | borderRadius: theme.shape.borderRadius,
10 | },
11 | '&:first-child': {
12 | borderRadius: theme.shape.borderRadius,
13 | },
14 | },
15 | }))(ToggleButtonGroup)
16 |
17 | const useStyles = makeStyles((theme) => ({
18 | flex: {
19 | display: 'flex',
20 | flexDirection: 'column',
21 | },
22 | searchBtn: {
23 | flexShrink: 0,
24 | height: 30,
25 | padding: '5px 10px',
26 | borderRadius: 4,
27 | backgroundColor: '#ffffff',
28 | boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.12)',
29 | '&:hover': {
30 | boxShadow: '0 2px 8px 0 rgba(0, 0, 0, 0.15), 0 5px 12px 0 rgba(0, 0, 0, 0.12)',
31 | },
32 | },
33 | searchIcon: {
34 | marginRight: 5,
35 | },
36 | searchBtnText: {
37 | width: 34,
38 | height: 15,
39 | color: '#868e96',
40 | fontSize: '12pt',
41 | fontWeight: '300',
42 | textAlign: 'center',
43 | },
44 | inputBox: {
45 | width: 220,
46 | padding: '5px 10px',
47 | borderRadius: 4,
48 | backgroundColor: '#ffffff',
49 | boxShadow: '0 2px 8px 0 rgba(0, 0, 0, 0.15), 0 5px 12px 0 rgba(0, 0, 0, 0.12)',
50 | },
51 | textfield: {
52 | width: '100%',
53 | height: 28,
54 | padding: '3px 7px',
55 | borderRadius: '4px',
56 | border: 'solid 1px #e9ecef',
57 | backgroundColor: '#f1f3f5',
58 | outline: 'none',
59 | '&:focus': { border: '1px solid #2083ff' },
60 | },
61 | marginBottom10: {
62 | marginBottom: 10,
63 | },
64 | popoverBtn: {
65 | height: 30,
66 | marginTop: 3,
67 | '&.Mui-selected': {
68 | color: 'white',
69 | backgroundColor: '#2083ff',
70 | fontWeight: 400,
71 | },
72 | },
73 | }))
74 |
75 | export default useStyles
76 |
--------------------------------------------------------------------------------
/src/main/components/Toast/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import CloseIcon from '@mui/icons-material/Close'
4 | import { Snackbar, IconButton, Slide, Grow, Fade } from '@mui/material'
5 | import Alert from '@mui/material/Alert'
6 |
7 | function SnackbarTransition({ transition, direction, ...rest }) {
8 | return (
9 |
14 | )
15 | }
16 |
17 | function Toast({ open, type, message, close, ...props }) {
18 | return (
19 |
32 |
33 |
34 | }
35 | {...props}
36 | >
37 | {type && (
38 |
39 | {message}
40 |
41 | )}
42 |
43 | )
44 | }
45 |
46 | export default Toast
47 |
--------------------------------------------------------------------------------
/src/main/components/ValidationMessage/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import clsx from 'clsx'
4 |
5 | import checkFalseImg from '@assets/images/check-false.png'
6 | import checkTrueImg from '@assets/images/check-true.png'
7 |
8 | import useStyles from './style.js'
9 |
10 | function ValidationMessage({ msg, check }) {
11 | const classes = useStyles()
12 |
13 | return (
14 |
20 |

21 | {msg}
22 |
23 | )
24 | }
25 |
26 | export default ValidationMessage
27 |
--------------------------------------------------------------------------------
/src/main/components/ValidationMessage/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | checkValidation: {
5 | position: 'relative',
6 | width: '100%',
7 | margin: '2px 0 10px 0',
8 | fontSize: '12px',
9 | fontWeight: 'normal',
10 | fontStretch: 'normal',
11 | fontStyle: 'normal',
12 | lineHeight: 'normal',
13 | letterSpacing: '-0.55px',
14 | opacity: 0,
15 | },
16 | checkTrue: {
17 | color: '#00b381',
18 | opacity: 1,
19 | },
20 | checkFalse: {
21 | color: '#ff6b6b',
22 | opacity: 1,
23 | },
24 | }))
25 |
26 | export default useStyles
27 |
--------------------------------------------------------------------------------
/src/main/components/modals/AlertModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Button } from '@mui/material'
4 |
5 | import alertIcon from '@assets/images/logo/group-2.svg'
6 | import {
7 | StyledDialog,
8 | StyledDialogContent,
9 | StyledDialogContentText,
10 | StyledDialogActions,
11 | } from '@main/components/modals/style'
12 |
13 | import useStyles from './style'
14 |
15 | function AlertModal({
16 | openBool,
17 | handleClose,
18 | contentText,
19 | btnYesText = '확인',
20 | handleNoText = '취소',
21 | handleYesClick,
22 | handleNoClick,
23 | children,
24 | }) {
25 | const classes = useStyles()
26 |
27 | return (
28 |
29 |
30 |
31 |
32 | {contentText || children}
33 |
34 |
35 |
36 | {handleYesClick && (
37 |
40 | )}
41 | {(handleNoClick || handleClose) && (
42 |
45 | )}
46 |
47 |
48 | )
49 | }
50 |
51 | export default AlertModal
52 |
--------------------------------------------------------------------------------
/src/main/components/modals/AlertModal/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | termsModal: {
5 | width: '600px',
6 | height: '500px',
7 | },
8 | alertModal: {
9 | display: 'flex',
10 | justifyContent: 'center',
11 | alignItems: 'center',
12 | fontWeight: 'bold',
13 | },
14 | alertIcon: {
15 | position: 'absolute',
16 | top: 0,
17 | left: 0,
18 | },
19 | alertModalBtn: {
20 | width: '100%',
21 | },
22 | }))
23 |
24 | export default useStyles
25 |
--------------------------------------------------------------------------------
/src/main/components/modals/TermsModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { DialogTitle, Button } from '@mui/material'
4 |
5 | import {
6 | StyledDialog,
7 | StyledDialogContent,
8 | StyledDialogContentText,
9 | StyledDialogActions,
10 | } from '@main/components/modals/style'
11 |
12 | import textInfo from './textInfo'
13 |
14 | function TermsModal({ open, onClose, onYesClick, onYesText = '동의함', onNoClick, onNoText = '동의안함' }) {
15 | return (
16 |
23 |
24 | 이용 약관 동의
25 |
26 |
27 |
28 |
29 |
30 | {onYesClick && (
31 |
34 | )}
35 | {onNoClick && (
36 |
39 | )}
40 |
41 |
42 | )
43 | }
44 |
45 | export default TermsModal
46 |
--------------------------------------------------------------------------------
/src/main/components/modals/index.jsx:
--------------------------------------------------------------------------------
1 | export { default as TermsModal } from './TermsModal'
2 | export { default as AlertModal } from './AlertModal'
3 |
--------------------------------------------------------------------------------
/src/main/components/modals/style.js:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogActions, DialogContent, DialogContentText } from '@mui/material'
2 | import { withStyles } from '@mui/styles'
3 |
4 | export const StyledDialog = withStyles((theme) => ({
5 | paperWidthSm: {
6 | width: 340,
7 | height: 216,
8 | },
9 | }))(Dialog)
10 |
11 | export const StyledDialogContent = withStyles((theme) => ({
12 | root: {
13 | '&:first-child': {
14 | padding: '60px 30px',
15 | overflowY: 'hidden',
16 | },
17 | },
18 | }))(DialogContent)
19 |
20 | export const StyledDialogContentText = withStyles((theme) => ({
21 | root: {
22 | marginBottom: 12,
23 |
24 | color: theme.palette.text.primary,
25 |
26 | fontSize: 14,
27 | },
28 | }))(DialogContentText)
29 |
30 | export const StyledDialogActions = withStyles((theme) => ({
31 | root: {
32 | display: 'flex',
33 | flex: '0 0 auto',
34 | padding: 8,
35 |
36 | borderTop: '1px solid rgba(0, 0, 0, 0.15)',
37 |
38 | alignItems: 'center',
39 | justifyContent: 'space-around',
40 | },
41 | }))(DialogActions)
42 |
--------------------------------------------------------------------------------
/src/main/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import AdapterMoment from '@mui/lab/AdapterMoment'
4 | import LocalizationProvider from '@mui/lab/LocalizationProvider'
5 | import CssBaseline from '@mui/material/CssBaseline'
6 | import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'
7 | import { createBrowserHistory } from 'history'
8 | import ReactDOM from 'react-dom'
9 | import { Provider } from 'react-redux'
10 | import { Router } from 'react-router-dom'
11 | import { createStore, applyMiddleware } from 'redux'
12 | import { composeWithDevTools } from 'redux-devtools-extension'
13 | import createSagaMiddleware from 'redux-saga'
14 | import thunkMiddleware from 'redux-thunk'
15 |
16 | import { rootReducer, rootSaga } from '@modules'
17 | import '@assets/scss/font.scss'
18 |
19 | import App from './App'
20 | import theme from './theme'
21 |
22 | const browserHistory = createBrowserHistory()
23 | const sagaMiddleware = createSagaMiddleware()
24 |
25 | const store = createStore(
26 | rootReducer,
27 | composeWithDevTools({ trace: true })(applyMiddleware(sagaMiddleware, thunkMiddleware))
28 | )
29 |
30 | sagaMiddleware.run(rootSaga)
31 |
32 | ReactDOM.render(
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ,
45 | document.getElementById('root')
46 | )
47 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/AlarmList/style.js:
--------------------------------------------------------------------------------
1 | import { Avatar } from '@mui/material'
2 | import { grey } from '@mui/material/colors'
3 | import { makeStyles, withStyles } from '@mui/styles'
4 |
5 | const useStyles = makeStyles((theme) => ({
6 | root: {
7 | width: 360,
8 | minHeight: 305,
9 | maxHeight: 524,
10 | overflow: 'scroll',
11 | backgroundColor: theme.palette.background.theme,
12 | },
13 | title: {
14 | width: 100,
15 | height: 29,
16 | fontSize: 20,
17 | fontWeight: 'bold',
18 | fontStretch: 'normal',
19 | fontStyle: 'normal',
20 | lineHeight: 'normal',
21 | letterSpacing: 'normal',
22 | color: '#000000',
23 | },
24 | listItem: {
25 | paddingLeft: 10,
26 | backgroundColor: theme.palette.background.paper,
27 | },
28 | avatar: {
29 | marginRight: 10,
30 | },
31 | icon: {
32 | width: 56,
33 | height: 56,
34 | },
35 | cover: {
36 | height: 358,
37 | backgroundSize: 'auto',
38 | },
39 | text: {
40 | '& .MuiListItemText-primary': {
41 | fontSize: 14,
42 | },
43 | '& .MuiListItemText-secondary': {
44 | fontSize: 11,
45 | },
46 | },
47 | noticeText: {
48 | '& .MuiListItemText-secondary': {
49 | fontWeight: 'bold',
50 | color: '#2083ff',
51 | },
52 | },
53 | readText: {
54 | '& .MuiListItemText-primary': {
55 | color: '#b9b9b9 !important',
56 | },
57 | '& .MuiListItemText-secondary': {
58 | color: '#b9b9b9 !important',
59 | },
60 | },
61 | bgGrey: {
62 | backgroundColor: `${grey[200]} !important`,
63 | },
64 | }))
65 |
66 | export const SmallAvatar = withStyles((theme) => ({
67 | root: {
68 | width: 28,
69 | height: 28,
70 | border: `2px solid ${theme.palette.background.paper}`,
71 | backgroundColor: '#2083ff',
72 | },
73 | }))(Avatar)
74 |
75 | export default useStyles
76 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/History/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useState } from 'react'
2 |
3 | import LinkIcon from '@mui/icons-material/Link'
4 | import OpenInNewIcon from '@mui/icons-material/OpenInNew'
5 | import { IconButton, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText } from '@mui/material'
6 | import clsx from 'clsx'
7 |
8 | import logoImg from '@assets/images/logo/logo16.png'
9 | import newTabImg from '@assets/images/new-tab.svg'
10 | import { useToast } from '@modules/ui'
11 | import { createTab } from '@utils/chromeApis/tab'
12 | import copyLink from '@utils/copyLink'
13 | import { GAEvent } from '@utils/ga'
14 |
15 | import useStyles from './style'
16 |
17 | function History({ isSelected = false, data = {}, ...props }) {
18 | const classes = useStyles()
19 | const { openToast } = useToast()
20 |
21 | const [faviconLink, setFaviconLink] = useState(`https://www.google.com/s2/favicons?domain=${data.hostName}`)
22 |
23 | const handleNewTab = useCallback(
24 | (e) => {
25 | e.stopPropagation()
26 | GAEvent('방문기록', '링크 새 탭 열기')
27 | createTab(data.url)
28 | },
29 | [data.url]
30 | )
31 |
32 | const handleLinkCopy = useCallback(
33 | (e) => {
34 | e.stopPropagation()
35 | GAEvent('방문기록', '링크 복사 하기')
36 | copyLink(data.url)
37 | openToast({ type: 'success', message: '링크가 복사 되었습니다.' })
38 | },
39 | [data.url, openToast]
40 | )
41 |
42 | return (
43 |
54 |
55 |
setFaviconLink(logoImg)} src={faviconLink} alt={data.title} />
56 |
57 |
60 | {data.title + ` (${data.visitCount})`}
61 | {data.hostName}
62 | >
63 | }
64 | />
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | export default memo(History)
78 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/History/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | listItem: {
5 | margin: '10px 0',
6 | padding: '2px 253px 2px 16px',
7 |
8 | width: 583,
9 | height: 36,
10 |
11 | borderRadius: 4,
12 | backgroundColor: theme.palette.background.paper,
13 |
14 | '& .Mui-focusVisible': {
15 | backgroundColor: 'transparent',
16 | },
17 | '&:hover': {
18 | backgroundColor: '#F2F2F2',
19 | '& > $buttonGroup': {
20 | visibility: 'inherit',
21 | },
22 | },
23 | },
24 | selected: {
25 | backgroundColor: '#E8F1FF',
26 | },
27 | listButton: {
28 | padding: '1px 0 0 0',
29 | width: 555,
30 |
31 | '& .MuiListItemIcon-root': {
32 | minWidth: 14,
33 | marginRight: 17,
34 | },
35 | '& .MuiListItemText-primary': {
36 | display: 'flex',
37 | },
38 | '&:hover': {
39 | backgroundColor: 'transparent',
40 | },
41 | },
42 | favicon: {
43 | width: 14,
44 | height: 14,
45 | },
46 | mainFont: {
47 | display: 'inline-block',
48 | verticalAlign: 'bottom',
49 |
50 | overflow: 'hidden',
51 |
52 | width: 266,
53 | marginRight: 16,
54 |
55 | color: theme.palette.text.secondary,
56 |
57 | fontSize: 14,
58 | fontWeight: 400,
59 | letterSpacing: -0.44,
60 | whiteSpace: 'nowrap',
61 | textOverflow: 'ellipsis',
62 | },
63 | subFont: {
64 | display: 'inline-block',
65 | verticalAlign: 'bottom',
66 |
67 | overflow: 'hidden',
68 |
69 | width: 157,
70 |
71 | color: theme.palette.text.description,
72 |
73 | fontSize: 14,
74 | fontWeight: 400,
75 | letterSpacing: -0.6,
76 | whiteSpace: 'nowrap',
77 | textOverflow: 'ellipsis',
78 | },
79 | buttonGroup: {
80 | visibility: 'hidden',
81 |
82 | display: 'flex',
83 | gap: 7,
84 | },
85 | iconButton: {
86 | padding: 7,
87 |
88 | width: 25,
89 | height: 25,
90 | borderRadius: 4,
91 | backgroundColor: '#FFFFFF',
92 | color: '#666666',
93 | },
94 | openInNewIcon: {
95 | width: 11.11,
96 | },
97 | linkIcon: {
98 | width: 16.67,
99 | },
100 | }))
101 |
102 | export default useStyles
103 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/HistoryDateTitle/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import useStyles from './style'
4 |
5 | function HistoryDateTitle({ data }) {
6 | const classes = useStyles()
7 | const curDate = new Date().toLocaleDateString()
8 | const linkDate = new Date(data.lastVisitTime).toLocaleDateString()
9 |
10 | if (!data.first) return null
11 | return (
12 |
13 | {curDate === linkDate ? '오늘' : linkDate}
14 |
15 |
16 | )
17 | }
18 |
19 | export default HistoryDateTitle
20 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/HistoryDateTitle/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | root: {
5 | width: 547,
6 | },
7 | title: {
8 | minWidth: 42,
9 | maxWidth: 94,
10 | fontWeight: 400,
11 | fontSize: 14,
12 | lineHeight: '36px',
13 |
14 | position: 'absolute',
15 |
16 | paddingRight: 8,
17 |
18 | backgroundColor:
19 | theme.palette.type !== 'dark' ? theme.palette.colorGroup.lightGrey : theme.palette.background.default,
20 | },
21 | line: {
22 | width: 570,
23 | height: 0.5,
24 |
25 | display: 'inline-block',
26 | background: '#C3C3C3',
27 | },
28 | }))
29 |
30 | export default useStyles
31 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/HistoryDragBox/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react'
2 |
3 | import clsx from 'clsx'
4 |
5 | import moveLink from '@assets/images/move.png'
6 |
7 | import useStyles from './style'
8 |
9 | const HistoryDragBox = forwardRef(({ selectedCount }, ref) => {
10 | const classes = useStyles()
11 |
12 | return (
13 |
14 |
15 |

16 |
17 |
링크 {selectedCount}개 이동
18 |
19 | )
20 | })
21 |
22 | HistoryDragBox.displayName = 'HistoryDragBox'
23 |
24 | export default HistoryDragBox
25 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/HistoryDragBox/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | tabMove: {
5 | position: 'absolute',
6 | zIndex: -1,
7 | top: 10,
8 | right: 0,
9 |
10 | display: 'flex',
11 | justifyContent: 'center',
12 | alignItems: 'center',
13 |
14 | width: 110,
15 | height: 35,
16 |
17 | color: theme.palette.common.white,
18 | backgroundColor: theme.palette.primary.main,
19 | borderRadius: 3.2,
20 | boxShadow: '0 11px 22px 0 rgba(0, 0, 0, 0.15), 0 8px 8px 0 rgba(0, 0, 0, 0.12)',
21 |
22 | fontSize: 12,
23 | },
24 | circle: {
25 | display: 'flex',
26 | justifyContent: 'center',
27 | alignItems: 'center',
28 |
29 | width: 23,
30 | height: 23,
31 | marginRight: 7,
32 |
33 | borderRadius: '50%',
34 | boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 2px 0 rgba(0, 0, 0, 0.1)',
35 | },
36 | moveIcon: {
37 | position: 'relative',
38 | left: 1,
39 |
40 | width: 20,
41 | height: 20,
42 | },
43 | }))
44 |
45 | export default useStyles
46 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/DraggableHistoryList/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | root: {
5 | position: 'relative',
6 |
7 | width: 656,
8 | height: 'calc(100vh - 72px);',
9 |
10 | backgroundColor:
11 | theme.palette.type !== 'dark' ? theme.palette.colorGroup.lightGrey : theme.palette.background.default,
12 | boxShadow: '0 2px 4px 0 rgba(0, 0, 0, 0.1)',
13 | },
14 | header: {
15 | display: 'flex',
16 | justifyContent: 'space-between',
17 |
18 | padding: '17px 36px',
19 | },
20 | mainText: {
21 | marginRight: 10,
22 | marginTop: 2,
23 | height: 36,
24 |
25 | backgroundColor: 'transparent',
26 |
27 | fontWeight: 700,
28 | fontSize: 20,
29 | color: '#333333',
30 | },
31 | reloadIcon: {
32 | width: 16,
33 | height: 16,
34 | padding: 0,
35 | color: '#666666',
36 | },
37 | headerButtonGroup: {
38 | display: 'flex',
39 | alignItems: 'center',
40 | gap: 10,
41 | },
42 | headerSelectedLinkText: {
43 | color: '#747778',
44 |
45 | fontWeight: 400,
46 | fontSize: 14,
47 | lineHeight: '30px',
48 | },
49 | headerButton: {
50 | padding: '12px 24px',
51 |
52 | backgroundColor: '#EDF0FF',
53 | borderRadius: 8,
54 | },
55 | headerButtonText: {
56 | color: '#0058CB',
57 |
58 | fontWeight: 400,
59 | fontSize: 14,
60 | lineHeight: '14px',
61 | },
62 | content: {
63 | overflowY: 'scroll',
64 | overflowX: 'hidden',
65 |
66 | height: '100%',
67 | padding: '12px 36px 0px',
68 | },
69 | linkListEmpty: {
70 | marginTop: 80,
71 | fontWeight: 400,
72 | fontSize: 20,
73 | lineHeight: '14px',
74 | color: '#77777B',
75 | },
76 |
77 | /* common */
78 | rowSpread: {
79 | display: 'flex',
80 | alignItems: 'center',
81 | },
82 | center: {
83 | display: 'flex',
84 | alignItems: 'center',
85 | justifyContent: 'center',
86 | },
87 | }))
88 |
89 | export default useStyles
90 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/Profile/style.js:
--------------------------------------------------------------------------------
1 | import makeStyles from '@mui/styles/makeStyles'
2 |
3 | const useStyles = makeStyles({
4 | root: {
5 | width: 320,
6 | height: 228,
7 | },
8 | title: {
9 | width: 100,
10 | height: 29,
11 | fontSize: 20,
12 | fontWeight: 'bold',
13 | fontStretch: 'normal',
14 | fontStyle: 'normal',
15 | lineHeight: 'normal',
16 | letterSpacing: 'normal',
17 | color: '#000000',
18 | },
19 | content: {
20 | paddingTop: 24,
21 | },
22 | profileImg: {
23 | width: 52,
24 | height: 52,
25 | marginRight: 16,
26 | marginBottom: 24,
27 | },
28 | profileInfoGrid: {
29 | paddingTop: 5,
30 | },
31 | profileName: {
32 | fontSize: 14,
33 | },
34 | profileEmail: {
35 | fontSize: 12,
36 | },
37 | profileBtn: {
38 | fontSize: 12,
39 | marginTop: 5,
40 | },
41 | logoutBtn: {
42 | width: '100%',
43 | },
44 | })
45 |
46 | export default useStyles
47 |
--------------------------------------------------------------------------------
/src/main/pages/Home/AppBar/style.js:
--------------------------------------------------------------------------------
1 | import { ListItem } from '@mui/material'
2 | import { makeStyles, withStyles } from '@mui/styles'
3 |
4 | export const useStyles = makeStyles((theme) => ({
5 | appBar: {
6 | position: 'sticky',
7 | top: 0,
8 | zIndex: 100,
9 |
10 | paddingTop: 12,
11 | paddingBottom: 12,
12 |
13 | display: 'flex',
14 | justifyContent: 'flex-end',
15 | alignItems: 'center',
16 |
17 | backgroundColor:
18 | theme.palette.type !== 'dark' ? theme.palette.colorGroup.lightGrey : theme.palette.background.default,
19 | },
20 | appBarInversion: {
21 | backgroundColor: '#FFFFFF',
22 | },
23 | imgButton: {
24 | width: 17,
25 | height: 17,
26 |
27 | '& > img': {
28 | objectFit: 'contain',
29 | },
30 | },
31 | iconButtonGroup: {
32 | padding: 0,
33 | marginLeft: 11,
34 | marginRight: 37,
35 | display: 'flex',
36 | gap: 11,
37 | },
38 | drawer: {
39 | '& > .MuiDrawer-paper': {
40 | zIndex: theme.zIndex.drawer - 1,
41 | },
42 | '& > .MuiDrawer-paperAnchorRight': {
43 | top: 73,
44 | },
45 | },
46 | }))
47 |
48 | export const StyledListItem = withStyles((theme) => ({
49 | root: {
50 | width: 48,
51 | height: 48,
52 |
53 | backgroundColor: 'white',
54 | borderRadius: 8,
55 | justifyContent: 'center',
56 |
57 | '&:hover': {
58 | backgroundColor: '#d6e4f5',
59 | '& img': {
60 | filter: 'brightness(10)',
61 | },
62 | },
63 | },
64 | }))(ListItem)
65 |
66 | export default useStyles
67 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/AddCategoryModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import { Button } from '@mui/material'
4 | import clsx from 'clsx'
5 | import { useDispatch } from 'react-redux'
6 |
7 | import { categoriesRead, categoryCreateThunk } from '@modules/category'
8 | import { useToast } from '@modules/ui'
9 | import { GAEvent } from '@utils/ga'
10 |
11 | import { StyledDialog, StyledDialogTitle, StyledDialogContent, StyledDialogActions, useStyles } from './style'
12 |
13 | function AddCategoryModal({ open, onClose }) {
14 | const dispatch = useDispatch()
15 | const { openToast } = useToast()
16 | const classes = useStyles()
17 | const [categoryName, setCategoryName] = useState('')
18 | const [addLoading, setAddLoading] = useState(false)
19 |
20 | const handleClickConfirm = async () => {
21 | setAddLoading(true)
22 | if (addLoading || !categoryName) return
23 | try {
24 | await dispatch(categoryCreateThunk({ name: categoryName, is_favorited: false }))
25 | dispatch(categoriesRead.request(undefined, { selectFirstCategory: true }))
26 | setAddLoading(false)
27 | setCategoryName('')
28 | GAEvent('카테고리', '카테고리 생성 완료')
29 | onClose()
30 | } catch (error) {
31 | openToast({ type: 'error', message: error?.response?.data?.message || '네트워크 오류!!' })
32 | }
33 | }
34 | const handleChangeInput = (e) => {
35 | if (e.target.value.length > 24) return
36 | setCategoryName((prev) => (prev = e.target.value))
37 | }
38 | const handleKeyUpEnter = (e) => {
39 | e.stopPropagation()
40 | if (e.key === 'Enter') handleClickConfirm()
41 | }
42 | const handleClickCancel = () => {
43 | setCategoryName('')
44 | onClose()
45 | GAEvent('카테고리', '카테고리 생성 취소')
46 | }
47 |
48 | return (
49 |
50 | 카테고리 생성
51 |
52 |
61 |
62 |
63 |
66 |
69 |
70 |
71 | )
72 | }
73 |
74 | export default AddCategoryModal
75 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/AddCategoryModal/style.js:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'
2 | import { withStyles } from '@mui/styles'
3 | import { makeStyles } from '@mui/styles'
4 |
5 | export const StyledDialog = withStyles((theme) => ({
6 | paperWidthSm: {
7 | padding: '36px 48px 40px',
8 | width: 508,
9 | height: 315,
10 | boxShadow: '8px 8px 24px rgba(0, 0, 0, 0.12)',
11 | borderRadius: 8,
12 | justifyContent: 'space-between',
13 | },
14 | }))(Dialog)
15 |
16 | export const StyledDialogTitle = withStyles((theme) => ({
17 | root: { fontWeight: 'bold', padding: 0, fontSize: 24 },
18 | }))(DialogTitle)
19 |
20 | export const StyledDialogContent = withStyles((theme) => ({
21 | root: { padding: 0, display: 'inline-block', flex: '0 1 auto' },
22 | }))(DialogContent)
23 |
24 | export const StyledDialogActions = withStyles((theme) => ({
25 | root: {
26 | display: 'flex',
27 | flex: '0 0 auto',
28 | padding: 0,
29 | alignItems: 'center',
30 | justifyContent: 'flex-end',
31 | },
32 | }))(DialogActions)
33 |
34 | export const useStyles = makeStyles((theme) => ({
35 | categoryNameInput: {
36 | width: '100%',
37 | height: 59,
38 | background: '#FCFCFC',
39 | border: '1px solid #AAAAAA',
40 | borderRadius: 8,
41 | padding: 16,
42 | fontSize: 18,
43 | color: '#333',
44 | '&::placeholder': {
45 | color: '#999',
46 | },
47 | '&:focus': {
48 | border: `1px solid ${theme.palette.primary.main}`,
49 | outline: 'none',
50 | },
51 | },
52 | modalButton: {
53 | width: 86,
54 | height: 48,
55 | color: '#666666',
56 | background: '#fff',
57 | border: '1px solid #E6E6E6',
58 | borderRadius: 8,
59 |
60 | '&.confirm': {
61 | backgroundColor: theme.palette.primary.main,
62 | color: '#fff',
63 | fontWeight: 'bold',
64 | },
65 | },
66 | }))
67 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/CategoryHeader/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import useStyles from './style'
4 |
5 | function CategoryHeader({ type }) {
6 | const classes = useStyles()
7 |
8 | return {type === 'favorite' ? 'Favorite' : 'Category'}
9 | }
10 |
11 | export default React.memo(CategoryHeader)
12 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/CategoryHeader/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | categoryHeader: {
5 | display: 'flex',
6 | justifyContent: 'space-between',
7 | alignItems: 'center',
8 | fontSize: 14,
9 | color: '#333',
10 | lineHeight: '32px',
11 | },
12 | }))
13 |
14 | export default useStyles
15 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/CategoryItem/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | tabContainer: {
5 | position: 'relative',
6 | height: 40,
7 | display: 'block',
8 | borderRadius: 4,
9 | margin: '4px auto',
10 | outline: 'none',
11 | cursor: 'pointer',
12 | backgroundColor: ({ hovered, selected }) => (hovered || selected ? '#E8F1FF' : 'transparent'),
13 | '&:hover': {
14 | backgroundColor: ({ selected }) => (selected ? '#E8F1FF' : '#F2F2F2'),
15 | },
16 | },
17 | tab: {
18 | borderRadius: 4,
19 | width: '100%',
20 | padding: '0 16px',
21 | height: '100%',
22 | display: 'flex',
23 | alignItems: 'center',
24 | justifyContent: 'space-between',
25 | backgroundColor: ({ moreOpen, selected }) =>
26 | moreOpen && selected ? '#E8F1FF' : moreOpen && !selected ? '#F2F2F2' : 'transparent',
27 | '& > .show-btn-group': {
28 | visibility: ({ moreOpen }) => (moreOpen ? 'visible' : 'hidden'),
29 | },
30 | '&:hover': {
31 | '& > .show-btn-group': {
32 | visibility: 'visible',
33 | },
34 | '& > .link-container': {
35 | visibility: 'hidden',
36 | },
37 | },
38 | },
39 | dragFinished: {
40 | opacity: '80%',
41 | fontWeight: 'bold',
42 | },
43 | title: {
44 | fontSize: 14,
45 | width: 'calc(100% - 60px)',
46 | fontWeight: ({ selected }) => (selected ? 700 : 400),
47 | lineHeight: '32px',
48 | color: ({ selected }) => (selected ? '#333' : '#666'),
49 | textOverflow: 'ellipsis',
50 | overflow: 'hidden',
51 | whiteSpace: 'nowrap',
52 | },
53 | urlCount: {
54 | display: 'inline-block',
55 | fontSize: 12,
56 | lineHeight: '32px',
57 | color: '#999',
58 | opacity: 0.6,
59 | textAlign: 'center',
60 | },
61 | link: {
62 | display: 'flex',
63 | alignItems: 'center',
64 | justifyContent: 'flex-end',
65 | visibility: ({ moreOpen }) => (moreOpen ? 'hidden' : 'visible'),
66 | },
67 | btnGroup: {
68 | position: 'absolute',
69 | padding: '8px 16px 8px 0',
70 | top: 0,
71 | right: 0,
72 | display: 'flex',
73 | borderRadius: 4,
74 |
75 | '& > button': {
76 | display: 'flex',
77 | flexDirection: 'column',
78 | justifyContent: 'center',
79 | alignItems: 'center',
80 | width: 24,
81 | height: 24,
82 | backgroundColor: '#fff',
83 | borderRadius: 4,
84 | padding: 0,
85 | border: 'unset',
86 | marginLeft: 8,
87 | cursor: 'pointer',
88 | },
89 | },
90 | moreBtnGroup: {
91 | display: 'flex',
92 | flexDirection: 'column',
93 | alignItems: 'flex-start',
94 | justifyContent: 'space-between',
95 | padding: 12,
96 | position: 'fixed',
97 | backgroundColor: '#fff',
98 | boxShadow: '8px 8px 10px rgba(0, 0, 0, 0.08)',
99 | borderRadius: 8,
100 | zIndex: 10,
101 | width: 107,
102 | height: 77,
103 |
104 | '& > button': {
105 | fontSize: 14,
106 | lineHeight: '20px',
107 | color: '#666',
108 | backgroundColor: 'transparent',
109 | border: 'unset',
110 | width: '100%',
111 | textAlign: 'left',
112 | cursor: 'pointer',
113 |
114 | '&:hover': {
115 | fontWeight: '900',
116 | },
117 | },
118 | },
119 | }))
120 |
121 | export default useStyles
122 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/CategoryItemWrapper/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 |
3 | import { DRAG } from '@modules/ui'
4 |
5 | import useStyles from './style'
6 | const { CATEGORY } = DRAG
7 |
8 | function CategoryItemWrapper({
9 | data,
10 | handleDragStart,
11 | handleDragOver,
12 | handleDragLeave,
13 | handleDragDrop,
14 | handleDragEnd,
15 | children,
16 | }) {
17 | const classes = useStyles()
18 |
19 | const dragLineRef = useRef(null)
20 | const categoryRef = useRef(null)
21 |
22 | return (
23 |
24 |
25 |
35 | {children}
36 |
37 |
38 | )
39 | }
40 |
41 | export default React.memo(CategoryItemWrapper)
42 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/CategoryItemWrapper/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | dragline: {
5 | width: 208,
6 | height: 2,
7 | borderRadius: 2,
8 | backgroundImage: 'linear-gradient(271deg, #e0f6ff, #2083ff)',
9 | opacity: 0,
10 | margin: 'auto',
11 | },
12 | }))
13 |
14 | export default useStyles
15 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/FirstCategoryDropZone/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import useStyles from './style'
4 |
5 | function FirstCategoryDropZone({ openDropZone, handleDragDrop, handleDragOverFirstCategory }) {
6 | const classes = useStyles()
7 |
8 | return (
9 |
15 |
카테고리를 생성해주세요.
16 |
17 | )
18 | }
19 |
20 | export default FirstCategoryDropZone
21 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/FirstCategoryDropZone/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | hidden: {
5 | display: 'none',
6 | },
7 | categoryDropZone: {
8 | width: '100%',
9 | height: '300px',
10 | },
11 | title: {
12 | borderRadius: '4px',
13 | border: 'dashed 1px #CCCCCC',
14 | backgroundColor: '#FCFCFC',
15 | fontSize: 14,
16 | fontWeight: 400,
17 | fontStretch: 'normal',
18 | fontStyle: 'normal',
19 | lineHeight: '38px',
20 | letterSpacing: 'normal',
21 | textAlign: 'center',
22 | color: '#CCCCCC',
23 | margin: '10px auto',
24 | },
25 | }))
26 |
27 | export default useStyles
28 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/FirstFavoriteDropZone/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import useStyles from './style'
4 |
5 | function FirstFavoriteDropZone({ handleDragDrop, handleDragOverFirstFavorite }) {
6 | const classes = useStyles()
7 |
8 | return (
9 |
15 |
자주 사용하는 카테고리 등록
16 |
17 | )
18 | }
19 |
20 | export default FirstFavoriteDropZone
21 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/FirstFavoriteDropZone/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | firstFavoriteDropZone: {
5 | width: '100%',
6 | height: '90px',
7 | },
8 | title: {
9 | borderRadius: '4px',
10 | border: 'dashed 1px #CCCCCC',
11 | backgroundColor: '#FCFCFC',
12 | fontSize: 14,
13 | fontWeight: 400,
14 | fontStretch: 'normal',
15 | fontStyle: 'normal',
16 | lineHeight: '38px',
17 | letterSpacing: 'normal',
18 | textAlign: 'center',
19 | color: '#CCCCCC',
20 | margin: '10px auto',
21 | },
22 | }))
23 |
24 | export default useStyles
25 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/UpdateCategoryModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 |
3 | import { Button } from '@mui/material'
4 | import clsx from 'clsx'
5 | import { useSelector, useDispatch } from 'react-redux'
6 |
7 | import {
8 | categoryEdit,
9 | categoryClearEdit,
10 | categorySelect,
11 | categorySelector,
12 | categoryModifyThunk,
13 | categoriesReadThunk,
14 | } from '@modules/category'
15 | import { useToast } from '@modules/ui'
16 | import { GAEvent } from '@utils/ga'
17 |
18 | import { StyledDialog, StyledDialogTitle, StyledDialogContent, StyledDialogActions, useStyles } from './style'
19 |
20 | function UpdateCategoryModal({ open, onClose }) {
21 | const dispatch = useDispatch()
22 | const editedCategory = useSelector(categorySelector.editedCategory)
23 | const { openToast } = useToast()
24 | const inputRef = useRef()
25 | const classes = useStyles()
26 | const [categoryName, setCategoryName] = useState(editedCategory.name)
27 | const [updateLoading, setUpdateLoading] = useState(false)
28 |
29 | const handleConfirm = async () => {
30 | setUpdateLoading(true)
31 | if (updateLoading || !categoryName) return
32 | try {
33 | const response = await dispatch(
34 | categoryModifyThunk({
35 | id: editedCategory.id,
36 | name: inputRef.current.value,
37 | })
38 | )
39 | await dispatch(categoriesReadThunk())
40 | dispatch(categorySelect({ ...response }))
41 | dispatch(categoryClearEdit())
42 | setUpdateLoading(false)
43 | GAEvent('메인', '카테고리 제목 수정 완료')
44 | onClose()
45 | } catch (error) {
46 | openToast({ type: 'error', message: error?.response?.data?.message || '네트워크 오류!!' })
47 | }
48 | }
49 | const handleChangeInput = (e) => {
50 | if (e.target.value.length > 24) return
51 | setCategoryName((prev) => (prev = e.target.value))
52 | dispatch(categoryEdit({ name: e.target.value }))
53 | }
54 | const handleKeyUpEnter = (e) => {
55 | e.stopPropagation()
56 | if (e.key === 'Enter') handleConfirm()
57 | }
58 | const handleCancel = () => {
59 | dispatch(categoryClearEdit())
60 | onClose()
61 | GAEvent('메인', '카테고리 제목 수정 취소')
62 | }
63 |
64 | return (
65 |
66 | 카테고리 수정
67 |
68 |
78 |
79 |
80 |
83 |
86 |
87 |
88 | )
89 | }
90 |
91 | export default UpdateCategoryModal
92 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/UpdateCategoryModal/style.js:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'
2 | import { withStyles } from '@mui/styles'
3 | import { makeStyles } from '@mui/styles'
4 |
5 | export const StyledDialog = withStyles((theme) => ({
6 | paperWidthSm: {
7 | padding: '36px 48px 40px',
8 | width: 508,
9 | height: 315,
10 | boxShadow: '8px 8px 24px rgba(0, 0, 0, 0.12)',
11 | borderRadius: 8,
12 | justifyContent: 'space-between',
13 | },
14 | }))(Dialog)
15 |
16 | export const StyledDialogTitle = withStyles((theme) => ({
17 | root: { fontWeight: 'bold', padding: 0, fontSize: 24 },
18 | }))(DialogTitle)
19 |
20 | export const StyledDialogContent = withStyles((theme) => ({
21 | root: { padding: 0, display: 'inline-block', flex: '0 1 auto' },
22 | }))(DialogContent)
23 |
24 | export const StyledDialogActions = withStyles((theme) => ({
25 | root: {
26 | display: 'flex',
27 | flex: '0 0 auto',
28 | padding: 0,
29 | alignItems: 'center',
30 | justifyContent: 'flex-end',
31 | },
32 | }))(DialogActions)
33 |
34 | export const useStyles = makeStyles((theme) => ({
35 | categoryNameInput: {
36 | width: '100%',
37 | height: 59,
38 | background: '#FCFCFC',
39 | border: '1px solid #AAAAAA',
40 | borderRadius: 8,
41 | padding: 16,
42 | fontSize: 18,
43 | color: '#333',
44 | '&::placeholder': {
45 | color: '#999',
46 | },
47 | '&:focus': {
48 | border: `1px solid ${theme.palette.primary.main}`,
49 | outline: 'none',
50 | },
51 | },
52 | modalButton: {
53 | width: 86,
54 | height: 48,
55 | color: '#666666',
56 | background: '#fff',
57 | border: '1px solid #E6E6E6',
58 | borderRadius: 8,
59 |
60 | '&.confirm': {
61 | backgroundColor: theme.palette.primary.main,
62 | color: '#fff',
63 | fontWeight: 'bold',
64 | },
65 | },
66 | }))
67 |
--------------------------------------------------------------------------------
/src/main/pages/Home/CategoryList/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles(() => ({
4 | logo: {
5 | width: 95,
6 | margin: '24px 0 28px',
7 | },
8 | drawerPaper: {
9 | width: '100%',
10 | height: '100%',
11 | padding: '0 28px',
12 | },
13 | categoryContainer: {
14 | width: '100%',
15 | height: 'calc(100% - 200px)',
16 | scrollbarWidth: 'none',
17 | '&::-webkit-scrollbar': {
18 | display: 'none',
19 | },
20 | overflowY: 'scroll',
21 | borderTop: ({ observerVisible }) => (observerVisible ? 'unset' : '1px solid #C0C0C0'),
22 | },
23 | ioBox: {
24 | width: '100%',
25 | height: 52,
26 | },
27 | flexCenterBackground: {
28 | display: 'flex',
29 | flexDirection: 'column',
30 | justifyContent: 'center',
31 | alignItems: 'center',
32 | height: '100vh',
33 | },
34 | favoriteList: {
35 | padding: 0,
36 | marginBottom: 52,
37 | },
38 | notFavoriteList: {},
39 | addCategoryBtn: {
40 | height: '120px',
41 | width: '100%',
42 | borderTop: '1px solid #C0C0C0',
43 | display: 'flex',
44 | flexDirection: 'column',
45 | justifyContent: 'center',
46 | alignItems: 'center',
47 | backgroundColor: 'transparent',
48 | border: 'unset',
49 | borderRadius: 'unset',
50 |
51 | '& > h3': {
52 | fontSize: 16,
53 | fontWeight: 400,
54 | lineHeight: '32px',
55 | color: '#666',
56 | padding: '4px 16px',
57 | borderRadius: 8,
58 | justifyContent: 'space-between',
59 | display: 'flex',
60 | width: '100%',
61 | },
62 |
63 | '&:hover': {
64 | cursor: 'pointer',
65 | backgroundColor: 'transparent',
66 | '& > h3': {
67 | backgroundColor: '#F2F2F2',
68 | },
69 | },
70 | '&:focus-visible': {
71 | outline: 'unset',
72 | },
73 | },
74 | }))
75 |
76 | export default useStyles
77 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkDropZone/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 |
3 | import { AddToPhotos as AddToPhotosIcon } from '@mui/icons-material'
4 | import clsx from 'clsx'
5 | import { useDispatch, useSelector } from 'react-redux'
6 |
7 | import { categoriesRead, categorySelector } from '@modules/category'
8 | import { linkCreateThunk, linksRead } from '@modules/link'
9 | import { useToast } from '@modules/ui'
10 | import { DROP_ZONE, DRAG, useDrag, useDropZone } from '@modules/ui'
11 |
12 | import useStyles from './style'
13 |
14 | const { LINK } = DRAG
15 | const { LINK_DROP_ZONE } = DROP_ZONE
16 |
17 | function LinkDropZone() {
18 | const classes = useStyles()
19 | const dispatch = useDispatch()
20 | const { openToast } = useToast()
21 | const categoryList = useSelector(categorySelector.listData)
22 | const selectedCategory = useSelector(categorySelector.selectedCategory)
23 | const { listData, clearDragData } = useDrag(LINK)
24 | const { open } = useDropZone(LINK_DROP_ZONE)
25 |
26 | const handleDropOnCardArea = useCallback(
27 | async (e) => {
28 | try {
29 | e.stopPropagation()
30 | if (!categoryList.length) {
31 | openToast({ type: 'error', message: '카테고리를 생성해주세요.' })
32 | return
33 | }
34 | if (!selectedCategory.id) {
35 | openToast({ type: 'error', message: '링크를 저장할 카테고리를 만들거나 선택해주세요.' })
36 | return
37 | }
38 | const path = listData.reduce((prev, data) => prev.concat(data.path), [])
39 | await dispatch(linkCreateThunk({ categoryId: selectedCategory.id, path }))
40 | clearDragData()
41 | dispatch(linksRead.request({ categoryId: selectedCategory.id }, { key: selectedCategory.id }))
42 | dispatch(categoriesRead.request())
43 | openToast({ type: 'success', message: '링크가 저장 되었습니다.' })
44 | } catch (error) {
45 | openToast({ type: 'error', message: error?.response?.data?.message || '네트워크 오류!!' })
46 | }
47 | },
48 | [dispatch, listData, openToast, selectedCategory.id, clearDragData, categoryList]
49 | )
50 |
51 | const handleDragOverOnCardArea = useCallback((e) => {
52 | e.preventDefault()
53 | }, [])
54 |
55 | return (
56 |
65 | )
66 | }
67 |
68 | export default LinkDropZone
69 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkDropZone/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | coverBackground: {
5 | position: 'absolute',
6 | zIndex: 1,
7 |
8 | display: 'flex',
9 | justifyContent: 'center',
10 | alignItems: 'center',
11 |
12 | width: 'calc(100% - 917px)',
13 | height: 'calc(100vh - 72px);',
14 |
15 | backgroundColor: 'rgba(53, 142, 255, 0.15)',
16 | opacity: 1,
17 | },
18 | displayNone: {
19 | display: 'none',
20 | },
21 | addLinkIcon: {
22 | width: 50,
23 | height: 50,
24 |
25 | color: theme.palette.primary.main,
26 | },
27 | }))
28 |
29 | export default useStyles
30 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/Header/EditableCategoryTitle/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import CreateIcon from '@mui/icons-material/Create'
4 | import IconButton from '@mui/material/IconButton'
5 | import Typography from '@mui/material/Typography'
6 | import clsx from 'clsx'
7 | import { useSelector, useDispatch } from 'react-redux'
8 |
9 | import { MODAL_NAME, uiSelector, useDialog } from '@/modules/ui'
10 | import { categoryEdit, categorySelector } from '@modules/category'
11 | import { GAEvent } from '@utils/ga'
12 |
13 | import useStyles from './style'
14 |
15 | function EditableCategoryTitle() {
16 | const classes = useStyles()
17 |
18 | const dispatch = useDispatch()
19 | const category = useSelector(categorySelector.selectedCategory)
20 | const isAppBarInversion = useSelector(uiSelector.isAppBarInversion)
21 |
22 | const { toggle: updateCategoryToggle } = useDialog(MODAL_NAME.UPDATE_CATEGORY_MODAL)
23 |
24 | const handleClickChangeName = (e) => {
25 | e.stopPropagation()
26 | updateCategoryToggle()
27 | dispatch(categoryEdit({ id: category?.id, name: category?.name }))
28 | GAEvent('메인', '카테고리 제목 수정 버튼 클릭')
29 | }
30 |
31 | if (!category.id) return null
32 |
33 | return (
34 |
35 |
40 | {category?.name}
41 |
42 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default EditableCategoryTitle
54 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/Header/EditableCategoryTitle/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | export const useStyles = makeStyles((theme) => ({
4 | root: {
5 | display: 'flex',
6 | alignItems: 'center',
7 | justifyContent: 'space-between',
8 | flexShrink: 0,
9 | maxWidth: 499,
10 | borderRadius: 8,
11 | },
12 | title: {
13 | fontWeight: 700,
14 | fontSize: 28,
15 | color: '#333',
16 | },
17 | titleInversion: {
18 | fontSize: 16,
19 | },
20 | updateBtn: {
21 | marginLeft: 10,
22 | },
23 | updateBtnInversion: {
24 | opacity: 0,
25 | },
26 | confirmBtn: {
27 | width: 74,
28 | height: 38,
29 | backgroundColor: theme.palette.primary.main,
30 | borderRadius: 8,
31 | color: '#fff',
32 | fontWeight: 400,
33 | fontSize: 14,
34 | border: 'unset',
35 | cursor: 'pointer',
36 | },
37 | }))
38 |
39 | export default useStyles
40 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/Header/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | export const useStyles = makeStyles((theme) => ({
4 | toolbar: {
5 | paddingLeft: 0,
6 | paddingRight: 0,
7 |
8 | display: 'flex',
9 | justifyContent: 'space-between',
10 |
11 | [theme.breakpoints.up('sm')]: {
12 | height: 40,
13 | },
14 | },
15 | selectLinksBtn: {
16 | padding: '8px 20px',
17 | color: '#666666',
18 | backgroundColor: '#ffffff',
19 | border: '1px solid #E6E6E6',
20 | borderRadius: 8,
21 | },
22 | selectLinksBtnGroup: {
23 | display: 'flex',
24 | flexDirection: 'row',
25 | alignItems: 'center',
26 | fontSize: 14,
27 | marginLeft: 'auto',
28 | },
29 | selectBoxInversion: {
30 | opacity: 0,
31 | display: 'none',
32 | },
33 | chosenLinks: {
34 | marginRight: 8,
35 | },
36 | btnInBtnGroup: {
37 | padding: '8px 20px',
38 | marginLeft: 8,
39 | backgroundColor: '#EDF0FF',
40 | borderRadius: 8,
41 | },
42 | deleteLinksBtn: {
43 | color: 'red',
44 | padding: '8px 20px',
45 | marginLeft: 8,
46 | backgroundColor: '#FFEDE9',
47 | borderRadius: 8,
48 | },
49 | refreshBtn: {
50 | padding: 6,
51 | },
52 | }))
53 |
54 | export default useStyles
55 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/Link/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | backdrop: {
5 | position: 'absolute',
6 | backgroundColor: 'rgba(0, 0, 0, 0.2)',
7 | },
8 | root: {
9 | position: 'relative',
10 | width: 300,
11 | height: 348,
12 | marginTop: 20,
13 | boxShadow: 'none',
14 | borderRadius: 8,
15 |
16 | '&:hover': {
17 | transform: 'translateY(-12px)',
18 | transition: 'transform 0.5s',
19 | cursor: 'pointer',
20 | },
21 | },
22 | selectedBackdrop: {
23 | position: 'absolute',
24 | backgroundColor: 'rgba(29, 120, 255, 0.1)',
25 | },
26 | checkbox: {
27 | position: 'absolute',
28 | top: 0,
29 | right: 0,
30 | color: 'white',
31 | '&.Mui-checked': {
32 | color: 'white',
33 |
34 | '& > .MuiSvgIcon-root': {
35 | backgroundColor: '#1D78FF',
36 | borderRadius: 4,
37 | },
38 | },
39 | '& > .MuiSvgIcon-root': {
40 | backgroundColor: 'rgba(129, 147, 174, 0.4)',
41 | borderRadius: 4,
42 | },
43 | },
44 | urlBox: {
45 | display: 'flex',
46 | alignItems: 'center',
47 | },
48 | urlFavicon: {
49 | paddingBottom: 8,
50 | },
51 | urlSubFont: {
52 | color: theme.palette.text.secondary,
53 | fontSize: 12,
54 | fontWeight: 400,
55 | letterSpacing: -0.6,
56 | paddingLeft: 8,
57 | paddingBottom: 8,
58 | },
59 | newTabIcon: {
60 | position: 'absolute',
61 | top: 5,
62 | right: 5,
63 | padding: 0,
64 | },
65 | cardContent: {
66 | height: 182,
67 | padding: '12px 16px 0 16px',
68 | },
69 | contentTitleEditable: {
70 | width: '100%',
71 | overflow: 'hidden',
72 | fontSize: 16,
73 | padding: '4px 8px',
74 | marginBottom: 4,
75 | backgroundColor: '#F6F6F6',
76 | borderRadius: 4,
77 | },
78 | contentDescEditable: {
79 | display: 'box',
80 | boxOrient: 'vertical',
81 | overflow: 'hidden',
82 | width: '100%',
83 | maxHeight: 65,
84 | fontSize: 14,
85 | whiteSpace: 'pre-line',
86 | lineClamp: 3,
87 | padding: '4px 8px',
88 | backgroundColor: '#F6F6F6',
89 | borderRadius: 4,
90 | },
91 | contentTitle: {
92 | width: '100%',
93 | overflow: 'hidden',
94 | display: 'box',
95 | boxOrient: 'vertical',
96 | lineClamp: 2,
97 | maxHeight: 50,
98 | fontWeight: 400,
99 | fontSize: 16,
100 | marginBottom: 8,
101 | },
102 | contentDesc: {
103 | display: 'box',
104 | boxOrient: 'vertical',
105 | overflow: 'hidden',
106 | width: '100%',
107 | maxHeight: 65,
108 | fontWeight: 300,
109 | fontSize: 13,
110 | whiteSpace: 'pre-line',
111 | lineClamp: 3,
112 | },
113 | cardActions: {
114 | position: 'absolute',
115 | bottom: 0,
116 | width: '100%',
117 | padding: '0 16px 16px 16px',
118 | },
119 | copyIcon: {
120 | width: 18,
121 | height: 18,
122 | },
123 | doneIcon: {
124 | marginLeft: 'auto',
125 | },
126 | unionIcon: {
127 | width: 30,
128 | height: 30,
129 | marginLeft: 'auto',
130 | },
131 | menuIconImg: {
132 | width: 30,
133 | height: 20,
134 | paddingRight: 10,
135 | },
136 | alarmIconActive: {
137 | color: '#fdd835',
138 | },
139 | dateTimePicker: {
140 | display: 'none',
141 | },
142 | }))
143 |
144 | export default useStyles
145 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/LinkSkeleton/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Card from '@mui/material/Card'
4 | import CardContent from '@mui/material/CardContent'
5 | import Skeleton from '@mui/material/Skeleton'
6 |
7 | import useStyles from './style'
8 |
9 | function LinkSkeleton() {
10 | const classes = useStyles()
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default LinkSkeleton
26 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/LinkSkeleton/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | const useStyles = makeStyles((theme) => ({
4 | card: {
5 | width: 300,
6 | height: 348,
7 | marginTop: 20,
8 | boxShadow: 'none',
9 | },
10 | cardContent: {
11 | padding: '12px 16px',
12 | '& span': {
13 | transform: 'scale(1.0)',
14 | },
15 | },
16 | }))
17 |
18 | export default useStyles
19 |
--------------------------------------------------------------------------------
/src/main/pages/Home/LinkList/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | export const useStyles = makeStyles((theme) => ({
4 | root: {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | justifyContent: 'center',
8 | alignItems: 'center',
9 | height: 'max-content',
10 | [theme.breakpoints.up('sm')]: {
11 | width: '100%',
12 | },
13 | [theme.breakpoints.up('md')]: {
14 | minWidth: 748,
15 | },
16 | [theme.breakpoints.up('lg')]: {
17 | minWidth: 1084,
18 | },
19 | [theme.breakpoints.up('xl')]: {
20 | minWidth: 1420,
21 | },
22 | backgroundColor:
23 | theme.palette.type !== 'dark' ? theme.palette.colorGroup.lightGrey : theme.palette.background.default,
24 | padding: '80px 56px 172px 56px',
25 | },
26 | header: {
27 | position: 'sticky',
28 | top: 8,
29 | zIndex: 101,
30 |
31 | marginBottom: 32,
32 | [theme.breakpoints.up('sm')]: {
33 | width: '100%',
34 | },
35 | [theme.breakpoints.up('md')]: {
36 | width: 636,
37 | },
38 | [theme.breakpoints.up('lg')]: {
39 | width: 972,
40 | },
41 | [theme.breakpoints.up('xl')]: {
42 | width: 1308,
43 | },
44 | },
45 | content: {
46 | display: 'flex',
47 | justifyContent: 'flex-start',
48 | flexFlow: 'row wrap',
49 | gap: 36,
50 | margin: '0',
51 | [theme.breakpoints.up('sm')]: {
52 | width: '100%',
53 | },
54 | [theme.breakpoints.up('md')]: {
55 | width: 636,
56 | },
57 | [theme.breakpoints.up('lg')]: {
58 | width: 972,
59 | },
60 | [theme.breakpoints.up('xl')]: {
61 | width: 1308,
62 | },
63 | },
64 | center: {
65 | display: 'flex',
66 | flexDirection: 'column',
67 | alignItems: 'center',
68 | justifyContent: 'center',
69 | width: '100%',
70 | height: 482,
71 | backgroundColor: '#ffffff',
72 | borderRadius: 8,
73 | [theme.breakpoints.up('sm')]: {
74 | width: '100%',
75 | },
76 | [theme.breakpoints.up('md')]: {
77 | width: 636,
78 | },
79 | [theme.breakpoints.up('lg')]: {
80 | width: 972,
81 | },
82 | [theme.breakpoints.up('xl')]: {
83 | width: 1308,
84 | },
85 | },
86 | centerFont: {
87 | height: 40,
88 | fontWeight: 400,
89 | fontSize: 20,
90 | color: '#77777B',
91 | },
92 | centerSubFont: {
93 | height: 25,
94 | fontWeight: 300,
95 | fontSize: 16,
96 | color: '#77777B',
97 | },
98 | }))
99 |
100 | export default useStyles
101 |
--------------------------------------------------------------------------------
/src/main/pages/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef, useState } from 'react'
2 |
3 | import { Resizable } from 're-resizable'
4 | import { useDispatch, useSelector } from 'react-redux'
5 |
6 | import ScrollUpButton from '@/main/components/ScrollUpButton'
7 | import { useAlarmNoticeConnection } from '@/modules/alarmNotice'
8 | import { categorySelector } from '@/modules/category'
9 | import { appBarInversionChangeState } from '@/modules/ui'
10 |
11 | import AppBar from './AppBar'
12 | import CategoryList from './CategoryList'
13 | import LinkDropZone from './LinkDropZone'
14 | import LinkList from './LinkList'
15 | import useStyles from './style'
16 |
17 | export default function Home() {
18 | useAlarmNoticeConnection()
19 |
20 | const [resizing, setResizing] = useState(false)
21 | const onResizeStart = () => setResizing(true)
22 | const onResizeStop = () => setResizing(false)
23 | const classes = useStyles({ resizing })
24 |
25 | const dispatch = useDispatch()
26 | const selectedCategory = useSelector(categorySelector.selectedCategory)
27 |
28 | const mainRef = useRef(null)
29 |
30 | const [isShowScrollUpButton, setIshShowScrollUpButton] = useState(null)
31 |
32 | const handleScrollMain = useCallback(
33 | (e) => {
34 | const scrollTop = e.target.scrollTop
35 | const clientHeight = e.target.clientHeight
36 | dispatch(appBarInversionChangeState(scrollTop > 150))
37 | setIshShowScrollUpButton(scrollTop + clientHeight / 2 > clientHeight)
38 | },
39 | [dispatch]
40 | )
41 |
42 | useEffect(() => {
43 | mainRef.current.scrollTop = 0
44 | }, [selectedCategory])
45 |
46 | return (
47 |
48 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/pages/Home/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | export const useStyles = makeStyles((theme) => ({
4 | root: {
5 | display: 'flex',
6 | height: '100vh',
7 | backgroundColor: '#fafafa',
8 |
9 | '& > .resizable-category .resize-handler': {
10 | right: '0 !important',
11 | borderRight: ({ resizing }) => (resizing ? '2px solid #E9E9E9' : 'transparent'),
12 | '&:hover': {
13 | borderRight: '2px solid #E9E9E9',
14 | },
15 | },
16 | },
17 | main: {
18 | width: '100%',
19 | height: '100vh',
20 |
21 | overflow: 'scroll',
22 | },
23 | }))
24 |
25 | export default useStyles
26 |
--------------------------------------------------------------------------------
/src/main/pages/Login/LoginForm/GloginButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 |
3 | import Button from '@mui/material/Button'
4 | import { useDispatch } from 'react-redux'
5 |
6 | import LogoGoogle from '@assets/images/logo-google.png'
7 | import { useToast } from '@modules/ui'
8 | import { userGloginThunk } from '@modules/user'
9 | import { GAEvent } from '@utils/ga'
10 |
11 | function GloginButton() {
12 | const dispatch = useDispatch()
13 | const { openToast } = useToast()
14 |
15 | const handleGoogleLogin = useCallback(
16 | async (e) => {
17 | e.preventDefault()
18 | try {
19 | GAEvent('로그인', '구글 로그인')
20 | await dispatch(userGloginThunk())
21 | window.location.href = '/index.html'
22 | } catch (error) {
23 | openToast({ type: 'error', message: error?.response?.data?.message || '네트워크 오류!!' })
24 | }
25 | },
26 | [dispatch, openToast]
27 | )
28 |
29 | return (
30 | }
32 | className="btn-GoogleLogin"
33 | onClick={handleGoogleLogin}
34 | >
35 | 구글 이메일로 로그인
36 |
37 | )
38 | }
39 |
40 | export default GloginButton
41 |
--------------------------------------------------------------------------------
/src/main/pages/Login/LoginForm/NloginForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 |
3 | import { yupResolver } from '@hookform/resolvers/yup'
4 | import { Button } from '@mui/material'
5 | import { Link } from 'react-chrome-extension-router'
6 | import { useForm } from 'react-hook-form'
7 | import { useDispatch } from 'react-redux'
8 | import * as yup from 'yup'
9 |
10 | import ValidationMessage from '@main/components/ValidationMessage'
11 | import Register from '@main/pages/Register'
12 | import { useToast } from '@modules/ui'
13 | import { userLoginThunk } from '@modules/user'
14 | import { GAEvent } from '@utils/ga'
15 |
16 | const SCHEMA = yup.object().shape({
17 | email: yup.string().required('이메일은 필수 입력입니다.'),
18 | password: yup.string().required('비밀번호는 필수 입력입니다.'),
19 | })
20 |
21 | function NloginForm() {
22 | const dispatch = useDispatch()
23 | const { openToast } = useToast()
24 | const { register, handleSubmit, errors, formState } = useForm({
25 | defaultValues: {
26 | email: '',
27 | password: '',
28 | },
29 | mode: 'onChange',
30 | resolver: yupResolver(SCHEMA),
31 | })
32 |
33 | const handleLogin = useCallback(
34 | async (formData) => {
35 | try {
36 | GAEvent('로그인', '일반 로그인')
37 | await dispatch(userLoginThunk(formData))
38 | window.location.href = '/index.html'
39 | } catch (error) {
40 | openToast({ type: 'error', message: error.response?.data?.message || '네트워크 오류!!' })
41 | }
42 | },
43 | [dispatch, openToast]
44 | )
45 |
46 | return (
47 |
85 | )
86 | }
87 |
88 | export default NloginForm
89 |
--------------------------------------------------------------------------------
/src/main/pages/Login/LoginForm/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import URLinkLogo from '@assets/images/logo-urlink-full.png'
4 |
5 | import GloginButton from './GloginButton'
6 | import NloginForm from './NloginForm'
7 |
8 | function LoginForm() {
9 | return (
10 |
11 |

12 |
로그인
13 |
14 |
15 | OR
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default LoginForm
23 |
--------------------------------------------------------------------------------
/src/main/pages/Login/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import '@assets/scss/LoginSignup.scss'
4 |
5 | import LoginForm from './LoginForm'
6 |
7 | function Login() {
8 | return (
9 |
10 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default Login
19 |
--------------------------------------------------------------------------------
/src/main/pages/Register/RegisterForm/GregisterButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 |
3 | import Button from '@mui/material/Button'
4 | import { useDispatch } from 'react-redux'
5 |
6 | import LogoGoogle from '@assets/images/logo-google.png'
7 | import { useToast } from '@modules/ui'
8 | import { userGregisterThunk } from '@modules/user'
9 | import { GAEvent } from '@utils/ga'
10 |
11 | function GregisterButton() {
12 | const dispatch = useDispatch()
13 | const { openToast } = useToast()
14 |
15 | const handleGoogleSignup = useCallback(
16 | async (e) => {
17 | e.preventDefault()
18 | try {
19 | GAEvent('회원가입', '구글 회원가입')
20 | await dispatch(userGregisterThunk())
21 | window.location.href = '/index.html'
22 | } catch (error) {
23 | openToast({ type: 'error', message: error?.response?.data?.message || '네트워크 오류!!' })
24 | }
25 | },
26 | [dispatch, openToast]
27 | )
28 |
29 | return (
30 | }
32 | className="btn-GoogleLogin"
33 | onClick={handleGoogleSignup}
34 | >
35 | 구글 이메일로 회원가입
36 |
37 | )
38 | }
39 |
40 | export default GregisterButton
41 |
--------------------------------------------------------------------------------
/src/main/pages/Register/RegisterForm/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import URLinkLogo from '@assets/images/logo-urlink-full.png'
4 |
5 | import GregisterButton from './GregisterButton'
6 | import NregisterForm from './NregisterForm'
7 |
8 | function RegisterForm() {
9 | return (
10 |
11 |

12 |
회원가입
13 |
14 |
15 | OR
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default RegisterForm
23 |
--------------------------------------------------------------------------------
/src/main/pages/Register/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import '@assets/scss/LoginSignup.scss'
4 |
5 | import RegisterForm from './RegisterForm'
6 |
7 | function Register() {
8 | return (
9 |
10 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default Register
19 |
--------------------------------------------------------------------------------
/src/main/pages/Start/style.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@mui/styles'
2 |
3 | import mainBackground from '@assets/images/mainBackground2.png'
4 |
5 | const useStyles = makeStyles((theme) => ({
6 | root: {
7 | overflow: 'hidden',
8 | padding: 40,
9 | display: 'flex',
10 | height: '100vh',
11 | flexDirection: 'column',
12 | alignItems: 'center',
13 | justifyContent: 'center',
14 | backgroundImage: `url(${mainBackground}), linear-gradient(to top, #0260d8, #157cff 68%)`,
15 | backgroundSize: '100%',
16 | '&::-webkit-scrollbar': {
17 | display: 'none !important',
18 | },
19 | },
20 | titleCenter: {
21 | display: 'flex',
22 | flexDirection: 'column',
23 | alignItems: 'center',
24 | justifyContent: 'center',
25 | width: '100%',
26 | },
27 | getStartBtn: {
28 | marginTop: '10px',
29 | display: 'flex',
30 | alignItems: 'center',
31 | justifyContent: 'center',
32 | width: '200px',
33 | height: '48px',
34 | borderRadius: '40px',
35 | boxShadow: '0 2px 16px 0 rgba(0, 0, 0, 0.24)',
36 | backgroundColor: '#ffffff',
37 | textDecoration: 'none',
38 | },
39 | getStartText: {
40 | fontSize: '24px',
41 | fontWeight: 'bold',
42 | },
43 | textBlack: {
44 | color: '#212529',
45 | },
46 | textBlue: {
47 | color: '#358eff',
48 | },
49 | textGrp: {
50 | flexDirection: 'column',
51 | alignItems: 'center',
52 | justifyContent: 'center',
53 | marginTop: 20,
54 | },
55 | textCenter: {
56 | textAlign: 'center',
57 | height: 29,
58 |
59 | fontSize: 15,
60 | letterSpacing: -0.47,
61 | color: '#ffffff',
62 | },
63 | textBold: {
64 | fontWeight: 'bold',
65 | },
66 | imgCenter: {
67 | paddingTop: 20,
68 | display: 'flex',
69 | flexDirection: 'column',
70 | alignItems: 'center',
71 | justifyContent: 'center',
72 | },
73 | imgAutoSize: {
74 | width: '100%',
75 | },
76 | }))
77 |
78 | export default useStyles
79 |
--------------------------------------------------------------------------------
/src/main/theme.js:
--------------------------------------------------------------------------------
1 | import { createTheme } from '@mui/material/styles'
2 |
3 | const theme = createTheme({
4 | breakpoints: {
5 | values: {
6 | xs: 0,
7 | sm: 600,
8 | md: 1098,
9 | lg: 1435,
10 | xl: 1770,
11 | },
12 | },
13 | palette: {
14 | colorGroup: {
15 | battleshipGrey: '#737b84',
16 | lightGrey: '#fafafa',
17 | blueGrey: '#868e96',
18 | paleGrey: '#f6f7f9',
19 | salmon: '#ff6b6b',
20 | greenBlue: '#00b381',
21 | },
22 | common: {
23 | black: '#212529',
24 | },
25 | primary: {
26 | main: '#1D78FF',
27 | },
28 | text: {
29 | primary: '#333333',
30 | secondary: '#666666',
31 | description: '#999999',
32 | },
33 | },
34 | typography: {
35 | fontFamily: ['"Spoqa Han Sans"', '"Spoqa Han Sans JP"', 'sans-serif'].join(','),
36 | },
37 | components: {
38 | MuiCssBaseline: {
39 | styleOverrides: `
40 | ::-webkit-scrollbar {
41 | display: none;
42 | }
43 |
44 | *: {
45 | // box-sizing: border-box;
46 | font-family: "Spoqa Han Sans", "Spoqa Han Sans JP", "sans-serif";
47 | }
48 |
49 | button: {
50 | cursor: pointer;
51 | outline: 0;
52 | border: unset;
53 | background-color: transparent;
54 | }
55 | `,
56 | },
57 | },
58 | })
59 |
60 | export default theme
61 |
--------------------------------------------------------------------------------
/src/modules/alarm/api.js:
--------------------------------------------------------------------------------
1 | import { axios } from '@utils/http/client'
2 | import queryFilter from '@utils/http/queryFilter'
3 |
4 | import queryInfoData from './queryInfoData'
5 |
6 | export const requestAlarmsRead = (data = {}) => {
7 | const queryData = queryInfoData['alarmsRead']
8 | const info = queryFilter({ queryData, originDataInfo: data })
9 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
10 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
11 | }
12 |
13 | export const requestAlarmRemove = (data = {}) => {
14 | const queryData = queryInfoData['alarmRemove']
15 | const info = queryFilter({ queryData, originDataInfo: data })
16 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
17 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
18 | }
19 |
20 | export const requestAlarmCreate = (data = {}) => {
21 | const queryData = queryInfoData['alarmCreate']
22 | const info = queryFilter({ queryData, originDataInfo: data })
23 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
24 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
25 | }
26 |
--------------------------------------------------------------------------------
/src/modules/alarm/index.js:
--------------------------------------------------------------------------------
1 | export * from './api'
2 | export { default as queryInfoData } from './queryInfoData'
3 | export * from './reducer'
4 | export * from './saga'
5 |
--------------------------------------------------------------------------------
/src/modules/alarm/queryInfoData.js:
--------------------------------------------------------------------------------
1 | const queryInfoData = {
2 | /**
3 | * * alarm 리스트 모두 조회 GET
4 | * * alarm/
5 | * * JWT 필요
6 | */
7 | alarmsRead: {
8 | API: 'alarm/',
9 | method: 'get',
10 | bodyQuery: {},
11 | urlQuery: {},
12 | replaceAPI() {
13 | return this.API
14 | },
15 | },
16 |
17 | /**
18 | * * alarm 등록 POST
19 | * * alarm/?category={categoryId}&url={urlId} (all Required)
20 | * * JWT 필요
21 | */
22 | alarmCreate: {
23 | API: 'alarm/?category={categoryId}&url={urlId}',
24 | method: 'post',
25 | bodyQuery: {
26 | name: '',
27 | reserved_time: {
28 | year: '',
29 | month: '',
30 | day: '',
31 | hour: '',
32 | minute: '',
33 | },
34 | },
35 | urlQuery: {
36 | categoryId: '',
37 | urlId: '',
38 | },
39 | replaceAPI({ categoryId, urlId }) {
40 | if (!categoryId || !urlId) return
41 | return this.API.replace('{categoryId}', categoryId).replace('{urlId}', urlId)
42 | },
43 | },
44 |
45 | /**
46 | * * alarm 등록 DELETE
47 | * * alarm/{alarmId}/ (Required)
48 | * * JWT 필요
49 | */
50 | alarmRemove: {
51 | API: 'alarm/{alarmId}/',
52 | method: 'delete',
53 | bodyQuery: {},
54 | urlQuery: {
55 | alarmId: '',
56 | },
57 | replaceAPI({ alarmId }) {
58 | if (!alarmId) return
59 | return this.API.replace('{alarmId}', alarmId)
60 | },
61 | },
62 | }
63 |
64 | export default queryInfoData
65 |
--------------------------------------------------------------------------------
/src/modules/alarm/reducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 |
3 | import { createRequestAction, createRequestThunk } from '../helpers'
4 |
5 | export const ALARM = 'ALARM'
6 |
7 | export const alarmsRead = createRequestAction(`${ALARM}/READ`)
8 |
9 | export const alarmCreate = createRequestAction(`${ALARM}/CREATE`)
10 | export const alarmCreateThunk = createRequestThunk(alarmCreate)
11 |
12 | export const alarmRemove = createRequestAction(`${ALARM}/REMOVE`)
13 |
14 | // Reducer
15 | const initialState = {}
16 | export const alarmReducer = createReducer(initialState, {})
17 |
--------------------------------------------------------------------------------
/src/modules/alarm/saga.js:
--------------------------------------------------------------------------------
1 | import { call, takeLatest } from 'redux-saga/effects'
2 |
3 | import { createRequestSaga } from '../helpers'
4 | import * as api from './api'
5 | import { alarmsRead, alarmCreate, alarmRemove } from './reducer'
6 |
7 | const watchAlarmsRead = createRequestSaga(alarmsRead, function* () {
8 | const { data } = yield call(api.requestAlarmsRead)
9 | return data
10 | })
11 |
12 | const watchalarmCreate = createRequestSaga(alarmCreate, function* (action) {
13 | const { data } = yield call(api.requestAlarmCreate, action.payload)
14 | return data
15 | })
16 |
17 | const watchAlarmRemove = createRequestSaga(alarmRemove, function* (action) {
18 | const { data } = yield call(api.requestAlarmRemove, action.payload)
19 | return data
20 | })
21 |
22 | export function* alarmSaga() {
23 | yield takeLatest(alarmsRead.REQUEST, watchAlarmsRead)
24 | yield takeLatest(alarmCreate.REQUEST, watchalarmCreate)
25 | yield takeLatest(alarmRemove.REQUEST, watchAlarmRemove)
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/alarmNotice/hooks/useAlarmNoticeConnection.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { alarmNoticeConnection } from '@modules/alarmNotice'
6 | import { ERROR } from '@modules/error'
7 | import { PENDING } from '@modules/pending'
8 | import { alarmSocket, SOCKET_READY_STATE } from '@utils/http/ws'
9 |
10 | const useAlarmNoticeConnection = () => {
11 | const dispatch = useDispatch()
12 | const pending = useSelector((state) => state[PENDING][alarmNoticeConnection.TYPE])
13 | const error = useSelector((state) => state[ERROR][alarmNoticeConnection.TYPE])
14 |
15 | useEffect(() => {
16 | if (!alarmSocket.ws || alarmSocket.ws?.readyState !== SOCKET_READY_STATE.OPEN) {
17 | alarmSocket
18 | .onConnection()
19 | .setOnmessage((event) => {
20 | dispatch(alarmNoticeConnection.request({ event }))
21 | })
22 | .setOnerror((event) => {
23 | dispatch(alarmNoticeConnection.failure({ event }))
24 | })
25 | }
26 | return () => {
27 | alarmSocket.onClose()
28 | }
29 | }, [dispatch])
30 |
31 | return { pending, error }
32 | }
33 |
34 | export default useAlarmNoticeConnection
35 |
--------------------------------------------------------------------------------
/src/modules/alarmNotice/index.js:
--------------------------------------------------------------------------------
1 | export * from './ws'
2 | export { default as useAlarmNoticeConnection } from './hooks/useAlarmNoticeConnection'
3 | export { default as queryInfoData } from './queryInfoData'
4 | export * from './reducer'
5 | export * from './saga'
6 |
--------------------------------------------------------------------------------
/src/modules/alarmNotice/queryInfoData.js:
--------------------------------------------------------------------------------
1 | const queryInfoData = {
2 | /**
3 | * * alarm Read SOCKET
4 | * * 알람 봤다고 요청
5 | */
6 | alarmReadNotice: {
7 | method: 'socket',
8 | bodyQuery: {
9 | alarm_id: '',
10 | action: 'read',
11 | },
12 | },
13 |
14 | /**
15 | * * alarm No message return SOCKET
16 | * * 알람에 노출하지 않기로 요청
17 | */
18 | alarmNoReturn: {
19 | method: 'socket',
20 | bodyQuery: {
21 | alarm_id: '',
22 | action: 'done',
23 | },
24 | },
25 | }
26 |
27 | export default queryInfoData
28 |
--------------------------------------------------------------------------------
/src/modules/alarmNotice/reducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 |
3 | import { createRequestAction, createRequestThunk } from '../helpers'
4 |
5 | export const ALARM_NOTICE = 'ALARM_NOTICE'
6 |
7 | export const alarmNoticeConnection = createRequestAction(`${ALARM_NOTICE}/CONNECTION`)
8 | export const alarmNoticeListLoad = createRequestAction(`${ALARM_NOTICE}/LIST_LOAD`)
9 | export const alarmNoticeAdd = createRequestAction(`${ALARM_NOTICE}/ADD`)
10 | export const alarmNoticeModify = createRequestAction(`${ALARM_NOTICE}/MODIFY`)
11 | export const alarmNoticeReadNotice = createRequestAction(`${ALARM_NOTICE}/READ_NOTICE`)
12 | export const alarmNoticeReadNoticeThunk = createRequestThunk(alarmNoticeReadNotice)
13 | export const alarmNoticeNoReturnNotice = createRequestAction(`${ALARM_NOTICE}/NO_RETURN_NOTICE`)
14 | export const alarmNoticeNoReturnNoticeThunk = createRequestThunk(alarmNoticeNoReturnNotice)
15 |
16 | // Reducer
17 | const initialState = {
18 | listData: [],
19 | }
20 | export const alarmNoticeReducer = createReducer(initialState, {
21 | [alarmNoticeListLoad.SUCCESS]: (state, { payload: listData }) => {
22 | state.listData = listData
23 | },
24 | [alarmNoticeAdd.SUCCESS]: (state, { payload: data }) => {
25 | state.listData.push(data)
26 | },
27 | [alarmNoticeModify.SUCCESS]: (state, { payload: listData }) => {
28 | state.listData = listData
29 | },
30 | })
31 |
32 | // Select
33 | export const alarmNoticeSelector = {
34 | listData: (state) => state[ALARM_NOTICE].listData,
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/alarmNotice/saga.js:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest, takeEvery } from 'redux-saga/effects'
2 |
3 | import { createRequestSaga } from '@modules/helpers'
4 |
5 | import {
6 | alarmNoticeConnection,
7 | alarmNoticeListLoad,
8 | alarmNoticeAdd,
9 | alarmNoticeModify,
10 | alarmNoticeReadNotice,
11 | alarmNoticeNoReturnNotice,
12 | } from './reducer'
13 | import * as ws from './ws'
14 |
15 | const watchAlarmNoticeConnection = createRequestSaga(alarmNoticeConnection, function* ({ payload: { event } }) {
16 | const { message, status } = JSON.parse(event.data)
17 | switch (status) {
18 | case 'initial':
19 | yield put(alarmNoticeListLoad.success(message))
20 | break
21 | case 'alarm':
22 | yield put(alarmNoticeAdd.success(message))
23 | break
24 | case 'update':
25 | yield put(alarmNoticeModify.success(message))
26 | break
27 | default:
28 | break
29 | }
30 | })
31 |
32 | const watchAlarmNoticeReadNotice = createRequestSaga(alarmNoticeReadNotice, function* (action) {
33 | yield call(ws.requestAlarmReadNotice, action.payload)
34 | })
35 |
36 | const watchAlarmNoticeNoReturnNotice = createRequestSaga(alarmNoticeNoReturnNotice, function* (action) {
37 | yield call(ws.requestAlarmNoReturn, action.payload)
38 | })
39 |
40 | export function* alarmNoticeSaga() {
41 | yield takeLatest(alarmNoticeConnection.REQUEST, watchAlarmNoticeConnection)
42 | yield takeEvery(alarmNoticeReadNotice.REQUEST, watchAlarmNoticeReadNotice)
43 | yield takeEvery(alarmNoticeNoReturnNotice.REQUEST, watchAlarmNoticeNoReturnNotice)
44 | }
45 |
--------------------------------------------------------------------------------
/src/modules/alarmNotice/ws.js:
--------------------------------------------------------------------------------
1 | import queryFilter from '@utils/http/queryFilter'
2 | import { alarmSocket } from '@utils/http/ws'
3 |
4 | import queryInfoData from './queryInfoData'
5 |
6 | export const requestAlarmReadNotice = (data = {}) => {
7 | const queryData = queryInfoData['alarmReadNotice']
8 | const info = queryFilter({ queryData, originDataInfo: { ...data, action: 'read' } })
9 | return alarmSocket.ws.send(JSON.stringify(info))
10 | }
11 |
12 | export const requestAlarmNoReturn = (data = {}) => {
13 | const queryData = queryInfoData['alarmNoReturn']
14 | const info = queryFilter({ queryData, originDataInfo: { ...data, action: 'done' } })
15 | return alarmSocket.ws.send(JSON.stringify(info))
16 | }
17 |
--------------------------------------------------------------------------------
/src/modules/category/api.js:
--------------------------------------------------------------------------------
1 | import { axios } from '@utils/http/client'
2 | import queryFilter from '@utils/http/queryFilter'
3 |
4 | import queryInfoData from './queryInfoData'
5 |
6 | export const requestCategoriesRead = (data = {}) => {
7 | const queryData = queryInfoData['categoriesRead']
8 | const info = queryFilter({ queryData, originDataInfo: data })
9 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
10 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
11 | }
12 |
13 | export const requestCategoryCreate = (data = {}) => {
14 | const queryData = queryInfoData['categoryCreate']
15 | const info = queryFilter({ queryData, originDataInfo: data })
16 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
17 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
18 | }
19 |
20 | export const requestCategoryModify = (data = {}) => {
21 | const queryData = queryInfoData['categoryModify']
22 | const info = queryFilter({ queryData, originDataInfo: data })
23 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
24 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
25 | }
26 |
27 | export const requestCategoryRemove = (data = {}) => {
28 | const queryData = queryInfoData['categoryRemove']
29 | const info = queryFilter({ queryData, originDataInfo: data })
30 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
31 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/category/hooks/useCategories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { categoriesRead, categorySelector } from '@modules/category'
6 | import { ERROR } from '@modules/error'
7 | import { PENDING } from '@modules/pending'
8 |
9 | const useCategories = () => {
10 | const dispatch = useDispatch()
11 | const pending = useSelector((state) => state[PENDING][categoriesRead.TYPE])
12 | const error = useSelector((state) => state[ERROR][categoriesRead.TYPE])
13 | const categories = useSelector(categorySelector.listData)
14 | const favoritedArr = useSelector(categorySelector.favoriteCategories)
15 | const notFavoritedArr = useSelector(categorySelector.normalCategories)
16 |
17 | const reload = () => {
18 | dispatch(categoriesRead.request())
19 | }
20 |
21 | React.useEffect(() => {
22 | dispatch(categoriesRead.request(undefined, { selectFirstCategory: true }))
23 | }, [dispatch])
24 |
25 | return { pending, error, categories, favoritedArr, notFavoritedArr, reload }
26 | }
27 |
28 | export default useCategories
29 |
--------------------------------------------------------------------------------
/src/modules/category/index.js:
--------------------------------------------------------------------------------
1 | export * from './api'
2 | export { default as useCategories } from './hooks/useCategories'
3 | export { default as queryInfoData } from './queryInfoData'
4 | export * from './reducer'
5 | export * from './saga'
6 |
--------------------------------------------------------------------------------
/src/modules/category/queryInfoData.js:
--------------------------------------------------------------------------------
1 | const queryInfoData = {
2 | /**
3 | * * 카테고리 리스트 조회 GET
4 | * * /category/
5 | * * Authorization: JWT 필요
6 | */
7 | categoriesRead: {
8 | API: 'category/',
9 | method: 'get',
10 | bodyQuery: {},
11 | urlQuery: {},
12 | replaceAPI() {
13 | return this.API
14 | },
15 | },
16 |
17 | /**
18 | * * 카테고리 생성 POST
19 | * * /category/
20 | * * Authorization: JWT 필요
21 | */
22 | categoryCreate: {
23 | API: 'category/',
24 | method: 'post',
25 | bodyQuery: {
26 | name: '',
27 | is_favorited: '',
28 | },
29 | urlQuery: {},
30 | replaceAPI() {
31 | return this.API
32 | },
33 | },
34 |
35 | /**
36 | * * 카테고리 수정 PATCH
37 | * * /category/{id}/
38 | * * Authorization: JWT 필요
39 | */
40 | categoryModify: {
41 | API: 'category/{id}/',
42 | method: 'patch',
43 | bodyQuery: {
44 | id: '',
45 | name: '',
46 | order: '',
47 | is_favorited: '',
48 | },
49 | urlQuery: {
50 | id: '',
51 | },
52 | replaceAPI({ id }) {
53 | if (!id) return
54 | return this.API.replace('{id}', id)
55 | },
56 | },
57 |
58 | /**
59 | * * 카테고리 삭제 DELETE
60 | * * /category/{id}/
61 | * * Authorization: JWT 필요
62 | */
63 | categoryRemove: {
64 | API: 'category/{id}/',
65 | method: 'delete',
66 | bodyQuery: {},
67 | urlQuery: {
68 | id: '',
69 | },
70 | replaceAPI({ id }) {
71 | if (!id) return
72 | return this.API.replace('{id}', id)
73 | },
74 | },
75 | }
76 |
77 | export default queryInfoData
78 |
--------------------------------------------------------------------------------
/src/modules/category/reducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer, createAction, createSelector } from '@reduxjs/toolkit'
2 |
3 | import { createRequestAction, createRequestThunk } from '../helpers'
4 |
5 | export const CATEGORY = 'CATEGORY'
6 |
7 | export const categoriesRead = createRequestAction(`${CATEGORY}/READ`)
8 | export const categoriesReadThunk = createRequestThunk(categoriesRead)
9 |
10 | export const categoryCreate = createRequestAction(`${CATEGORY}/CREATE`)
11 | export const categoryCreateThunk = createRequestThunk(categoryCreate)
12 |
13 | export const categoryModify = createRequestAction(`${CATEGORY}/MODIFY`)
14 | export const categoryModifyThunk = createRequestThunk(categoryModify)
15 |
16 | export const categoryRemove = createRequestAction(`${CATEGORY}/REMOVE`)
17 | export const categoryRemoveThunk = createRequestThunk(categoryRemove)
18 |
19 | export const categorySelect = createAction(`${CATEGORY}/SELECT`)
20 | export const categoryEdit = createAction(`${CATEGORY}/EDIT`)
21 | export const categoryClearEdit = createAction(`${CATEGORY}/CLEAR_EDIT`)
22 |
23 | // Reducer
24 | const initialState = {
25 | listData: [],
26 | selectedCategory: {},
27 | editedCategory: {},
28 | }
29 | export const categoryReducer = createReducer(initialState, {
30 | [categoriesRead.SUCCESS]: (state, { payload, meta }) => {
31 | state.listData = payload
32 | if (meta?.selectFirstCategory) {
33 | state.selectedCategory = {
34 | ...state.selectedCategory,
35 | ...payload[0],
36 | }
37 | }
38 | },
39 | [categoryRemove.SUCCESS]: (state, { payload, meta }) => {
40 | if (meta?.key === state.selectedCategory.id) {
41 | state.selectedCategory = initialState.selectedCategory
42 | }
43 | },
44 | [categorySelect]: (state, { payload }) => {
45 | state.selectedCategory = { ...state.selectedCategory, ...payload }
46 | },
47 | [categoryEdit]: (state, { payload }) => {
48 | state.editedCategory = { ...state.editedCategory, ...payload }
49 | },
50 | [categoryClearEdit]: (state) => {
51 | state.editedCategory = initialState.editedCategory
52 | },
53 | })
54 |
55 | // Select
56 | const selectFavoriteCategoriesState = createSelector(
57 | (state) => state.listData,
58 | (listData) => {
59 | return listData.filter((item) => Boolean(item.is_favorited))
60 | }
61 | )
62 | const selectNormalCategoriesState = createSelector(
63 | (state) => state.listData,
64 | (listData) => {
65 | return listData.filter((item) => !item.is_favorited)
66 | }
67 | )
68 | export const categorySelector = {
69 | listData: (state) => state[CATEGORY].listData,
70 | favoriteCategories: (state) => selectFavoriteCategoriesState(state[CATEGORY]),
71 | normalCategories: (state) => selectNormalCategoriesState(state[CATEGORY]),
72 | selectedCategory: (state) => state[CATEGORY].selectedCategory,
73 | editedCategory: (state) => state[CATEGORY].editedCategory,
74 | }
75 |
--------------------------------------------------------------------------------
/src/modules/category/saga.js:
--------------------------------------------------------------------------------
1 | import { call, debounce, takeLatest } from 'redux-saga/effects'
2 |
3 | import { createRequestSaga } from '../helpers'
4 | import * as api from './api'
5 | import { categoriesRead, categoryCreate, categoryModify, categoryRemove } from './reducer'
6 |
7 | const watchCategoriesRead = createRequestSaga(categoriesRead, function* () {
8 | const { data } = yield call(api.requestCategoriesRead)
9 | return data
10 | })
11 |
12 | const watchCategoryCreate = createRequestSaga(categoryCreate, function* (action) {
13 | const { data } = yield call(api.requestCategoryCreate, action.payload)
14 | return data
15 | })
16 |
17 | const watchCategoryModify = createRequestSaga(categoryModify, function* (action) {
18 | const { data } = yield call(api.requestCategoryModify, action.payload)
19 | return data
20 | })
21 |
22 | const watchCategoryRemove = createRequestSaga(categoryRemove, function* (action) {
23 | const { data } = yield call(api.requestCategoryRemove, action.payload)
24 | return data
25 | })
26 |
27 | export function* categorySaga() {
28 | yield debounce(100, categoriesRead.REQUEST, watchCategoriesRead)
29 | yield takeLatest(categoryCreate.REQUEST, watchCategoryCreate)
30 | yield takeLatest(categoryModify.REQUEST, watchCategoryModify)
31 | yield takeLatest(categoryRemove.REQUEST, watchCategoryRemove)
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/error/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/modules/error/reducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer, createAction } from '@reduxjs/toolkit'
2 |
3 | import { getActionName } from '../helpers'
4 |
5 | export const ERROR = 'ERROR'
6 | const NO_KEY = undefined
7 |
8 | export const removeError = createAction('error/REMOVE_ERROR')
9 |
10 | // Reducer
11 | const initialState = {}
12 | export const errorReducer = createReducer(
13 | initialState,
14 | {
15 | [removeError]: (state, { payload: actionName }) => {
16 | delete state[actionName]
17 | },
18 | },
19 | [
20 | {
21 | matcher: ({ type }) => {
22 | return (
23 | getActionName(type) && (type.endsWith('/REQUEST') || type.endsWith('/SUCCESS') || type.endsWith('/FAILURE'))
24 | )
25 | },
26 | reducer(state, { meta, payload, type }) {
27 | const actionName = getActionName(type)
28 | const key = meta?.key
29 |
30 | if (type.endsWith('/FAILURE')) {
31 | if (key !== NO_KEY) {
32 | if (typeof state[actionName] !== 'object') state[actionName] = {}
33 | state[actionName][key] = payload
34 | } else {
35 | state[actionName] = payload
36 | }
37 | } else if (type.endsWith('/SUCCESS') || type.endsWith('/REQUEST')) {
38 | if (key !== NO_KEY) {
39 | if (typeof state[actionName] !== 'object') state[actionName] = {}
40 | delete state[actionName][key]
41 | } else {
42 | delete state[actionName]
43 | }
44 | }
45 | },
46 | },
47 | ]
48 | )
49 |
--------------------------------------------------------------------------------
/src/modules/helpers.js:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 | import { call, put } from 'redux-saga/effects'
3 |
4 | export function createRequestAction(type) {
5 | const REQUEST = `${type}/REQUEST`
6 | const SUCCESS = `${type}/SUCCESS`
7 | const FAILURE = `${type}/FAILURE`
8 |
9 | return {
10 | TYPE: type,
11 | REQUEST,
12 | SUCCESS,
13 | FAILURE,
14 | request: createAction(REQUEST, (payload, meta) => ({ payload, meta })),
15 | success: createAction(SUCCESS, (payload, meta) => ({ payload, meta })),
16 | failure: createAction(FAILURE, (payload, meta) => ({ payload, meta })),
17 | }
18 | }
19 |
20 | export function createRequestSaga(actions, saga) {
21 | return function* (action) {
22 | try {
23 | const result = yield call(saga, action)
24 | yield put(actions.success(result, action.meta))
25 | if (action.resolve) {
26 | action.resolve(result)
27 | }
28 | } catch (e) {
29 | yield put(actions.failure(e, action.meta))
30 | if (action.reject) {
31 | action.reject(e)
32 | }
33 | }
34 | }
35 | }
36 |
37 | export function createRequestThunk(actions) {
38 | return (payload, meta) => (dispatch) => {
39 | return new Promise((resolve, reject) => {
40 | dispatch({ ...actions.request(payload, meta), resolve, reject })
41 | })
42 | }
43 | }
44 |
45 | export function getActionName(actionType) {
46 | if (typeof actionType !== 'string') {
47 | return null
48 | }
49 |
50 | return actionType.split('/').slice(0, -1).join('/')
51 | }
52 |
--------------------------------------------------------------------------------
/src/modules/historyLink/hooks/useHistoryLinks.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback } from 'react'
2 |
3 | import moment from 'moment'
4 | import { useDispatch, useSelector } from 'react-redux'
5 |
6 | import { ERROR } from '@modules/error'
7 | import { historyLinkListRead, historyLinkSelector, setHistoryLinkChangeFilter } from '@modules/historyLink'
8 | import { INIT, PENDING } from '@modules/pending'
9 |
10 | const MIN_TIME = new Date(moment().add(-1, 'year')).getTime()
11 |
12 | const useHistoryLinks = () => {
13 | const dispatch = useDispatch()
14 | const pending = useSelector((state) => state[PENDING][historyLinkListRead.TYPE])
15 | const error = useSelector((state) => state[ERROR][historyLinkListRead.TYPE])
16 | const filter = useSelector(historyLinkSelector.filter)
17 | const listData = useSelector(historyLinkSelector.listData)
18 |
19 | const reload = useCallback(() => {
20 | dispatch(
21 | setHistoryLinkChangeFilter({
22 | text: '',
23 | startTime: new Date(moment().add(-1, 'day')).getTime(),
24 | endTime: new Date().getTime(),
25 | isNext: false,
26 | })
27 | )
28 | }, [dispatch])
29 |
30 | const keywordSearch = useCallback(
31 | (value) => {
32 | dispatch(
33 | setHistoryLinkChangeFilter({
34 | text: value,
35 | startTime: value ? 0 : new Date(moment().add(-1, 'day')).getTime(),
36 | endTime: new Date().getTime(),
37 | isNext: false,
38 | })
39 | )
40 | },
41 | [dispatch]
42 | )
43 |
44 | const dateSearch = useCallback(
45 | (value) => {
46 | if (!value) value = new Date()
47 | dispatch(
48 | setHistoryLinkChangeFilter({
49 | text: '',
50 | startTime: new Date(moment(value).startOf('day').add(-1, 'day')).getTime(),
51 | endTime: new Date(moment(value).startOf('day').add(1, 'day')).getTime(),
52 | isNext: false,
53 | })
54 | )
55 | },
56 | [dispatch]
57 | )
58 |
59 | const next = useCallback(() => {
60 | dispatch(
61 | setHistoryLinkChangeFilter({
62 | startTime: new Date(moment(filter.startTime).add(-1, 'day')).getTime(),
63 | endTime: filter.startTime,
64 | isNext: true,
65 | })
66 | )
67 | }, [dispatch, filter.startTime])
68 |
69 | useEffect(() => {
70 | if (pending === INIT) reload()
71 | }, [pending, reload])
72 |
73 | useEffect(() => {
74 | if (MIN_TIME < filter.startTime || !filter.startTime) {
75 | dispatch(historyLinkListRead.request(filter))
76 | }
77 | }, [filter, dispatch])
78 |
79 | return { pending, error, filter, listData, reload, keywordSearch, dateSearch, next }
80 | }
81 |
82 | export default useHistoryLinks
83 |
--------------------------------------------------------------------------------
/src/modules/historyLink/index.js:
--------------------------------------------------------------------------------
1 | export { default as useHistoryLinks } from './hooks/useHistoryLinks'
2 | export * from './reducer'
3 | export * from './saga'
4 |
--------------------------------------------------------------------------------
/src/modules/historyLink/reducer.js:
--------------------------------------------------------------------------------
1 | import { createAction, createReducer } from '@reduxjs/toolkit'
2 |
3 | import { createRequestAction } from '../helpers'
4 |
5 | export const HISTORY_LINK = 'HISTORY_LINK'
6 |
7 | export const setInitHistoryLinkFilter = createAction(`${HISTORY_LINK}/INIT_FILTER`)
8 | export const setHistoryLinkChangeFilter = createAction(`${HISTORY_LINK}/CHANGE_FILTER`)
9 | export const historyLinkListRead = createRequestAction(`${HISTORY_LINK}/LIST_READ`)
10 |
11 | // Reducer
12 | const initialState = {
13 | filter: {
14 | text: '',
15 | startTime: 0,
16 | endTime: 0,
17 | maxResults: 0,
18 | isNext: false,
19 | },
20 | listData: [],
21 | }
22 | export const historyLinkReducer = createReducer(initialState, {
23 | [setInitHistoryLinkFilter]: (state) => {
24 | state.filter = initialState.filter
25 | },
26 | [setHistoryLinkChangeFilter]: (state, { payload: data }) => {
27 | state.filter = { ...state.filter, ...data }
28 | },
29 | [historyLinkListRead.SUCCESS]: (state, { payload: listData }) => {
30 | if (state.filter.isNext) state.listData.push(...listData)
31 | else state.listData = listData
32 | },
33 | })
34 |
35 | // Select
36 | export const historyLinkSelector = {
37 | listData: (state) => state[HISTORY_LINK].listData,
38 | filter: (state) => state[HISTORY_LINK].filter,
39 | }
40 |
--------------------------------------------------------------------------------
/src/modules/historyLink/saga.js:
--------------------------------------------------------------------------------
1 | import { call, takeLatest } from 'redux-saga/effects'
2 |
3 | import { getHistoryList } from '@utils/chromeApis/history'
4 |
5 | import { createRequestSaga } from '../helpers'
6 | import { historyLinkListRead } from './reducer'
7 |
8 | const watchHistoryLinkListRead = createRequestSaga(historyLinkListRead, function* (action) {
9 | const resultList = yield call(getHistoryList, action.payload)
10 | return resultList
11 | })
12 |
13 | export function* historyLinkSaga() {
14 | yield takeLatest(historyLinkListRead.REQUEST, watchHistoryLinkListRead)
15 | }
16 |
--------------------------------------------------------------------------------
/src/modules/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { fork, all } from 'redux-saga/effects'
3 |
4 | import { ALARM, alarmReducer, alarmSaga } from './alarm'
5 | import { ALARM_NOTICE, alarmNoticeReducer, alarmNoticeSaga } from './alarmNotice'
6 | import { CATEGORY, categoryReducer, categorySaga } from './category'
7 | import { ERROR, errorReducer } from './error'
8 | import { HISTORY_LINK, historyLinkReducer, historyLinkSaga } from './historyLink'
9 | import { LINK, linkReducer, linkSaga } from './link'
10 | import { PENDING, pendingReducer } from './pending'
11 | import { UI, uiReducer } from './ui'
12 | import { USER, userReducer, userSaga } from './user'
13 |
14 | // root reducer
15 | export const rootReducer = combineReducers({
16 | [PENDING]: pendingReducer,
17 | [ERROR]: errorReducer,
18 | [CATEGORY]: categoryReducer,
19 | [LINK]: linkReducer,
20 | [ALARM]: alarmReducer,
21 | [USER]: userReducer,
22 | [HISTORY_LINK]: historyLinkReducer,
23 | [ALARM_NOTICE]: alarmNoticeReducer,
24 | [UI]: uiReducer,
25 | })
26 |
27 | // root saga
28 | export function* rootSaga() {
29 | yield all([
30 | fork(categorySaga),
31 | fork(linkSaga),
32 | fork(alarmSaga),
33 | fork(userSaga),
34 | fork(historyLinkSaga),
35 | fork(alarmNoticeSaga),
36 | ])
37 | }
38 |
--------------------------------------------------------------------------------
/src/modules/link/api.js:
--------------------------------------------------------------------------------
1 | import { axios } from '@utils/http/client'
2 | import queryFilter from '@utils/http/queryFilter'
3 |
4 | import queryInfoData from './queryInfoData'
5 |
6 | export const requestLinksRead = (data = {}) => {
7 | const queryData = queryInfoData['linksRead']
8 | const info = queryFilter({ queryData, originDataInfo: data })
9 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
10 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
11 | }
12 |
13 | export const requestLinkModify = (data = {}) => {
14 | const queryData = queryInfoData['linkModify']
15 | const info = queryFilter({ queryData, originDataInfo: data })
16 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
17 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
18 | }
19 |
20 | export const requestLinkRemove = (data = {}) => {
21 | const queryData = queryInfoData['linkRemove']
22 | const info = queryFilter({ queryData, originDataInfo: data })
23 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
24 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
25 | }
26 |
27 | export const requestLinkCreate = (data = {}) => {
28 | const queryData = queryInfoData['linkCreate']
29 | const info = queryFilter({ queryData, originDataInfo: data })
30 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
31 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/link/hooks/useLinks.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { ERROR } from '@modules/error'
6 | import { linksRead, linkSelector } from '@modules/link'
7 | import { INIT, PENDING } from '@modules/pending'
8 |
9 | const useLinks = ({ detact = false, categoryId, selectedName, keyword }) => {
10 | const dispatch = useDispatch()
11 | const pending = useSelector((state) => state[PENDING][linksRead.TYPE])
12 | const error = useSelector((state) => state[ERROR][linksRead.TYPE])
13 | const links = useSelector((state) => linkSelector.linksData(state)[categoryId]) || []
14 |
15 | const reload = useCallback(() => {
16 | if (categoryId) dispatch(linksRead.request({ categoryId }, { key: categoryId }))
17 | }, [dispatch, categoryId])
18 |
19 | const filterChangeLoad = useCallback(() => {
20 | if (categoryId) {
21 | dispatch(
22 | linksRead.request(
23 | {
24 | categoryId,
25 | [selectedName]: keyword,
26 | },
27 | { key: categoryId }
28 | )
29 | )
30 | }
31 | }, [dispatch, selectedName, keyword, categoryId])
32 |
33 | useEffect(() => {
34 | if (pending === INIT) {
35 | reload()
36 | }
37 | }, [pending, reload])
38 |
39 | useEffect(() => {
40 | if (detact) {
41 | filterChangeLoad()
42 | }
43 | }, [detact, filterChangeLoad])
44 |
45 | return { pending, error, links, reload }
46 | }
47 |
48 | export default useLinks
49 |
--------------------------------------------------------------------------------
/src/modules/link/index.js:
--------------------------------------------------------------------------------
1 | export * from './api'
2 | export { default as useLinks } from './hooks/useLinks'
3 | export { default as queryInfoData } from './queryInfoData'
4 | export * from './reducer'
5 | export * from './saga'
6 |
--------------------------------------------------------------------------------
/src/modules/link/queryInfoData.js:
--------------------------------------------------------------------------------
1 | const queryInfoData = {
2 | /**
3 | * * Link 리스트 조회 GET
4 | * * url/?category={categoryId}&path={path}&title={title}
5 | * * JWT 필요
6 | */
7 | linksRead: {
8 | API: 'url/?category={categoryId}&path={path}&title={title}',
9 | method: 'get',
10 | bodyQuery: {},
11 | urlQuery: {
12 | categoryId: '',
13 | path: '',
14 | title: '',
15 | },
16 | replaceAPI({ categoryId, path, title }) {
17 | if (!categoryId) return
18 | return this.API.replace('{categoryId}', categoryId)
19 | .replace('&path={path}', path ? `&path=${path}` : '')
20 | .replace('&title={title}', title ? `&title=${title}` : '')
21 | },
22 | },
23 |
24 | /**
25 | * * URL 등록 POST
26 | * * url/?category={categoryId}
27 | * * JWT 필요
28 | */
29 | linkCreate: {
30 | API: 'url/?category={categoryId}',
31 | method: 'post',
32 | bodyQuery: {
33 | path: [],
34 | },
35 | urlQuery: {
36 | categoryId: '',
37 | },
38 | replaceAPI({ categoryId }) {
39 | if (!categoryId) return
40 | return this.API.replace('{categoryId}', categoryId)
41 | },
42 | },
43 |
44 | /**
45 | * * URL 등록 PATCH
46 | * * /url/{urlId}/
47 | * * JWT 필요
48 | */
49 | linkModify: {
50 | API: 'url/{urlId}/',
51 | method: 'patch',
52 | bodyQuery: {
53 | title: '',
54 | description: '',
55 | is_favorited: '',
56 | },
57 | urlQuery: {
58 | urlId: '',
59 | },
60 | replaceAPI({ urlId }) {
61 | if (!urlId) return
62 | return this.API.replace('{urlId}', urlId)
63 | },
64 | },
65 |
66 | /**
67 | * * URL 삭제 DELETE
68 | * * url/{urlId}/
69 | * * JWT 필요
70 | */
71 | linkRemove: {
72 | API: 'url/{urlId}/',
73 | method: 'delete',
74 | bodyQuery: {},
75 | urlQuery: {
76 | urlId: '',
77 | },
78 | replaceAPI({ urlId }) {
79 | if (!urlId) return
80 | return this.API.replace('{urlId}', urlId)
81 | },
82 | },
83 | }
84 |
85 | export default queryInfoData
86 |
--------------------------------------------------------------------------------
/src/modules/link/reducer.js:
--------------------------------------------------------------------------------
1 | import { createAction, createReducer } from '@reduxjs/toolkit'
2 |
3 | import { createRequestAction, createRequestThunk } from '../helpers'
4 |
5 | export const LINK = 'LINK'
6 |
7 | export const linksRead = createRequestAction(`${LINK}/LIST/READ`)
8 | export const linksReadThunk = createRequestThunk(linksRead)
9 |
10 | export const linkCreate = createRequestAction(`${LINK}/CREATE`)
11 | export const linkCreateThunk = createRequestThunk(linkCreate)
12 |
13 | export const linkModify = createRequestAction(`${LINK}/MODIFY`)
14 | export const linkModifyThunk = createRequestThunk(linkModify)
15 |
16 | export const linkRemove = createRequestAction(`${LINK}/REMOVE`)
17 | export const linkRemoveThunk = createRequestThunk(linkRemove)
18 |
19 | export const linksRemove = createRequestAction(`${LINK}/LIST/REMOVE`)
20 | export const linksRemoveThunk = createRequestThunk(linksRemove)
21 |
22 | export const linkSelectBoxChangeState = createAction(`${LINK}/CHANGE_SELECT_BOX_STATE`)
23 | export const linkSelect = createAction(`${LINK}/SELECT`)
24 | export const linkCancleSelect = createAction(`${LINK}/CANCLE_SELECT`)
25 | export const linkClearSelect = createAction(`${LINK}/CLEAR_SELECT`)
26 |
27 | export const linkSearchFilterInit = createAction(`${LINK}/INIT_SEARCH_FILTER`)
28 | export const linkSearchFilterChangeState = createAction(`${LINK}/CHANGE_SEARCH_FILTER`)
29 |
30 | // Reducer
31 | const initialState = {
32 | searchFilter: {
33 | selectedName: '',
34 | keyword: '',
35 | },
36 | linksData: {},
37 | isOpenLinkSelectBox: false,
38 | selectedLink: [],
39 | createLinksCategoryId: undefined,
40 | }
41 | export const linkReducer = createReducer(initialState, {
42 | [linkSearchFilterInit]: (state) => {
43 | state.searchFilter = initialState.searchFilter
44 | },
45 | [linkSearchFilterChangeState]: (state, { payload: data }) => {
46 | state.searchFilter = { ...state.searchFilter, ...data }
47 | },
48 | [linksRead.SUCCESS]: (state, { payload, meta }) => {
49 | state.linksData[meta.key] = payload
50 | },
51 | [linkCreate.REQUEST]: (state, { payload }) => {
52 | state.createLinksCategoryId = payload.categoryId
53 | },
54 | [linkSelectBoxChangeState]: (state, { payload }) => {
55 | state.isOpenLinkSelectBox = payload
56 | },
57 | [linkSelect]: (state, { payload: linkData }) => {
58 | if (!state.selectedLink.find((data) => data.id === linkData.id)) {
59 | state.selectedLink.push(linkData)
60 | }
61 | },
62 | [linkCancleSelect]: (state, { payload: linkData }) => {
63 | state.selectedLink = state.selectedLink.filter((data) => data.id !== linkData.id)
64 | },
65 | [linkClearSelect]: (state) => {
66 | state.selectedLink = initialState.selectedLink
67 | },
68 | })
69 |
70 | // Select
71 | export const linkSelector = {
72 | linksData: (state) => state[LINK].linksData,
73 | isOpenLinkSelectBox: (state) => state[LINK].isOpenLinkSelectBox,
74 | selectSelectedLink: (state) => state[LINK].selectedLink,
75 | searchFilter: (state) => state[LINK].searchFilter,
76 | createLinksCategoryId: (state) => state[LINK].createLinksCategoryId,
77 | }
78 |
--------------------------------------------------------------------------------
/src/modules/link/saga.js:
--------------------------------------------------------------------------------
1 | import { all, call, debounce, takeLatest } from 'redux-saga/effects'
2 |
3 | import { createRequestSaga } from '../helpers'
4 | import * as api from './api'
5 | import { linksRead, linkCreate, linkModify, linkRemove, linksRemove } from './reducer'
6 |
7 | const watchLinksRead = createRequestSaga(linksRead, function* (action) {
8 | const { data } = yield call(api.requestLinksRead, action.payload)
9 | return data
10 | })
11 |
12 | const watchLinkCreate = createRequestSaga(linkCreate, function* (action) {
13 | const { data } = yield call(api.requestLinkCreate, action.payload)
14 | return data
15 | })
16 |
17 | const watchLinkModify = createRequestSaga(linkModify, function* (action) {
18 | const { data } = yield call(api.requestLinkModify, action.payload)
19 | return data
20 | })
21 |
22 | const watchLinkRemove = createRequestSaga(linkRemove, function* (action) {
23 | const { data } = yield call(api.requestLinkRemove, action.payload)
24 | return data
25 | })
26 |
27 | const watchLinksRemove = createRequestSaga(linksRemove, function* ({ payload: { urlIdList } }) {
28 | const listData = yield all(urlIdList.map((data) => call(api.requestLinkRemove, data)))
29 | return listData.map((res) => res.data)
30 | })
31 |
32 | export function* linkSaga() {
33 | yield debounce(100, linksRead.REQUEST, watchLinksRead)
34 | yield takeLatest(linkCreate.REQUEST, watchLinkCreate)
35 | yield takeLatest(linkModify.REQUEST, watchLinkModify)
36 | yield takeLatest(linkRemove.REQUEST, watchLinkRemove)
37 | yield takeLatest(linksRemove.REQUEST, watchLinksRemove)
38 | }
39 |
--------------------------------------------------------------------------------
/src/modules/pending/constants.js:
--------------------------------------------------------------------------------
1 | export const INIT = undefined
2 |
--------------------------------------------------------------------------------
/src/modules/pending/index.js:
--------------------------------------------------------------------------------
1 | export * from './constants'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/modules/pending/reducer.js:
--------------------------------------------------------------------------------
1 | import { createAction, createReducer } from '@reduxjs/toolkit'
2 |
3 | import { getActionName } from '../helpers'
4 |
5 | export const PENDING = 'PENDING'
6 | export const NO_KEY = undefined
7 |
8 | export const setPending = createAction(`${PENDING}/SET_PENDING`)
9 | export const clearPendingHistory = createAction(`${PENDING}/CLEAR_PENDING_HISTORY`)
10 |
11 | // Reducer
12 | const initialState = {}
13 | export const pendingReducer = createReducer(
14 | initialState,
15 | {
16 | [setPending]: (state, { payload: actionName }) => {
17 | state[actionName] = true
18 | },
19 | [clearPendingHistory]: (state, { payload: { actionName, key } }) => {
20 | if (key !== NO_KEY) {
21 | if (state[actionName]?.[key]) {
22 | delete state[actionName][key]
23 | }
24 | } else {
25 | delete state[actionName]
26 | }
27 | },
28 | },
29 | [
30 | {
31 | matcher: ({ type }) => {
32 | return (
33 | getActionName(type) && (type.endsWith('/REQUEST') || type.endsWith('/SUCCESS') || type.endsWith('/FAILURE'))
34 | )
35 | },
36 | reducer(state, { meta, type }) {
37 | const actionName = getActionName(type)
38 | const isPending = type.endsWith('/REQUEST')
39 | const key = meta?.key
40 |
41 | if (key !== NO_KEY) {
42 | if (typeof state[actionName] !== 'object') state[actionName] = {}
43 | state[actionName][key] = isPending
44 | } else {
45 | state[actionName] = isPending
46 | }
47 | },
48 | },
49 | ]
50 | )
51 |
--------------------------------------------------------------------------------
/src/modules/token/api.js:
--------------------------------------------------------------------------------
1 | import { axios } from '@utils/http/client'
2 | import queryFilter from '@utils/http/queryFilter'
3 |
4 | import queryInfoData from './queryInfoData'
5 |
6 | export const requestUpdateToken = (data = {}) => {
7 | const queryData = queryInfoData['updateToken']
8 | const info = queryFilter({ queryData, originDataInfo: data })
9 | return axios[queryData.method](queryData.repaceAPI(), info)
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/token/index.js:
--------------------------------------------------------------------------------
1 | export * from './api'
2 | export { default as queryInfoData } from './queryInfoData'
3 |
--------------------------------------------------------------------------------
/src/modules/token/queryInfoData.js:
--------------------------------------------------------------------------------
1 | const queryInfoData = {
2 | /**
3 | * * JWT 갱신 POST
4 | * * /user/token/refresh/
5 | * * JWT 불필요
6 | */
7 | updateToken: {
8 | API: '/user/token/refresh/',
9 | method: 'post',
10 | bodyQuery: {
11 | refresh: '',
12 | },
13 | repaceAPI() {
14 | return this.API
15 | },
16 | },
17 |
18 | /**
19 | * * JWT 검사 POST
20 | * * /user/token/verify/
21 | * * JWT 불필요
22 | */
23 | checkToken: {
24 | API: '/user/token/verify/',
25 | method: 'post',
26 | bodyQuery: {
27 | token: '',
28 | },
29 | repaceAPI() {
30 | return this.API
31 | },
32 | },
33 | }
34 |
35 | export default queryInfoData
36 |
--------------------------------------------------------------------------------
/src/modules/ui/constants.js:
--------------------------------------------------------------------------------
1 | export const MODAL_NAME = {
2 | TERMS_MODAL: 'TERMS_MODAL',
3 | REMOVE_USER_ALERT_MODAL: 'REMOVE_USER_ALERT_MODAL',
4 | DELETE_CATEGORY_ALERT_MODAL: 'DELETE_CATEGORY_ALERT_MODAL',
5 | UPDATE_CATEGORY_MODAL: 'UPDATE_CATEGORY_MODAL',
6 | ADD_CATEGORY_MODAL: 'ADD_CATEGORY_MODAL',
7 | BOOKMARKS_MIGRATION_MODAL: 'BOOKMARKS_MIGRATION_MODAL',
8 | }
9 |
10 | export const DRAG = {
11 | DRAG_STATUS: 'drag',
12 | DROP_STATUS: 'drop',
13 | LINK: 'link',
14 | CATEGORY: 'category',
15 | }
16 |
17 | export const DROP_ZONE = {
18 | LINK_DROP_ZONE: 'LINK_DROP_ZONE',
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/ui/hooks/useDialog.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { openDialog, closeDialog, closeAllDialogs, uiSelector } from '@modules/ui'
6 |
7 | const useDialog = (type) => {
8 | const dispatch = useDispatch()
9 | const dialogs = useSelector(uiSelector.dialogs)
10 | const open = !!dialogs.find((dialog) => dialog.type === type)
11 |
12 | const toggle = useCallback(() => {
13 | if (open) {
14 | dispatch(closeDialog({ type }))
15 | } else {
16 | dispatch(openDialog({ type }))
17 | }
18 | }, [open, type, dispatch])
19 |
20 | const close = useCallback(() => {
21 | dispatch(closeDialog({ type }))
22 | }, [dispatch, type])
23 |
24 | const closeAll = useCallback(() => {
25 | if (dialogs.length) {
26 | dispatch(closeAllDialogs())
27 | }
28 | }, [dispatch, dialogs])
29 |
30 | return { open, toggle, close, closeAll }
31 | }
32 |
33 | export default useDialog
34 |
--------------------------------------------------------------------------------
/src/modules/ui/hooks/useDrag.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { UI, DRAG, setDrag, clearDrag } from '@modules/ui'
6 |
7 | const { DRAG_STATUS } = DRAG
8 |
9 | const useDrag = (type) => {
10 | const dispatch = useDispatch()
11 | const {
12 | type: dragType,
13 | status: dragStatus,
14 | listData,
15 | data,
16 | } = useSelector((state) => {
17 | return {
18 | type: state[UI].drag.type,
19 | status: state[UI].drag.status,
20 | ...state[UI].drag?.[type],
21 | }
22 | })
23 |
24 | const setDragData = useCallback(
25 | (dragData) => {
26 | if (Array.isArray(dragData)) {
27 | dispatch(
28 | setDrag({
29 | type,
30 | status: DRAG_STATUS,
31 | listData: dragData,
32 | })
33 | )
34 | } else {
35 | dispatch(
36 | setDrag({
37 | type,
38 | status: DRAG_STATUS,
39 | data: dragData,
40 | })
41 | )
42 | }
43 | },
44 | [type, dispatch]
45 | )
46 |
47 | const clearDragData = useCallback(() => {
48 | dispatch(clearDrag())
49 | }, [dispatch])
50 |
51 | return { dragType, dragStatus, listData, data, setDragData, clearDragData }
52 | }
53 |
54 | export default useDrag
55 |
--------------------------------------------------------------------------------
/src/modules/ui/hooks/useDropZone.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { openDropZone, closeDropZone, closeAllDropZones, uiSelector } from '@modules/ui'
6 |
7 | const useDropZone = (type) => {
8 | const dispatch = useDispatch()
9 | const dropZones = useSelector(uiSelector.dropZones)
10 | const open = !!dropZones.find((dropZone) => dropZone.type === type)
11 |
12 | const toggle = useCallback(() => {
13 | if (open) dispatch(closeDropZone({ type }))
14 | else dispatch(openDropZone({ type }))
15 | }, [open, type, dispatch])
16 |
17 | const close = useCallback(() => {
18 | dispatch(closeDropZone({ type }))
19 | }, [dispatch, type])
20 |
21 | const closeAll = useCallback(() => {
22 | if (dropZones.length) {
23 | dispatch(closeAllDropZones())
24 | }
25 | }, [dispatch, dropZones])
26 |
27 | return { open, toggle, close, closeAll }
28 | }
29 |
30 | export default useDropZone
31 |
--------------------------------------------------------------------------------
/src/modules/ui/hooks/useToast.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { setToast, uiSelector } from '@modules/ui'
6 |
7 | const useToast = () => {
8 | const dispatch = useDispatch()
9 | const { open, type, message } = useSelector(uiSelector.toast)
10 |
11 | const openToast = useCallback(
12 | ({ type, message }) => {
13 | dispatch(setToast({ open: true, type, message }))
14 | },
15 | [dispatch]
16 | )
17 |
18 | const close = useCallback(() => {
19 | dispatch(setToast({ open: false }))
20 | }, [dispatch])
21 |
22 | return { open, type, message, openToast, close }
23 | }
24 |
25 | export default useToast
26 |
--------------------------------------------------------------------------------
/src/modules/ui/index.js:
--------------------------------------------------------------------------------
1 | export * from './constants'
2 | export { default as useDialog } from './hooks/useDialog'
3 | export { default as useDrag } from './hooks/useDrag'
4 | export { default as useDropZone } from './hooks/useDropZone'
5 | export { default as useToast } from './hooks/useToast'
6 | export * from './reducer'
7 |
--------------------------------------------------------------------------------
/src/modules/ui/reducer.js:
--------------------------------------------------------------------------------
1 | import { createAction, createReducer } from '@reduxjs/toolkit'
2 |
3 | export const UI = 'UI'
4 |
5 | export const setIsMobile = createAction(`${UI}/SET_IS_MOBILE`)
6 | export const openDialog = createAction(`${UI}/OPEN_DIALOG`)
7 | export const closeDialog = createAction(`${UI}/CLOSE_DIALOG`)
8 | export const closeAllDialogs = createAction(`${UI}/CLOSE_ALL_DIALOGS`)
9 | export const setToast = createAction(`${UI}/SET_TOAST`)
10 | export const openDropZone = createAction(`${UI}/OPEN_DROP_ZONE`)
11 | export const closeDropZone = createAction(`${UI}/CLOSE_DROP_ZONE`)
12 | export const closeAllDropZones = createAction(`${UI}/CLOSE_ALL_DROP_ZONES`)
13 | export const setDrag = createAction(`${UI}/SET_DRAG`)
14 | export const clearDrag = createAction(`${UI}/CLEAR_DRAG`)
15 | export const appBarInversionChangeState = createAction(`${UI}/APP_BAR_INVERSION_CHANGE_STATE`)
16 |
17 | // Reducer
18 | const initialState = {
19 | dialogs: [],
20 | toast: {
21 | open: false,
22 | type: 'success',
23 | message: '',
24 | },
25 | dropZones: [],
26 | drag: {
27 | type: '',
28 | status: '',
29 | link: {
30 | listData: [],
31 | data: {
32 | id: undefined, // number
33 | path: '',
34 | },
35 | },
36 | category: {
37 | listData: [],
38 | data: {
39 | id: undefined, // number
40 | name: '',
41 | order: undefined, // number
42 | is_favorited: undefined, // true | false
43 | dragFinished: undefined, // true | false
44 | },
45 | },
46 | },
47 | isAppBarInversion: false,
48 | }
49 | export const uiReducer = createReducer(initialState, {
50 | [openDialog]: (state, { payload }) => {
51 | if (!state.dialogs.find((dialog) => dialog.type === payload.type)) {
52 | state.dialogs.push(payload)
53 | }
54 | },
55 | [closeDialog]: (state, { payload }) => {
56 | state.dialogs = state.dialogs.filter((dialog) => {
57 | if (payload.meta?.key) {
58 | return dialog.meta?.key !== payload.meta?.key
59 | }
60 | return dialog.type !== payload.type
61 | })
62 | },
63 | [closeAllDialogs]: (state) => {
64 | state.dialogs = initialState.dialogs
65 | },
66 | [setToast]: (state, { payload }) => {
67 | state.toast = { ...state.toast, ...payload }
68 | },
69 | [openDropZone]: (state, { payload }) => {
70 | if (!state.dropZones.find((dropZone) => dropZone.type === payload.type)) {
71 | state.dropZones.push(payload)
72 | }
73 | },
74 | [closeDropZone]: (state, { payload }) => {
75 | state.dropZones = state.dropZones.filter((dropZone) => {
76 | if (payload.meta?.key) {
77 | return dropZone.meta?.key !== payload.meta?.key
78 | }
79 | return dropZone.type !== payload.type
80 | })
81 | },
82 | [closeAllDropZones]: (state) => {
83 | state.dropZones = initialState.dropZones
84 | },
85 | [setDrag]: (state, { payload: { type, status, ...props } }) => {
86 | state.drag.type = type
87 | state.drag.status = status
88 | state.drag[type] = { ...state.drag[type], ...props }
89 | },
90 | [clearDrag]: (state) => {
91 | state.drag = initialState.drag
92 | },
93 | [appBarInversionChangeState]: (state, { payload }) => {
94 | state.isAppBarInversion = payload
95 | },
96 | })
97 |
98 | // Select
99 | export const uiSelector = {
100 | dialogs: (state) => state[UI].dialogs,
101 | toast: (state) => state[UI].toast,
102 | dropZones: (state) => state[UI].dropZones,
103 | drag: (state) => state[UI].drag,
104 | isAppBarInversion: (state) => state[UI].isAppBarInversion,
105 | }
106 |
--------------------------------------------------------------------------------
/src/modules/user/api.js:
--------------------------------------------------------------------------------
1 | import { axios } from '@utils/http/client'
2 | import queryFilter from '@utils/http/queryFilter'
3 |
4 | import queryInfoData from './queryInfoData'
5 |
6 | export const requestNregister = (data = {}) => {
7 | const queryData = queryInfoData['nRegister']
8 | const info = queryFilter({ queryData, originDataInfo: data })
9 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
10 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
11 | }
12 |
13 | export const requestNlogin = (data = {}) => {
14 | const queryData = queryInfoData['nLogin']
15 | const info = queryFilter({ queryData, originDataInfo: data })
16 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
17 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
18 | }
19 |
20 | export const requestGregister = (data = {}) => {
21 | const queryData = queryInfoData['gRegister']
22 | const info = queryFilter({ queryData, originDataInfo: data })
23 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
24 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
25 | }
26 |
27 | export const requestGLogin = (data = {}) => {
28 | const queryData = queryInfoData['gLogin']
29 | const info = queryFilter({ queryData, originDataInfo: data })
30 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
31 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
32 | }
33 |
34 | export const requestUserRead = (data = {}) => {
35 | const queryData = queryInfoData['userRead']
36 | const info = queryFilter({ queryData, originDataInfo: data })
37 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
38 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
39 | }
40 |
41 | export const requestUserModiify = (data = {}) => {
42 | const queryData = queryInfoData['userModiify']
43 | const info = queryFilter({ queryData, originDataInfo: data })
44 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
45 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
46 | }
47 |
48 | export const requestUserRemove = (data = {}) => {
49 | const queryData = queryInfoData['userRemove']
50 | const info = queryFilter({ queryData, originDataInfo: data })
51 | const urlData = queryFilter({ queryData, queryType: 'urlQuery', originDataInfo: data })
52 | return axios[queryData.method](queryData.replaceAPI({ ...urlData }), info)
53 | }
54 |
--------------------------------------------------------------------------------
/src/modules/user/hooks/useUser.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | import { ERROR } from '@modules/error'
6 | import { INIT, PENDING } from '@modules/pending'
7 | import { userRead, userSelector } from '@modules/user'
8 |
9 | const useUserData = () => {
10 | const dispatch = useDispatch()
11 | const data = useSelector(userSelector.data)
12 | const pending = useSelector((state) => state[PENDING][userRead.TYPE])
13 | const error = useSelector((state) => state[ERROR][userRead.TYPE])
14 |
15 | const reload = () => {
16 | dispatch(userRead.request())
17 | }
18 |
19 | useEffect(() => {
20 | if (pending === INIT) {
21 | dispatch(userRead.request())
22 | }
23 | }, [dispatch, pending])
24 |
25 | return { pending, error, data, reload }
26 | }
27 |
28 | export default useUserData
29 |
--------------------------------------------------------------------------------
/src/modules/user/index.js:
--------------------------------------------------------------------------------
1 | export * from './api'
2 | export { default as useUserData } from './hooks/useUser'
3 | export * from './reducer'
4 | export * from './saga'
5 |
--------------------------------------------------------------------------------
/src/modules/user/queryInfoData.js:
--------------------------------------------------------------------------------
1 | const queryInfoData = {
2 | /**
3 | * * 일반 회원가입 POST
4 | * * /user/sign-up/
5 | * * Authorization: JWT 불필요
6 | */
7 | nRegister: {
8 | API: '/user/sign-up/',
9 | method: 'post',
10 | bodyQuery: {
11 | sign_up_type: '', // * (normal default [server])
12 | email: '',
13 | username: '',
14 | password: '',
15 | },
16 | urlQuery: {},
17 | replaceAPI() {
18 | return this.API
19 | },
20 | },
21 |
22 | /**
23 | * * 일반 로그인 POST
24 | * * /user/sign-in/
25 | * * Authorization: JWT 불필요
26 | */
27 | nLogin: {
28 | API: '/user/sign-in/',
29 | method: 'post',
30 | bodyQuery: {
31 | email: '',
32 | password: '',
33 | },
34 | urlQuery: {},
35 | replaceAPI() {
36 | return this.API
37 | },
38 | },
39 |
40 | /**
41 | * * 구글 회원가입 POST
42 | * * /user/google/sign-up/
43 | * * Authorization: JWT 불필요
44 | */
45 | gRegister: {
46 | API: 'user/google/sign-up/',
47 | method: 'post',
48 | bodyQuery: {
49 | token: '',
50 | },
51 | urlQuery: {},
52 | replaceAPI() {
53 | return this.API
54 | },
55 | },
56 |
57 | /**
58 | * * 구글 로그인 POST
59 | * * /user/google/sign-in/
60 | * * Authorization: JWT 불필요
61 | * * email, password 불필요
62 | */
63 | gLogin: {
64 | API: '/user/google/sign-in/',
65 | method: 'post',
66 | bodyQuery: {
67 | token: '',
68 | },
69 | urlQuery: {},
70 | replaceAPI() {
71 | return this.API
72 | },
73 | },
74 |
75 | /**
76 | * * 로그아웃 POST
77 | * * /user/sign-out/
78 | * * Authorization: JWT 불필요
79 | * * email, password 불필요
80 | */
81 | logout: {
82 | API: '/user/sign-out/',
83 | method: 'post',
84 | bodyQuery: {},
85 | urlQuery: {},
86 | replaceAPI() {
87 | return this.API
88 | },
89 | },
90 |
91 | /**
92 | * * 회원정보 조회 GET
93 | * * /user/{userId}/ (option)
94 | * * Authorization: JWT 필요
95 | */
96 | userRead: {
97 | API: 'user/{userId}/',
98 | method: 'get',
99 | bodyQuery: {},
100 | urlQuery: {
101 | userId: '',
102 | },
103 | replaceAPI({ userId }) {
104 | return this.API.replace('{userId}/', userId ? `${userId}/` : '')
105 | },
106 | },
107 |
108 | /**
109 | * * 회원정보 부분 수정 PATCH
110 | * * /user/{userId}/ (option)
111 | * * Authorization: JWT 필요
112 | */
113 | userModiify: {
114 | API: 'user/{userId}/',
115 | method: 'patch',
116 | bodyQuery: {
117 | // "sign_up_type": "",
118 | // "email": "",
119 | username: '',
120 | password: '',
121 | },
122 | urlQuery: {
123 | userId: '',
124 | },
125 | replaceAPI({ userId }) {
126 | return this.API.replace('{userId}/', userId ? `${userId}/` : '')
127 | },
128 | },
129 |
130 | /**
131 | * * 회원정보 삭제 DELETE
132 | * * /user/{userId}/ (option)
133 | * * Authorization: JWT 필요
134 | */
135 | userRemove: {
136 | API: 'user/{userId}/',
137 | method: 'delete',
138 | bodyQuery: {},
139 | urlQuery: {
140 | userId: '',
141 | },
142 | replaceAPI({ userId }) {
143 | return this.API.replace('{userId}/', userId ? `${userId}/` : '')
144 | },
145 | },
146 | }
147 |
148 | export default queryInfoData
149 |
--------------------------------------------------------------------------------
/src/modules/user/reducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 |
3 | import { createRequestAction, createRequestThunk } from '../helpers'
4 |
5 | export const USER = 'USER'
6 |
7 | export const userRegister = createRequestAction(`${USER}/REGISTER`)
8 | export const userRegisterThunk = createRequestThunk(userRegister)
9 |
10 | export const userLogin = createRequestAction(`${USER}/LOGIN`)
11 | export const userLoginThunk = createRequestThunk(userLogin)
12 |
13 | export const userLogout = createRequestAction(`${USER}/LOGOUT`)
14 | export const userLogoutThunk = createRequestThunk(userLogout)
15 |
16 | export const userGregister = createRequestAction(`${USER}/G_REGISTER`)
17 | export const userGregisterThunk = createRequestThunk(userGregister)
18 |
19 | export const userGlogin = createRequestAction(`${USER}/G_LOGIN`)
20 | export const userGloginThunk = createRequestThunk(userGlogin)
21 |
22 | export const userRead = createRequestAction(`${USER}/READ`)
23 | export const userReadThunk = createRequestThunk(userRead)
24 |
25 | export const userModify = createRequestAction(`${USER}/MODIFY`)
26 | export const userModifyThunk = createRequestThunk(userModify)
27 |
28 | export const userRemove = createRequestAction(`${USER}/REMOVE`)
29 | export const userRemoveThunk = createRequestThunk(userRemove)
30 |
31 | // Reducer
32 | const initialState = {
33 | data: {},
34 | }
35 | export const userReducer = createReducer(initialState, {
36 | [userLogin.SUCCESS]: (state, { payload }) => {
37 | state.data = payload
38 | },
39 | [userGlogin.SUCCESS]: (state, { payload }) => {
40 | state.data = payload
41 | },
42 | [userRead.SUCCESS]: (state, { payload }) => {
43 | state.data = payload
44 | },
45 | })
46 |
47 | // Select
48 | export const userSelector = {
49 | data: (state) => state[USER].data,
50 | }
51 |
--------------------------------------------------------------------------------
/src/modules/user/saga.js:
--------------------------------------------------------------------------------
1 | import { call, takeLatest } from 'redux-saga/effects'
2 |
3 | import { getAuthToken, removeCachedAuthToken } from '@utils/chromeApis/OAuth'
4 | import { REMOVE_TOKEN, UPDATE_TOKEN } from '@utils/chromeApis/onMessage'
5 | import { sendMessage } from '@utils/chromeApis/sendMessage'
6 | import { setAccessToken, removeAccessToken } from '@utils/http/auth'
7 |
8 | import { createRequestSaga } from '../helpers'
9 | import * as api from './api'
10 | import {
11 | userRegister,
12 | userLogin,
13 | userLogout,
14 | userGregister,
15 | userGlogin,
16 | userRead,
17 | userModify,
18 | userRemove,
19 | } from './reducer'
20 |
21 | const watchUserRegister = createRequestSaga(userRegister, function* (action) {
22 | const { data } = yield call(api.requestNregister, action.payload)
23 | yield call(setAccessToken, data.token)
24 | yield call(sendMessage, { message: UPDATE_TOKEN })
25 | return data
26 | })
27 |
28 | const watchUserLogin = createRequestSaga(userLogin, function* (action) {
29 | const { data } = yield call(api.requestNlogin, action.payload)
30 | yield call(setAccessToken, data.token)
31 | yield call(sendMessage, { message: UPDATE_TOKEN })
32 | return data
33 | })
34 |
35 | const watchUserLogout = createRequestSaga(userLogout, function* (_action) {
36 | yield call(removeAccessToken)
37 | yield call(sendMessage, { message: REMOVE_TOKEN })
38 | })
39 |
40 | const watchUserGregister = createRequestSaga(userGregister, function* (_action) {
41 | let token
42 | try {
43 | token = yield call(getAuthToken)
44 | const { data } = yield call(api.requestGregister, { token })
45 | yield call(setAccessToken, data.token)
46 | yield call(sendMessage, { message: UPDATE_TOKEN })
47 | return data
48 | } catch (error) {
49 | if (token && error.response?.status >= 400) {
50 | yield call(removeCachedAuthToken, token)
51 | }
52 | throw error
53 | }
54 | })
55 |
56 | const watchUserGlogin = createRequestSaga(userGlogin, function* (_action) {
57 | let token
58 | try {
59 | token = yield call(getAuthToken)
60 | const { data } = yield call(api.requestGLogin, { token })
61 | yield call(setAccessToken, data.token)
62 | yield call(sendMessage, { message: UPDATE_TOKEN })
63 | return data
64 | } catch (error) {
65 | if (token && error.response?.status >= 400) {
66 | yield call(removeCachedAuthToken, token)
67 | }
68 | throw error
69 | }
70 | })
71 |
72 | const watchUserRead = createRequestSaga(userRead, function* (action) {
73 | const { data } = yield call(api.requestUserRead, action.payload, null)
74 | return data
75 | })
76 |
77 | const watchUserModify = createRequestSaga(userModify, function* (action) {
78 | const { data } = yield call(api.requestUserModiify, action.payload, null)
79 | return data
80 | })
81 |
82 | const watchUserRemove = createRequestSaga(userRemove, function* (action) {
83 | const { data } = yield call(api.requestUserRemove, action.payload)
84 | yield call(removeAccessToken)
85 | return data
86 | })
87 |
88 | export function* userSaga() {
89 | yield takeLatest(userRegister.REQUEST, watchUserRegister)
90 | yield takeLatest(userLogin.REQUEST, watchUserLogin)
91 | yield takeLatest(userLogout.REQUEST, watchUserLogout)
92 | yield takeLatest(userGregister.REQUEST, watchUserGregister)
93 | yield takeLatest(userGlogin.REQUEST, watchUserGlogin)
94 |
95 | yield takeLatest(userRead.REQUEST, watchUserRead)
96 | yield takeLatest(userModify.REQUEST, watchUserModify)
97 | yield takeLatest(userRemove.REQUEST, watchUserRemove)
98 | }
99 |
--------------------------------------------------------------------------------
/src/setting.js:
--------------------------------------------------------------------------------
1 | export const STORAGE = localStorage
2 | export const SERVER_TOKEN = 'JWT'
3 | export const SERVER_TOKEN_NOT_VALID = 'token_not_valid'
4 | export const LOGIN_REQUIRED_VALID = 'authentication credentials'
5 | export const TOKEN_NAME = 'accessToken'
6 | export const UPDATE_TOKEN_NAME = 'refreshToken'
7 |
--------------------------------------------------------------------------------
/src/utils/chromeApis/OAuth.js:
--------------------------------------------------------------------------------
1 | export function getAuthToken() {
2 | return new Promise((resolve, reject) => {
3 | const idCheck = chrome.runtime?.id
4 | if (!idCheck) reject({ message: 'is not defined getAuthToken' })
5 | else {
6 | chrome.identity.getAuthToken({ interactive: true }, (token) => {
7 | if (token) resolve(token)
8 | })
9 | }
10 | })
11 | }
12 |
13 | export function removeCachedAuthToken(token) {
14 | return new Promise((resolve, reject) => {
15 | const idCheck = chrome.runtime?.id
16 | if (!idCheck) reject({ message: 'is not defined removeCachedAuthToken' })
17 | else chrome.identity.removeCachedAuthToken({ token }, () => resolve())
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/chromeApis/bookmarks.js:
--------------------------------------------------------------------------------
1 | export function getBookmarks() {
2 | chrome.bookmarks.getTree((data) => console.log(data))
3 | }
4 |
5 | const onBookmarkFolderAdded = (newFolder, fn) => {
6 | fn(newFolder.id)
7 | }
8 |
9 | export function createBookmark(data) {
10 | const { id, title, url } = data
11 | chrome.bookmarks.create({
12 | parentId: id,
13 | title,
14 | url,
15 | })
16 | }
17 |
18 | export function createBookmarkFolder(data, fn) {
19 | const { id, title } = data
20 | const idCheck = chrome.runtime?.id
21 | if (idCheck) {
22 | chrome.bookmarks.create(
23 | {
24 | parentId: id,
25 | title: title,
26 | },
27 | (newFolder) => onBookmarkFolderAdded(newFolder, fn)
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/chromeApis/history.js:
--------------------------------------------------------------------------------
1 | /**
2 | * * Get Chrome History List information -> promise Pattern
3 | * @param {String} text String
4 | * @param {Date} startTime Date
5 | * @param {Date} endTime Date
6 | * @param {Number} maxResults Number
7 | * @returns {Object} search
8 | * * first : (true|false) 해당 날짜의 첫번째 게시글
9 | * * id: "11081" (history Id)
10 | * * favicon: "https://www.google.com/s2/favicons?domain=${url}"
11 | * * lastVisitTime: 1588933029447.23
12 | * * title: "React App"
13 | * * typedCount: 0 (사용자가 주소(typing in the address)를 입력하여 이 페이지를 탐색 한 횟수)
14 | * * url: "chrome-extension://fljjldbbgojlhkhamhcgamibbjlincej/index.html?index=1"
15 | * * hostName: "chrome-extension://fljjldbbgojlhkhamhcgamibbjlincej/index.html"
16 | * * visitCount: 24 (사용자가이 페이지를 탐색(navigated to this page) 한 횟수)
17 | */
18 | export function getHistoryList({ text, startTime = 0, endTime = Date.now(), maxResults }) {
19 | return new Promise((resolve, _reject) => {
20 | const idCheck = chrome.runtime?.id
21 | if (!idCheck) resolve(urlTempList)
22 | else {
23 | chrome.history.search({ text, startTime, endTime, maxResults }, (historyList) => {
24 | historyList = historyList.filter(
25 | (history) => startTime <= history.lastVisitTime && history.lastVisitTime <= endTime
26 | )
27 | historyList.sort((preHistory, curHistory) => curHistory.lastVisitTime - preHistory.lastVisitTime)
28 | let prevDate = ''
29 | const resultList = historyList.map(function (history) {
30 | let curDate = new Date(history.lastVisitTime).toLocaleDateString()
31 | let first = false
32 | if (curDate !== prevDate) {
33 | first = true
34 | prevDate = curDate
35 | }
36 | const url = document.createElement('a')
37 | url.href = history.url
38 | return {
39 | ...history,
40 | first,
41 | hostName: url.hostname,
42 | favicon: `https://www.google.com/s2/favicons?domain=${url.hostname}`,
43 | }
44 | })
45 | resolve(resultList)
46 | })
47 | }
48 | })
49 | }
50 |
51 | const urlTempList = [
52 | {
53 | id: '1',
54 | lastVisitTime: 1588933029447.23,
55 | title: 'React App',
56 | typedCount: 0,
57 | hostName: 'https://www.naver.com',
58 | visitCount: 24,
59 | },
60 | {
61 | id: '2',
62 | lastVisitTime: 1588933029447.23,
63 | title: 'React App',
64 | typedCount: 0,
65 | hostName: 'https://www.naver.com',
66 | visitCount: 24,
67 | },
68 | {
69 | id: '3',
70 | lastVisitTime: 1588933029447.23,
71 | title: 'React App',
72 | typedCount: 0,
73 | hostName: 'https://www.naver.com',
74 | visitCount: 24,
75 | },
76 | {
77 | id: '4',
78 | lastVisitTime: 1588933029447.23,
79 | title: 'React App',
80 | typedCount: 0,
81 | hostName: 'https://www.naver.com',
82 | visitCount: 24,
83 | },
84 | {
85 | id: '5',
86 | lastVisitTime: 1588933029447.23,
87 | title: 'React App',
88 | typedCount: 0,
89 | hostName: 'https://www.naver.com',
90 | visitCount: 24,
91 | },
92 | ]
93 |
--------------------------------------------------------------------------------
/src/utils/chromeApis/onMessage.js:
--------------------------------------------------------------------------------
1 | export function onMessage(callback) {
2 | const idCheck = chrome.runtime?.id
3 | if (!idCheck) return
4 | else chrome.runtime?.onMessage.addListener(callback)
5 | }
6 |
7 | export const UPDATE_TOKEN = 'UPDATE_TOKEN'
8 | export const REMOVE_TOKEN = 'REMOVE_TOKEN'
9 |
--------------------------------------------------------------------------------
/src/utils/chromeApis/sendMessage.js:
--------------------------------------------------------------------------------
1 | export function sendMessage(data) {
2 | const idCheck = chrome.runtime?.id
3 | if (!idCheck) return
4 | else chrome.runtime.sendMessage(data)
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/chromeApis/tab.js:
--------------------------------------------------------------------------------
1 | export function createTab(url) {
2 | const idCheck = chrome.runtime?.id
3 | if (!idCheck) window.open(url)
4 | else chrome.tabs.create({ selected: true, url })
5 | }
6 |
7 | export function createTabList(urlList) {
8 | const idCheck = chrome.runtime?.id
9 | if (!idCheck) urlList.forEach((url) => window.open(url))
10 | else urlList.forEach((url) => chrome.tabs.create({ selected: true, url }))
11 | }
12 |
13 | export function getTabsQuery() {
14 | return new Promise((resolve, reject) => {
15 | const idCheck = chrome.runtime?.id
16 | if (!idCheck) reject({ message: 'is not defined query of tabs' })
17 | else chrome.tabs.query({ active: true, windowId: chrome.windows?.WINDOW_ID_CURRENT }, (tabs) => resolve(tabs))
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/copyLink.js:
--------------------------------------------------------------------------------
1 | async function copyLink(url) {
2 | try {
3 | await navigator.clipboard.writeText(url)
4 | return true
5 | } catch (e) {
6 | return false
7 | }
8 | }
9 |
10 | export default copyLink
11 |
--------------------------------------------------------------------------------
/src/utils/filter.js:
--------------------------------------------------------------------------------
1 | export function isObjValueEmpty(obj) {
2 | return Object.entries(obj).filter(([_, value]) => !value).length === Object.entries(obj).length
3 | }
4 |
5 | export function isObjKeysEmpty(obj) {
6 | return !!Object.keys(obj).length
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/ga.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga'
2 |
3 | const TRACKING_ID = 'UA-207149982-2'
4 |
5 | export const initGA = () => {
6 | const isDev = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'
7 | ReactGA.initialize(TRACKING_ID, { debug: isDev })
8 | }
9 |
10 | export const GAPageview = (path) => {
11 | ReactGA.set({ page: path })
12 | ReactGA.pageview(path)
13 | }
14 |
15 | export const GAEvent = (category, action, label) => {
16 | ReactGA.event({
17 | category,
18 | action,
19 | label,
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/http/auth.js:
--------------------------------------------------------------------------------
1 | import { STORAGE, TOKEN_NAME, UPDATE_TOKEN_NAME } from 'setting'
2 |
3 | export function getAccessToken() {
4 | return STORAGE.getItem(TOKEN_NAME) || ''
5 | }
6 |
7 | export function getRefreshToken() {
8 | return STORAGE.getItem(UPDATE_TOKEN_NAME) || ''
9 | }
10 |
11 | export function setAccessToken(token = {}) {
12 | const accessToken = token.access || ''
13 | const refreshToken = token.refresh || ''
14 | STORAGE.setItem(TOKEN_NAME, accessToken || STORAGE.getItem(TOKEN_NAME))
15 | STORAGE.setItem(UPDATE_TOKEN_NAME, refreshToken || STORAGE.getItem(UPDATE_TOKEN_NAME))
16 | }
17 |
18 | export function removeAccessToken() {
19 | STORAGE.setItem(TOKEN_NAME, '')
20 | STORAGE.setItem(UPDATE_TOKEN_NAME, '')
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/http/client.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios'
2 | import { SERVER_TOKEN, SERVER_TOKEN_NOT_VALID, LOGIN_REQUIRED_VALID } from 'setting'
3 |
4 | import { requestUpdateToken, queryInfoData } from '@modules/token'
5 |
6 | import { UPDATE_TOKEN } from '../chromeApis/onMessage'
7 | import { sendMessage } from '../chromeApis/sendMessage'
8 | import { getAccessToken, getRefreshToken, setAccessToken, removeAccessToken } from './auth'
9 | import queryFilter from './queryFilter'
10 |
11 | /**
12 | * * Basic
13 | * * Security scheme type: HTTP
14 | * * HTTP Authorization Scheme basic
15 |
16 | * * JWT
17 | * * Security scheme type: API Key
18 | * * Header parameter name: Authorization
19 | */
20 |
21 | export const axiosSetting = {
22 | scheme: 'https',
23 | host: 'urlink-official.com',
24 | api: '/api/v1',
25 | port: '',
26 | server: function () {
27 | return (this.scheme ? this.scheme + ':' : '') + '//' + this.host + this.api + (this.port ? ':' + this.port : '')
28 | },
29 | redirectPage: () => {
30 | window.location.reload()
31 | },
32 | }
33 |
34 | export const axios = Axios.create({
35 | baseURL: axiosSetting.server(),
36 | })
37 |
38 | // Add a request interceptor
39 | axios.interceptors.request.use(
40 | (config) => {
41 | const accessToken = getAccessToken()
42 | if (accessToken) config.headers.Authorization = `${SERVER_TOKEN} ${accessToken}`
43 | return config
44 | },
45 | (error) => {
46 | return Promise.reject(error)
47 | }
48 | )
49 |
50 | // Add a response interceptor
51 | axios.interceptors.response.use(
52 | (response) => response,
53 | async (error) => {
54 | if (!error.response) {
55 | error.response = { data: { message: '네트워크 연결이 끊어져 있습니다.' } }
56 | }
57 | const UPDATE_TOKEN_API = queryInfoData['updateToken'].API
58 | const CHECK_TOKEN_API = queryInfoData['checkToken'].API
59 |
60 | const status = error.response.status || ''
61 | const response = error.response.data || {}
62 | const originalRequest = error.config || {}
63 | const url = originalRequest.url || ''
64 |
65 | // API 호출 시, accessToken 만료일 때
66 | if (
67 | status === 401 &&
68 | response.code === SERVER_TOKEN_NOT_VALID &&
69 | url.indexOf(UPDATE_TOKEN_API) === -1 // UPDATE TOKEN 요청이 아닐 때
70 | ) {
71 | try {
72 | const refresh = getRefreshToken()
73 | if (!refresh) throw new Error()
74 |
75 | const response = await requestUpdateToken({ refresh })
76 | if (!response.data) throw new Error()
77 | else {
78 | setAccessToken(response.data)
79 | sendMessage({ message: UPDATE_TOKEN })
80 | // 만약 이전에 보냈던 url이 TOKEN 검사 요청이었을 때
81 | if (url.indexOf(CHECK_TOKEN_API) > -1) {
82 | const token = getAccessToken()
83 | const queryData = queryInfoData['updateToken']
84 | const checkToken = queryFilter({ queryData, originDataInfo: { token } })
85 | originalRequest.data = checkToken
86 | }
87 | return axios.request(originalRequest)
88 | }
89 | } catch (error) {
90 | console.dir(error)
91 | removeAccessToken()
92 | axiosSetting.redirectPage() // token_not_valid login => go login!!
93 | }
94 | }
95 | // login 필수
96 | else if (status === 401 && response.detail?.indexOf(LOGIN_REQUIRED_VALID) > -1) {
97 | removeAccessToken()
98 | axiosSetting.redirectPage() // no authentication => go login!!
99 | }
100 | // 그 외는 서버에서 내리는 에러
101 | else return Promise.reject(error)
102 | }
103 | )
104 |
--------------------------------------------------------------------------------
/src/utils/http/queryFilter.js:
--------------------------------------------------------------------------------
1 | function queryFilter({ queryData, queryType = 'bodyQuery', originDataInfo }) {
2 | let dataInfo = {}
3 | let isFormData = false
4 | const dataKeys = Object.keys(queryData[queryType])
5 | if (originDataInfo.toString().match('FormData')) {
6 | dataInfo = new FormData()
7 | isFormData = true
8 | }
9 |
10 | if (isFormData) {
11 | for (const [key, value] of originDataInfo.entries()) {
12 | if (dataKeys.includes(key)) dataInfo.append(key, value)
13 | }
14 | } else {
15 | Object.entries(originDataInfo).forEach(([key, value]) => {
16 | if (dataKeys.includes(key)) dataInfo[key] = value
17 | })
18 | }
19 |
20 | if (isFormData && queryType === 'urlQuery') return Object.fromEntries(dataInfo)
21 | else return dataInfo
22 | }
23 |
24 | export default queryFilter
25 |
--------------------------------------------------------------------------------
/src/utils/http/ws.js:
--------------------------------------------------------------------------------
1 | import { getAccessToken } from './auth'
2 | import { axiosSetting } from './client'
3 |
4 | export const SOCKET_READY_STATE = {
5 | OPEN: 1,
6 | CLOSED: 3,
7 | }
8 |
9 | export const socketInfoData = {
10 | host: `wss://${axiosSetting.host}/ws/connection/?token={token}`,
11 | replaceWS(token) {
12 | return this.host.replace('{token}', token)
13 | },
14 | }
15 |
16 | class AlarmWS {
17 | constructor() {
18 | this.connectionRetry = 1
19 | this.ws = null
20 | this.connectionRetry = 1
21 | }
22 |
23 | onConnection(settingWs = {}) {
24 | this.ws = new WebSocket(socketInfoData.replaceWS(getAccessToken()))
25 | const { onmessage, onerror, onclose } = settingWs
26 | this.ws.onmessage = onmessage
27 | this.ws.onerror = onerror
28 | this.ws.onclose = onclose
29 | return this
30 | }
31 |
32 | onClose() {
33 | if (!this.ws) return
34 | this.ws.close()
35 | return this
36 | }
37 |
38 | setOnmessage(callback) {
39 | try {
40 | if (!this.ws) this.onConnection()
41 | if (callback && typeof callback === 'function') this.ws.onmessage = callback
42 | else {
43 | this.ws.onmessage = (e) => {
44 | const { data } = JSON.parse(e.data)
45 | console.log(data)
46 | }
47 | }
48 | return this
49 | } catch (error) {
50 | console.error(error)
51 | }
52 | }
53 |
54 | setOnerror(callback, noRequestConnection = false) {
55 | try {
56 | if (!this.ws) return
57 | if (noRequestConnection && callback && typeof callback === 'function') this.ws.onerror = callback
58 | else {
59 | this.ws.onerror = async (event) => {
60 | if (callback && typeof callback === 'function') callback(event)
61 | if (this.ws.readyState === SOCKET_READY_STATE.CLOSED && this.connectionRetry <= 10) {
62 | setTimeout(() => {
63 | this.onConnection(this.ws)
64 | this.connectionRetry++
65 | }, 2000)
66 | }
67 | }
68 | }
69 | return this
70 | } catch (error) {
71 | console.error(error)
72 | }
73 | }
74 | }
75 |
76 | export const alarmSocket = new AlarmWS()
77 |
--------------------------------------------------------------------------------