├── .circleci
└── config.yml
├── .dockerignore
├── .eslintrc.js
├── .gitignore
├── .mergify.yml
├── .stylelintrc
├── .svgo.yml
├── .svgrrc.js
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── docker-compose.yml
├── docker
└── run-dev.sh
├── package.json
├── prettier.config.js
├── settings
├── dev.json
├── local.json
├── production.json
└── staging.json
├── src
├── App.tsx
├── Routes.jsx
├── ScrollManager.jsx
├── TrackManager.jsx
├── api
│ ├── actions.js
│ ├── api.js
│ ├── constants.js
│ ├── index.js
│ ├── interceptor.js
│ └── middleware.js
├── components
│ ├── ActionBar
│ │ ├── ActionBar.jsx
│ │ ├── ActionButton.jsx
│ │ ├── constants.js
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── BookDownLoader.tsx
│ ├── Books
│ │ ├── BookItem.jsx
│ │ ├── Disabled.jsx
│ │ ├── EmptyLandscapeBook.jsx
│ │ ├── FullButton.jsx
│ │ ├── index.jsx
│ │ └── refineBookData.jsx
│ ├── BooksWrapper
│ │ └── index.jsx
│ ├── BottomActionBar
│ │ ├── BottomActionBar.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Confirm
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Dialog
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Editable.tsx
│ ├── EditingBar
│ │ ├── SelectAllButton.tsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Empty
│ │ ├── EmptyShelves.jsx
│ │ └── index.jsx
│ ├── Error
│ │ ├── BookError.jsx
│ │ ├── InternalError.jsx
│ │ ├── NotFoundError.jsx
│ │ ├── PageLoadError.jsx
│ │ ├── base
│ │ │ ├── ComponentError.jsx
│ │ │ ├── ServiceError.jsx
│ │ │ └── serviceErrorStyles.js
│ │ └── index.jsx
│ ├── ErrorBoundary
│ │ └── index.jsx
│ ├── Filler.tsx
│ ├── FixedToolbarView.jsx
│ ├── FlexBar
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── FullScreenLoading.jsx
│ ├── HorizontalRuler.jsx
│ ├── IconButton.jsx
│ ├── LoadingSpinner.jsx
│ ├── Maintenance
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Modal
│ │ ├── FilterModal.jsx
│ │ ├── Modal.jsx
│ │ ├── ModalBackground.jsx
│ │ ├── ModalItem.jsx
│ │ ├── ModalItemGroup.jsx
│ │ ├── MoreModal.jsx
│ │ ├── MyMenuModal.jsx
│ │ ├── ShelfOrderModal.jsx
│ │ ├── UnitSortModal.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── PageAlert
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── PageRedirect.jsx
│ ├── Paginator
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Prompt
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── ResponsivePaginator.jsx
│ ├── Scrollable.jsx
│ ├── SearchBar
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── SearchBox.jsx
│ ├── SelectShelfModal
│ │ └── index.jsx
│ ├── SerialPreferenceBooks
│ │ ├── FullButton.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── SerialPreferenceToolBar
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── SeriesList
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── SeriesToolBar
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Shelf
│ │ ├── ShelfDetailLink.jsx
│ │ ├── ShelfSelectButton.jsx
│ │ ├── ShelfThumbnail.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Shelves
│ │ └── index.jsx
│ ├── ShelvesWrapper
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── SimpleShelvesWrapper
│ │ ├── SimpleShelvesItem.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Skeleton
│ │ ├── SkeletonBooks
│ │ │ ├── LandscapeBook.jsx
│ │ │ ├── PortraitBook.jsx
│ │ │ ├── index.jsx
│ │ │ └── landscapeBookStyles.js
│ │ ├── SkeletonShelves
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ │ ├── SkeletonSimpleShelves
│ │ │ ├── SkeletonSimpleShelf.jsx
│ │ │ └── index.jsx
│ │ └── SkeletonUnitDetailView
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ ├── TabBar
│ │ ├── TabItem.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── TitleBar
│ │ ├── Title.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Toast.jsx
│ ├── Toaster
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Tool
│ │ ├── Add.jsx
│ │ ├── Editing.jsx
│ │ ├── Filter.jsx
│ │ ├── More.jsx
│ │ ├── ShelfEdit.jsx
│ │ ├── ShelfOrder.jsx
│ │ ├── TotalCount.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── Tooltip
│ │ ├── TooltipBackground.jsx
│ │ ├── index.jsx
│ │ └── styles.js
│ └── UnitDetailView
│ │ ├── index.jsx
│ │ └── styles.js
├── config.js
├── constants
│ ├── authorRole.ts
│ ├── category.js
│ ├── environment.ts
│ ├── featureIds.js
│ ├── listInstructions.js
│ ├── orderOptions.js
│ ├── page.js
│ ├── serviceType.js
│ ├── shelves.js
│ ├── unitType.js
│ ├── urls.ts
│ └── viewType.js
├── index.ejs
├── index.jsx
├── pages
│ ├── base
│ │ ├── Environment
│ │ │ └── index.jsx
│ │ ├── Footer
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ │ ├── GNB
│ │ │ ├── FamilyServices.jsx
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ │ ├── LNB
│ │ │ ├── SearchAndEditingBar.jsx
│ │ │ ├── TabBar.jsx
│ │ │ ├── TitleAndEditingBar.jsx
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ │ ├── Layout.jsx
│ │ ├── PageLoadingSpinner
│ │ │ └── index.jsx
│ │ ├── Responsive
│ │ │ ├── index.tsx
│ │ │ └── styles.js
│ │ └── styles.js
│ ├── errors
│ │ ├── notFound.jsx
│ │ └── pageLoad.jsx
│ ├── login
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── purchased
│ │ ├── hidden
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ │ └── main
│ │ │ ├── index.tsx
│ │ │ └── styles.js
│ ├── serialPreference
│ │ ├── index.jsx
│ │ └── styles.js
│ ├── shelves
│ │ ├── detail
│ │ │ ├── SearchModal
│ │ │ │ ├── SearchBar.jsx
│ │ │ │ ├── SearchBooks.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── SelectShelf
│ │ │ │ ├── SimpleShelves
│ │ │ │ │ ├── SimpleShelf.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── styles.js
│ │ │ │ ├── index.jsx
│ │ │ │ └── styles.ts
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ │ └── list
│ │ │ ├── BetaAlert.jsx
│ │ │ ├── index.jsx
│ │ │ └── styles.js
│ └── unit
│ │ └── index.jsx
├── services
│ ├── account
│ │ ├── actions.js
│ │ ├── errors.js
│ │ ├── reducers.js
│ │ ├── requests.js
│ │ ├── sagas.js
│ │ └── selectors.js
│ ├── book
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── reducers.js
│ │ ├── requests.js
│ │ ├── sagas.js
│ │ └── selectors.ts
│ ├── bookDownload
│ │ ├── errors.ts
│ │ ├── reducers.ts
│ │ ├── requests.ts
│ │ ├── sagas.ts
│ │ └── selectors.ts
│ ├── common
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── errors.js
│ │ ├── requests.js
│ │ └── sagas.js
│ ├── confirm
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ └── state.js
│ ├── dialog
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ └── state.js
│ ├── excelDownload
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── reducers.js
│ │ ├── requests.js
│ │ ├── sagas.js
│ │ ├── selectors.js
│ │ └── state.js
│ ├── feature
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ ├── requests.ts
│ │ ├── sagas.js
│ │ └── selectors.js
│ ├── maintenance
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ ├── requests.js
│ │ └── state.js
│ ├── prompt
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ └── state.js
│ ├── purchased
│ │ ├── common
│ │ │ ├── actions.js
│ │ │ ├── constants.js
│ │ │ ├── errors.js
│ │ │ ├── reducers.js
│ │ │ ├── requests.js
│ │ │ ├── sagas
│ │ │ │ ├── hideAllExpiredBooksSagas.js
│ │ │ │ └── rootSagas.js
│ │ │ └── selectors.js
│ │ ├── filter
│ │ │ ├── actions.js
│ │ │ ├── reducers.js
│ │ │ ├── requests.js
│ │ │ ├── sagas.ts
│ │ │ └── selectors.js
│ │ ├── hidden
│ │ │ ├── actions.js
│ │ │ ├── reducers.js
│ │ │ ├── requests.js
│ │ │ ├── sagas.js
│ │ │ ├── selectors.js
│ │ │ └── state.js
│ │ └── main
│ │ │ ├── actions.js
│ │ │ ├── reducers.js
│ │ │ ├── requests.js
│ │ │ ├── sagas.js
│ │ │ ├── selectors.ts
│ │ │ └── state.js
│ ├── selection
│ │ ├── reducers.ts
│ │ └── selectors.js
│ ├── serialPreference
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ ├── requests.js
│ │ ├── sagas.js
│ │ ├── selectors.js
│ │ └── state.js
│ ├── shelf
│ │ ├── actions.js
│ │ ├── constants.ts
│ │ ├── reducers.js
│ │ ├── requests.ts
│ │ ├── sagas.js
│ │ └── selectors.js
│ ├── toast
│ │ ├── actions.ts
│ │ ├── constants.ts
│ │ ├── reducers.js
│ │ ├── sagas.js
│ │ └── selectors.js
│ ├── tracking
│ │ ├── actions.js
│ │ ├── constants.js
│ │ └── sagas.js
│ ├── ui
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ ├── sagas.js
│ │ └── state.js
│ └── unitPage
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ ├── requests.js
│ │ ├── sagas.js
│ │ ├── selectors.js
│ │ ├── seriesViewSagas.js
│ │ ├── state.js
│ │ └── utils.js
├── static
│ ├── OG
│ │ └── library.jpg
│ ├── cover
│ │ └── adult.png
│ ├── favicon
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-384x384.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── mstile-150x150.png
│ │ ├── safari-pinned-tab.svg
│ │ └── site.webmanifest.json
│ ├── separator
│ │ ├── portrait_book_w100.png
│ │ ├── portrait_book_w110.png
│ │ ├── portrait_book_w140.png
│ │ └── portrait_book_w86.png
│ └── spinner
│ │ └── blue_spinner.gif
├── store
│ ├── index.js
│ ├── reducers.js
│ └── sagas.js
├── styles
│ ├── books.js
│ ├── constants.js
│ ├── index.js
│ ├── reset.js
│ ├── responsive.js
│ └── unitDetailViewHeader.js
├── svgs
│ ├── ArrowLeft.svg
│ ├── ArrowTriangleDown.svg
│ ├── ArrowTriangleRight.svg
│ ├── BookOutline.svg
│ ├── CategoryFilter.svg
│ ├── Check.svg
│ ├── CheckCircle.svg
│ ├── CheckCircleFill.svg
│ ├── Close.svg
│ ├── Download.svg
│ ├── Edit.svg
│ ├── ErrorBook.svg
│ ├── ExclamationCircleFill.svg
│ ├── FeedbackIcon.svg
│ ├── FooterNewIcon.svg
│ ├── FooterPaperIcon.svg
│ ├── HeartOutline.svg
│ ├── Loading.svg
│ ├── LogoRidi.svg
│ ├── LogoRidibooks.svg
│ ├── LogoRidibooksApp.svg
│ ├── LogoRidiselect.svg
│ ├── Logout.svg
│ ├── MyMenu-active.svg
│ ├── MyMenu.svg
│ ├── NoneDashedArrowDown.svg
│ ├── NoneDashedArrowLeft.svg
│ ├── NoneDashedArrowRight.svg
│ ├── NoneDashedDoubleArrowRight.svg
│ ├── Note.svg
│ ├── NoticeFilled.svg
│ ├── On.svg
│ ├── Plus.svg
│ ├── Review.svg
│ ├── Search.svg
│ ├── SeriesCompleteIcon.svg
│ ├── Shelves.svg
│ ├── Star.svg
│ ├── Sync.svg
│ ├── ThreeDotsHorizontal.svg
│ └── ThreeDotsVertical.svg
├── types
│ └── svgr-svg.d.ts
└── utils
│ ├── array.js
│ ├── bookMetaData.js
│ ├── cookies.js
│ ├── dataObject.js
│ ├── datetime.js
│ ├── delay.js
│ ├── device.js
│ ├── file.js
│ ├── number.js
│ ├── order.js
│ ├── pagination.js
│ ├── retry.js
│ ├── scroll.js
│ ├── sentry.js
│ ├── settings.js
│ ├── snakelize.js
│ ├── storage.js
│ ├── tabFocus.js
│ ├── ttl.js
│ └── uri.js
├── traefik
└── library.toml
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | yarn-error.log
3 | node_modules/
4 | dist/
5 | .git
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 |
4 | yarn-error.log
5 | node_modules/
6 | build/
7 | out/
8 | dist/
9 |
10 | /secrets.json
11 |
12 | *.pyc
13 | docker-compose.override.yml
14 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatic merge when CI passes and reviews
3 | conditions:
4 | - base=master
5 | - "#approved-reviews-by>=1"
6 | - "#review-requested=0"
7 | - "#changes-requested-reviews-by=0"
8 | - "status-success=WIP"
9 | actions:
10 | merge:
11 | method: merge
12 | strict: smart
13 | strict_method: rebase
14 | delete_head_branch:
15 |
16 | - name: add label to hotfix branch
17 | conditions:
18 | - base=master
19 | - head~=^hotfix/
20 | actions:
21 | label:
22 | add:
23 | - hotfix
24 |
25 | - name: backport patches to release branch
26 | conditions:
27 | - base=master
28 | - label=hotfix
29 | actions:
30 | backport:
31 | branches:
32 | - release
33 |
34 | - name: automatic merge for backport
35 | conditions:
36 | - base=release
37 | - -head=master
38 | - "#approved-reviews-by>=1"
39 | actions:
40 | merge:
41 | method: merge
42 | strict: true
43 | strict_method: rebase
44 | delete_head_branch:
45 |
46 | - name: deploy new release
47 | conditions:
48 | - base=release
49 | - head=master
50 | - "#approved-reviews-by>=1"
51 | - "#review-requested=0"
52 | - "#changes-requested-reviews-by=0"
53 | - "#commented-reviews-by=0"
54 | actions:
55 | merge: {}
56 |
57 | - name: delete head branch when closed
58 | conditions:
59 | - -head=master
60 | - -head=release
61 | - closed
62 | actions:
63 | delete_head_branch:
64 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-standard"
4 | ],
5 | "rules": {
6 | "declaration-empty-line-before": null,
7 | "no-empty-source": null,
8 | "no-extra-semicolons": null,
9 | "no-missing-end-of-source-newline": null,
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.svgo.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - removeTitle: true
3 | - removeXMLNS: true
4 | - removeDimensions: true
5 | - removeAttrs:
6 | attrs:
7 | - 'fill'
8 | - 'fill-rule'
9 | - 'stroke'
10 | - 'version'
11 | - 'class'
12 | - sortAttrs:
13 | order:
14 | - 'viewBox'
15 | - 'class'
16 | - collapseGroups: true
17 |
--------------------------------------------------------------------------------
/.svgrrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | icon: true,
3 | dimensions: false,
4 | };
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## PR 기준
2 |
3 | - PR 전 이슈등록 필수
4 | - 허용 대상
5 | - Bug fix - o
6 | - Refactoring - 수정 범위에 따른 선택적 허용
7 |
8 | 좀더 명확한 PR 프로세스를 위해 Branch naming convention, Reviewer list 등 문서 업데이트 및 PR template 이 추가될 예정입니다.
9 |
10 | ```
11 | 현재 진행중인 프로젝트 이후 TS 적용 및 전반적인 Refactoring, Test 추가 프로젝트가 예정되어 있습니다.
12 | 따라서 당분간 Bug fix 외에는 PR 을 받지 않을 계획임을 참고 부탁드립니다.
13 | ```
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 RIDI Corporation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Library-Web
2 |
3 | ## Requirements
4 |
5 | - `nodejs`: v10 이상
6 |
7 | ## Installation
8 |
9 | ```
10 | yarn install
11 | ```
12 |
13 | ## Run Development
14 |
15 | ### Docker를 사용해 실행하는 방법
16 |
17 | `docker-compose`를 사용합니다.
18 |
19 | ```sh
20 | docker-compose up
21 | ```
22 |
23 | ### Docker 없이 실행하는 방법
24 |
25 | 1. 프론트엔드 `traefik` 리포지토리 안에 있는 `traefik` 디렉토리에 이
26 | 리포지토리의 `traefik/library.toml`을 복사해 넣습니다.
27 | 1. `yarn local`으로 개발 서버를 실행합니다.
28 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | const emotionAdditionalPresets = api.env('production')
3 | ? {
4 | sourceMap: true,
5 | hoist: true,
6 | }
7 | : {
8 | sourceMap: true,
9 | autoLabel: true,
10 | };
11 |
12 | const presets = [
13 | ['@babel/preset-react'],
14 | [
15 | '@babel/preset-env',
16 | {
17 | corejs: 3,
18 | useBuiltIns: 'entry',
19 | shippedProposals: true,
20 | },
21 | ],
22 | [
23 | '@emotion/babel-preset-css-prop',
24 | {
25 | autoLabel: true,
26 | labelFormat: '[local]',
27 | ...emotionAdditionalPresets,
28 | },
29 | ],
30 | ];
31 | const plugins = ['@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining'];
32 |
33 | return {
34 | presets,
35 | plugins,
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | library-web:
5 | image: node:12-alpine
6 | container_name: library-web
7 | volumes:
8 | - .:/app:ro
9 | - /app/node_modules
10 | working_dir: /app
11 | labels:
12 | - 'traefik.enable=true'
13 | - 'traefik.docker.network=${EXTERNAL_NETWORK:-ridi}'
14 | - 'traefik.frontend.rule=Host:library.local.ridi.io'
15 | networks:
16 | - traefik
17 | ports:
18 | - 3000
19 | init: true
20 | entrypoint: /app/docker/run-dev.sh
21 |
--------------------------------------------------------------------------------
/docker/run-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | yarn --frozen-lockfile
4 | yarn local
5 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 140,
3 | trailingComma: 'all',
4 | tabWidth: 2,
5 | singleQuote: true,
6 | semi: true,
7 | bracketSpacing: true,
8 | };
9 |
--------------------------------------------------------------------------------
/settings/dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "account_base_url": "https://account.dev.ridi.io/",
3 | "base_url": "https://library.ridi.io/",
4 | "book_api_base_url": "https://book-api.ridibooks.com/",
5 | "book_feedback_api_base_url": "https://book-feedback.dev.ridi.io/",
6 | "environment": "development",
7 | "help_base_url": "https://help.ridibooks.com/hc/ko",
8 | "library_api_base_url": "https://library-api.dev.ridi.io/",
9 | "policy_base_url": "https://policy.ridi.com",
10 | "ridi_logout_url": "https://master.test.ridi.io/account/logout?return_url=https%3A%2F%2Flibrary.ridi.io%2Flogin",
11 | "ridi_oauth2_client_id": "Nkt2Xdc0zMuWmye6MSkYgqCh9q6JjeMCsUiH1kgL",
12 | "ridi_reading_note_url": "https://master.test.ridi.io/reading-note/timeline",
13 | "ridi_review_url": "https://master.test.ridi.io/review",
14 | "ridi_status_url": "https://sorry.ridibooks.com/status",
15 | "ridi_token_authorize_url": "https://account.dev.ridi.io/ridi/authorize",
16 | "select_base_url": "https://select.dev.ridi.io/",
17 | "sentry_dsn": "",
18 | "sentry_env": "development",
19 | "static_url": "https://library.ridi.io/",
20 | "store_api_base_url": "https://master.test.ridi.io/",
21 | "store_base_url": "https://master.test.ridi.io/",
22 | "viewer_api_base_url": "https://viewer-api-v1.dev.ridi.io/"
23 | }
24 |
--------------------------------------------------------------------------------
/settings/local.json:
--------------------------------------------------------------------------------
1 | {
2 | "account_base_url": "https://account.dev.ridi.io/",
3 | "base_url": "https://library.local.ridi.io/",
4 | "book_api_base_url": "https://book-api.ridibooks.com/",
5 | "book_feedback_api_base_url": "https://book-feedback.dev.ridi.io/",
6 | "environment": "local",
7 | "help_base_url": "https://help.ridibooks.com/hc/ko",
8 | "library_api_base_url": "https://library-api.dev.ridi.io/",
9 | "policy_base_url": "https://policy.ridi.com",
10 | "ridi_logout_url": "https://master.test.ridi.io/account/logout?return_url=https%3A%2F%2Flibrary.local.ridi.io%2Flogin",
11 | "ridi_oauth2_client_id": "Nkt2Xdc0zMuWmye6MSkYgqCh9q6JjeMCsUiH1kgL",
12 | "ridi_reading_note_url": "https://master.test.ridi.io/reading-note/timeline",
13 | "ridi_review_url": "https://master.test.ridi.io/review",
14 | "ridi_status_url": "https://sorry.ridibooks.com/status",
15 | "ridi_token_authorize_url": "https://account.dev.ridi.io/ridi/authorize",
16 | "select_base_url": "https://select.dev.ridi.io/",
17 | "sentry_dsn": "",
18 | "sentry_env": "local",
19 | "static_url": "https://library.local.ridi.io/",
20 | "store_api_base_url": "https://master.test.ridi.io/",
21 | "store_base_url": "https://master.test.ridi.io/",
22 | "viewer_api_base_url": "https://viewer-api-v1.dev.ridi.io/"
23 | }
24 |
--------------------------------------------------------------------------------
/settings/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "account_base_url": "https://account.ridibooks.com/",
3 | "base_url": "https://library.ridibooks.com/",
4 | "book_api_base_url": "https://book-api.ridibooks.com/",
5 | "book_feedback_api_base_url": "https://book-feedback.ridibooks.com/",
6 | "environment": "production",
7 | "help_base_url": "https://help.ridibooks.com/hc/ko",
8 | "library_api_base_url": "https://library-api.ridibooks.com",
9 | "policy_base_url": "https://policy.ridi.com",
10 | "ridi_logout_url": "https://ridibooks.com/account/logout?return_url=https%3A%2F%2Flibrary.ridibooks.com%2Flogin",
11 | "ridi_oauth2_client_id": "ePgbKKRyPvdAFzTvFg2DvrS7GenfstHdkQ2uvFNd",
12 | "ridi_reading_note_url": "https://ridibooks.com/reading-note/timeline",
13 | "ridi_review_url": "https://ridibooks.com/review",
14 | "ridi_status_url": "https://sorry.ridibooks.com/status",
15 | "ridi_token_authorize_url": "https://account.ridibooks.com/ridi/authorize",
16 | "select_base_url": "https://select.ridibooks.com/",
17 | "sentry_dsn": "https://0100a981cf6840ceac1a206051a199ba@sentry.io/1335489",
18 | "sentry_env": "production",
19 | "static_url": "https://library.ridicdn.net/",
20 | "store_api_base_url": "https://ridibooks.com/",
21 | "store_base_url": "https://ridibooks.com/",
22 | "viewer_api_base_url": "https://ridibooks.com/"
23 | }
24 |
--------------------------------------------------------------------------------
/settings/staging.json:
--------------------------------------------------------------------------------
1 | {
2 | "account_base_url": "https://account.ridibooks.com/",
3 | "base_url": "https://library.ridibooks.com/",
4 | "book_api_base_url": "https://book-api.ridibooks.com/",
5 | "book_feedback_api_base_url": "https://book-feedback.ridibooks.com/",
6 | "environment": "staging",
7 | "help_base_url": "https://help.ridibooks.com/hc/ko",
8 | "library_api_base_url": "https://library-api.ridibooks.com",
9 | "policy_base_url": "https://policy.ridi.com",
10 | "ridi_logout_url": "https://ridibooks.com/account/logout?return_url=https%3A%2F%2Flibrary.ridibooks.com%2Flogin",
11 | "ridi_oauth2_client_id": "ePgbKKRyPvdAFzTvFg2DvrS7GenfstHdkQ2uvFNd",
12 | "ridi_reading_note_url": "https://ridibooks.com/reading-note/timeline",
13 | "ridi_review_url": "https://ridibooks.com/review",
14 | "ridi_status_url": "https://sorry.ridibooks.com/status",
15 | "ridi_token_authorize_url": "https://account.ridibooks.com/ridi/authorize",
16 | "select_base_url": "https://select.ridibooks.com/",
17 | "sentry_dsn": "https://0100a981cf6840ceac1a206051a199ba@sentry.io/1335489",
18 | "sentry_env": "staging",
19 | "static_url": "https://library.ridicdn.net/",
20 | "store_api_base_url": "https://ridibooks.com/",
21 | "store_base_url": "https://ridibooks.com/",
22 | "viewer_api_base_url": "https://ridibooks.com/"
23 | }
24 |
--------------------------------------------------------------------------------
/src/ScrollManager.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router-dom';
3 |
4 | function ScrollManager({ location }) {
5 | React.useEffect(() => {
6 | const { key, state } = location;
7 | if (window.sessionStorage.getItem(key) == null) {
8 | // state에 주어진 명령에 따라 스크롤 위치를 조작한다
9 | let position = { x: 0, y: 0 };
10 | if (state != null && state.scroll != null) {
11 | const { scroll } = state;
12 | if ('from' in scroll) {
13 | const scrollDataRaw = window.sessionStorage.getItem(scroll.from);
14 | position = scrollDataRaw ? JSON.parse(scrollDataRaw) : position;
15 | } else if ('x' in scroll && 'y' in scroll) {
16 | position = {
17 | x: Number(scroll.x),
18 | y: Number(scroll.y),
19 | };
20 | }
21 | }
22 | window.scrollTo(position.x, position.y);
23 | }
24 |
25 | return () => {
26 | window.sessionStorage.setItem(key, JSON.stringify({ x: window.scrollX, y: window.scrollY }));
27 | };
28 | }, [location]);
29 |
30 | return null;
31 | }
32 |
33 | export default withRouter(ScrollManager);
34 |
--------------------------------------------------------------------------------
/src/TrackManager.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 |
5 | import { initTracker, trackPage } from 'services/tracking/actions';
6 |
7 | const TrackManager = props => {
8 | const account = useSelector(state => state.account);
9 | const dispatch = useDispatch();
10 |
11 | const {
12 | location: { pathname },
13 | } = props;
14 |
15 | React.useEffect(() => {
16 | dispatch(trackPage(pathname));
17 | }, [account, pathname]);
18 |
19 | React.useEffect(() => {
20 | dispatch(initTracker());
21 | }, [account]);
22 |
23 | return null;
24 | };
25 |
26 | export default withRouter(TrackManager);
27 |
--------------------------------------------------------------------------------
/src/api/actions.js:
--------------------------------------------------------------------------------
1 | export const GET_API = 'GET_API';
2 | export const GET_PUBLIC_API = 'GET_PUBLIC_API';
3 |
4 | export const getAPI = (requestToken = null) => ({
5 | type: GET_API,
6 | payload: {
7 | requestToken,
8 | },
9 | });
10 |
11 | export const getPublicAPI = () => ({
12 | type: GET_PUBLIC_API,
13 | });
14 |
--------------------------------------------------------------------------------
/src/api/constants.js:
--------------------------------------------------------------------------------
1 | export const HttpStatusCode = {
2 | HTTP_200_SUCCESS: 200,
3 | HTTP_400_BAD_REQUEST: 400,
4 | HTTP_401_UNAUTHORIZED: 401,
5 | HTTP_404_NOT_FOUND: 404,
6 | HTTP_503_SERVICE_UNAVAILABLE: 503,
7 | };
8 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import API from './api';
2 | import { createAuthorizationInterceptor, createMaintenanceInterceptor } from './interceptor';
3 |
4 | let api = null;
5 |
6 | export const initializeApi = (req, store) => {
7 | if (req != null) {
8 | const { token } = req;
9 | api = new API(false, { Cookie: `ridi-at=${token};` });
10 | return api;
11 | }
12 |
13 | const withCredentials = true;
14 | api = new API(withCredentials);
15 | api.addInterceptors([createAuthorizationInterceptor(store), createMaintenanceInterceptor(store)]);
16 | api.registerInterceptor();
17 | return api;
18 | };
19 |
20 | export const getApi = context => {
21 | if (api == null) {
22 | return initializeApi(context && context.req);
23 | }
24 |
25 | return api;
26 | };
27 |
28 | let publicApi = null;
29 |
30 | export const initializePublicApi = store => {
31 | publicApi = new API();
32 | publicApi.addInterceptors([createMaintenanceInterceptor(store)]);
33 | publicApi.registerInterceptor();
34 |
35 | return publicApi;
36 | };
37 |
38 | export const getPublicApi = () => {
39 | if (publicApi == null) {
40 | return initializePublicApi();
41 | }
42 |
43 | return publicApi;
44 | };
45 |
--------------------------------------------------------------------------------
/src/api/middleware.js:
--------------------------------------------------------------------------------
1 | import { GET_API, GET_PUBLIC_API } from './actions';
2 | import { getApi, getPublicApi } from './index';
3 |
4 | const createApiMiddleware = () => () => next => action => {
5 | if (action.type === GET_API) {
6 | return getApi();
7 | }
8 |
9 | if (action.type === GET_PUBLIC_API) {
10 | return getPublicApi();
11 | }
12 |
13 | return next(action);
14 | };
15 |
16 | export default createApiMiddleware;
17 |
--------------------------------------------------------------------------------
/src/components/ActionBar/ActionBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Responsive from '../../pages/base/Responsive';
4 | import * as styles from './styles';
5 |
6 | export const ActionBar = ({ children }) => (
7 | <>
8 |
9 |
10 |
11 | {children}
12 |
13 |
14 | >
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/ActionBar/ActionButton.jsx:
--------------------------------------------------------------------------------
1 | import { ButtonType } from './constants';
2 | import * as styles from './styles';
3 |
4 | export const ActionButton = ({ name, onClick, type = ButtonType.NORMAL, disable = false, className = '' }) =>
5 | type === ButtonType.SPACER ? (
6 |
7 | ) : (
8 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/components/ActionBar/constants.js:
--------------------------------------------------------------------------------
1 | export const ButtonType = {
2 | NORMAL: 'normal',
3 | DANGER: 'danger',
4 | SPACER: 'spacer',
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/ActionBar/index.jsx:
--------------------------------------------------------------------------------
1 | export * from './ActionBar';
2 | export * from './ActionButton';
3 |
--------------------------------------------------------------------------------
/src/components/ActionBar/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import { ButtonType } from './constants';
4 |
5 | export const ACTION_BAR_HEIGHT = 51;
6 |
7 | export const actionBarPadding = css`
8 | height: ${ACTION_BAR_HEIGHT}px;
9 | `;
10 |
11 | export const actionBarFixedWrapper = {
12 | position: 'fixed',
13 | bottom: 0,
14 | left: 0,
15 | right: 0,
16 | zIndex: 8000,
17 | backgroundColor: '#f7f9fa',
18 | boxShadow: '0 -2px 10px 0 rgba(0, 0, 0, 0.04)',
19 | borderTop: '1px solid #d1d5d9',
20 | };
21 |
22 | export const actionBar = {
23 | display: 'flex',
24 | height: `${ACTION_BAR_HEIGHT - 1}px`,
25 | };
26 |
27 | export const actionButton = disable => {
28 | const disabledStyle = disable
29 | ? {
30 | opacity: '0.4',
31 | }
32 | : {};
33 | return {
34 | fontSize: 15,
35 | lineHeight: '1.2em',
36 | textAlign: 'center',
37 | height: 50,
38 | padding: '0 8px',
39 | ...disabledStyle,
40 | '&:first-of-type': {
41 | paddingLeft: 0,
42 | },
43 | '&:last-of-type': {
44 | paddingRight: 0,
45 | },
46 | };
47 | };
48 |
49 | export const actionButtonType = type => {
50 | switch (type) {
51 | case ButtonType.DANGER:
52 | return {
53 | color: '#e64938',
54 | };
55 | case ButtonType.SPACER:
56 | return {
57 | flex: '1',
58 | };
59 | case ButtonType.NORMAL:
60 | default:
61 | return {
62 | color: '#1f8ce6',
63 | };
64 | }
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/BookDownLoader.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 |
3 | import { getBookDownloadSrc } from 'services/bookDownload/selectors';
4 |
5 | const BookDownLoader: React.FC = () => {
6 | const src: string = useSelector(getBookDownloadSrc);
7 | return src ? : null;
8 | };
9 |
10 | export default BookDownLoader;
11 |
--------------------------------------------------------------------------------
/src/components/Books/Disabled.jsx:
--------------------------------------------------------------------------------
1 | const disabled = {
2 | display: 'block',
3 | position: 'absolute',
4 | left: 0,
5 | top: 0,
6 | width: '100%',
7 | height: '100%',
8 | zIndex: '300',
9 | overflow: 'hidden',
10 | };
11 | const dimmed = {
12 | display: 'block',
13 | position: 'absolute',
14 | left: 0,
15 | top: '-1px',
16 | width: '100%',
17 | height: '100%',
18 | background: '#f3f4f5',
19 | opacity: '0.4',
20 | };
21 |
22 | export const Disabled = () => (
23 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/Books/EmptyLandscapeBook.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from '../../styles/books';
2 |
3 | const EmptyLandscapeBook = () => (
4 |
7 | );
8 |
9 | export default EmptyLandscapeBook;
10 |
--------------------------------------------------------------------------------
/src/components/Books/FullButton.jsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | const fullButtonStyle = css`
4 | display: block;
5 | position: absolute;
6 | left: 0;
7 | top: 0;
8 | width: 100%;
9 | height: 100%;
10 | z-index: 350;
11 |
12 | a,
13 | button {
14 | display: block;
15 | width: 100%;
16 | height: 100%;
17 | font-size: 0;
18 | line-height: 0;
19 | color: transparent;
20 | }
21 | `;
22 |
23 | const FullButton = ({ children }) => {children}
;
24 |
25 | export default FullButton;
26 |
--------------------------------------------------------------------------------
/src/components/BooksWrapper/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect, useState } from 'react';
2 |
3 | import * as styles from 'commonStyles/books';
4 |
5 | const BooksWrapper = ({ viewType, books, renderBook, children }) => {
6 | const isLoaded = true;
7 | const booksWrapperClassName = 'BooksWrapper';
8 | const bookClassName = 'Book';
9 | const [additionalPadding, setAdditionalPadding] = useState(0);
10 |
11 | const setBooksAdditionalPadding = () => {
12 | const booksNode = document.querySelector(`.${booksWrapperClassName}`);
13 | const bookNode = document.querySelector(`.${bookClassName}`);
14 | booksNode && bookNode && setAdditionalPadding(Math.floor((booksNode.offsetWidth % bookNode.offsetWidth) / 2));
15 | };
16 |
17 | useEffect(() => {
18 | window.addEventListener('resize', setBooksAdditionalPadding);
19 | return () => {
20 | window.removeEventListener('resize', setBooksAdditionalPadding);
21 | };
22 | }, [isLoaded]);
23 |
24 | useLayoutEffect(() => {
25 | setBooksAdditionalPadding();
26 | }, [isLoaded, viewType]);
27 |
28 | const bookElements = books.map(book => renderBook({ book, className: bookClassName }));
29 | let body = bookElements;
30 | if (typeof children === 'function') {
31 | body = children({ books: bookElements });
32 | }
33 | return (
34 |
35 | {body}
36 |
37 | );
38 | };
39 |
40 | export default BooksWrapper;
41 |
--------------------------------------------------------------------------------
/src/components/BottomActionBar/BottomActionBar.jsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import Responsive from '../../pages/base/Responsive';
4 |
5 | const styles = {
6 | bottomActionBar: css({
7 | position: 'fixed',
8 | bottom: 0,
9 | left: 0,
10 | right: 0,
11 | height: 50,
12 | backgroundColor: '#f7f9fa',
13 | boxShadow: '0 -2px 10px 0 rgba(0, 0, 0, 0.04)',
14 | boxSizing: 'border-box',
15 | borderTop: '1px solid #d1d5d9',
16 | zIndex: 999,
17 | }),
18 | };
19 |
20 | const BottomActionBar = ({ children }) => (
21 |
22 | {children}
23 |
24 | );
25 |
26 | export default BottomActionBar;
27 |
--------------------------------------------------------------------------------
/src/components/BottomActionBar/index.jsx:
--------------------------------------------------------------------------------
1 | import { ActionBar, ActionButton } from '../ActionBar';
2 |
3 | const BottomActionBar = ({ buttonProps }) => (
4 |
5 | {buttonProps.map(button => (
6 |
13 | ))}
14 |
15 | );
16 |
17 | export default BottomActionBar;
18 |
--------------------------------------------------------------------------------
/src/components/BottomActionBar/styles.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/components/BottomActionBar/styles.js
--------------------------------------------------------------------------------
/src/components/Confirm/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Close from '../../svgs/Close.svg';
4 | import { disableScroll, enableScroll } from '../../utils/scroll';
5 | import IconButton from '../IconButton';
6 | import * as styles from './styles';
7 |
8 | export default class Confirm extends React.Component {
9 | componentDidMount() {
10 | disableScroll();
11 | }
12 |
13 | componentWillUnmount() {
14 | enableScroll();
15 | }
16 |
17 | render() {
18 | const { title, message, confirmLabel = '확인', onClickCloseButton, onClickConfirmButton } = this.props;
19 |
20 | return (
21 |
22 |
23 |
24 |
{title}
25 |
26 |
27 |
28 |
29 |
{message}
30 |
31 |
41 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Dialog/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Close from '../../svgs/Close.svg';
4 | import { disableScroll, enableScroll } from '../../utils/scroll';
5 | import IconButton from '../IconButton';
6 | import * as styles from './styles';
7 |
8 | export default class Dialog extends React.Component {
9 | componentDidMount() {
10 | disableScroll();
11 | }
12 |
13 | componentWillUnmount() {
14 | enableScroll();
15 | }
16 |
17 | render() {
18 | const { title, message, onClickCloseButton } = this.props;
19 | return (
20 |
21 |
22 |
23 |
{title}
24 |
25 |
26 |
27 |
28 |
{message}
29 |
30 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Dialog/styles.js:
--------------------------------------------------------------------------------
1 | export const dialogWrapper = {
2 | position: 'fixed',
3 | top: 0,
4 | bottom: 0,
5 | left: 0,
6 | right: 0,
7 | zIndex: 9999,
8 | backgroundColor: 'rgba(33, 37, 41, 0.6)',
9 |
10 | display: 'flex',
11 | alignItems: 'center',
12 | justifyContent: 'center',
13 | };
14 |
15 | export const dialog = {
16 | borderRadius: 3,
17 | backgroundColor: 'white',
18 |
19 | width: 300,
20 | padding: 20,
21 | boxSizing: 'border-box',
22 | };
23 |
24 | export const dialogHeader = {
25 | position: 'relative',
26 | };
27 |
28 | export const dialogFooter = {};
29 |
30 | export const dialogTitle = {
31 | fontSize: 17,
32 | fontWeight: 'bold',
33 | lineHeight: 1.41,
34 | letterSpacing: -0.3,
35 | color: '#303538',
36 | };
37 |
38 | export const dialogCloseButton = {
39 | position: 'absolute',
40 | top: 0,
41 | right: 0,
42 |
43 | width: 15,
44 | height: 15,
45 | fill: '#d1d5d9',
46 | };
47 |
48 | export const dialogContent = {
49 | fontSize: 15,
50 | letterSpacing: -0.3,
51 | color: '#525a61',
52 |
53 | margin: '20px 0',
54 | };
55 |
56 | export const dialogButton = {
57 | width: 92,
58 | height: 40,
59 | borderRadius: 4,
60 | backgroundColor: '#1f8ce6',
61 |
62 | fontSize: 14,
63 | fontWeight: 'bold',
64 | letterSpacing: -0.3,
65 | textAlign: 'center',
66 | color: '#ffffff',
67 | float: 'right',
68 | };
69 |
70 | export const clear = {
71 | clear: 'both',
72 | };
73 |
--------------------------------------------------------------------------------
/src/components/Editable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import BottomActionBar from './BottomActionBar';
4 | import EditingBar from './EditingBar';
5 | import FixedToolbarView from './FixedToolbarView';
6 |
7 | interface Props {
8 | actionBarProps?: any;
9 | allowFixed?: boolean;
10 | doubleEditBar?: boolean;
11 | editingBarProps?: any;
12 | isEditing?: boolean;
13 | nonEditBar: React.ReactNode;
14 | }
15 |
16 | export default function Editable(props: Props & { children?: React.ReactNode }) {
17 | const { actionBarProps, allowFixed, children, doubleEditBar, editingBarProps, isEditing, nonEditBar } = props;
18 | return (
19 | : nonEditBar}
23 | actionBar={isEditing ? : null}
24 | >
25 | {children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/EditingBar/SelectAllButton.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 | import React from 'react';
3 |
4 | export const buttonStyle = css`
5 | font-size: 15px;
6 | color: white;
7 | padding: 8px;
8 | margin-right: 8px;
9 | `;
10 |
11 | interface SelectAllButtonProps {
12 | isSelectedAll: boolean;
13 | handleSelectAllClick: (event: React.MouseEvent) => void;
14 | handleDeselectAllClick: (event: React.MouseEvent) => void;
15 | }
16 |
17 | const SelectAllButton: React.FC = ({ isSelectedAll, handleSelectAllClick, handleDeselectAllClick }) => (
18 |
21 | );
22 |
23 | export default SelectAllButton;
24 |
--------------------------------------------------------------------------------
/src/components/EditingBar/index.jsx:
--------------------------------------------------------------------------------
1 | import Responsive from 'pages/base/Responsive';
2 | import Check from 'svgs/Check.svg';
3 |
4 | import SelectAllButton from './SelectAllButton';
5 | import * as styles from './styles';
6 |
7 | const EditingBar = ({
8 | totalSelectedCount,
9 | isSelectedAllItem,
10 | onClickSelectAllItem,
11 | onClickDeselectAllItem,
12 | onClickSuccessButton,
13 | countUnit = '권',
14 | }) => (
15 |
16 |
17 |
18 |
19 |
20 | {totalSelectedCount}
21 | {countUnit} 선택
22 |
23 |
24 |
25 |
30 |
33 |
34 |
35 |
36 | );
37 |
38 | export default EditingBar;
39 |
--------------------------------------------------------------------------------
/src/components/EditingBar/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | export const editingBarWrapper = css`
4 | background-color: #0077d9;
5 | box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.04);
6 | width: 100%;
7 | `;
8 |
9 | export const editingBar = css`
10 | height: 46px;
11 | display: flex;
12 | align-items: center;
13 | justify-content: space-between;
14 | `;
15 |
16 | export const editingBarIconWrapper = css`
17 | flex: auto;
18 | display: flex;
19 | align-items: center;
20 | `;
21 |
22 | export const editingBarIcon = css`
23 | width: 18px;
24 | height: 18px;
25 | fill: white;
26 | margin-right: 4px;
27 | `;
28 |
29 | export const editingBarSelectCount = css`
30 | display: inline-block;
31 | font-size: 15px;
32 | color: white;
33 | `;
34 |
35 | export const editingBarCompleteButton = css`
36 | width: 52px;
37 | height: 30px;
38 | line-height: 28px;
39 | border-radius: 4px;
40 | background-color: white;
41 | box-shadow: 1px 1px 1px 0 rgba(0, 0, 0, 0.05);
42 | border: 1px solid #d1d5d9;
43 | box-sizing: border-box;
44 | font-size: 13px;
45 | font-weight: bold;
46 | text-align: center;
47 | color: #0077d9;
48 | `;
49 |
--------------------------------------------------------------------------------
/src/components/Empty/EmptyShelves.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Shelves from '../../svgs/Shelves.svg';
4 | import Empty from './index';
5 |
6 | export const EmptyShelves = () => ;
7 |
--------------------------------------------------------------------------------
/src/components/Empty/index.jsx:
--------------------------------------------------------------------------------
1 | const styles = {
2 | wrapper: {
3 | position: 'relative',
4 | color: '#40474d',
5 | width: '100%',
6 | height: '100%',
7 | minHeight: 400,
8 | },
9 | contents: {
10 | width: 300,
11 | height: 100,
12 | display: 'inline-block',
13 | position: 'absolute',
14 | left: '50%',
15 | top: '50%',
16 | marginLeft: -150,
17 | marginTop: -40,
18 | fontSize: 15,
19 | textAlign: 'center',
20 | },
21 | icon: {
22 | fill: '#d1d5d9',
23 | marginBottom: 20,
24 | },
25 | };
26 |
27 | const Empty = ({ message, IconComponent, iconWidth = 30, iconHeight = 38 }) => {
28 | const iconSize = { width: iconWidth, height: iconHeight };
29 | return (
30 |
31 |
32 | {IconComponent && }
33 |
34 | {message}
35 |
36 |
37 | );
38 | };
39 |
40 | export default Empty;
41 |
--------------------------------------------------------------------------------
/src/components/Error/BookError.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import BookOutline from '../../svgs/BookOutline.svg';
4 | import ComponentError from './base/ComponentError';
5 |
6 | export const BookError = ({ onClickRefreshButton }) => (
7 |
8 | 도서의 정보 구성 중 오류가 발생했습니다.
9 |
10 | 잠시 후 다시 시도해주세요.
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/components/Error/InternalError.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ServiceError from './base/ServiceError';
4 |
5 | export const InternalError = () => (
6 |
7 | 지금은 접속이 어렵습니다.
8 |
9 | 현재 오류 복구에 최선을 다하고 있으니,
10 |
11 | 잠시 후 다시 접속해주세요.
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/Error/NotFoundError.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ServiceError from './base/ServiceError';
4 |
5 | export const NotFoundError = () => (
6 |
7 | 요청하신 페이지가 없습니다.
8 |
9 | 입력하신 주소를 확인해 주세요.
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Error/PageLoadError.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ServiceError from './base/ServiceError';
4 |
5 | export const PageLoadError = () => (
6 |
7 | 요청하신 페이지를 불러오지 못했습니다.
8 |
9 | 잠시 후 다시 시도해 주세요.
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Error/base/ComponentError.jsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | const styles = {
4 | errorWrapper: css`
5 | margin-top: 150px;
6 | margin-bottom: 150px;
7 | text-align: center;
8 | `,
9 | icon: css`
10 | width: 30px;
11 | height: 38px;
12 | fill: #d1d5d9;
13 | margin-bottom: 20px;
14 | `,
15 | message: css`
16 | font-size: 15px;
17 | color: #40474d;
18 | margin-bottom: 20px;
19 | `,
20 | refreshButton: css`
21 | width: 68px;
22 | height: 30px;
23 | border-radius: 4px;
24 | background-color: white;
25 | box-shadow: 1px 1px 1px 0 rgba(0, 0, 0, 0.05);
26 | border: 1px solid #d1d5d9;
27 |
28 | font-size: 13px;
29 | font-weight: bold;
30 | color: #808991;
31 | `,
32 | };
33 |
34 | const ComponentError = ({ children, ErrorIcon, onClickRefreshButton }) => (
35 |
36 |
37 |
{children}
38 | {onClickRefreshButton && (
39 |
42 | )}
43 |
44 | );
45 |
46 | export default ComponentError;
47 |
--------------------------------------------------------------------------------
/src/components/Error/base/serviceErrorStyles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | const styles = {
4 | pageError: css`
5 | text-align: center;
6 | padding: 60px 0;
7 | `,
8 |
9 | errorTitle: css`
10 | margin: 8px 0;
11 | font-size: 60px;
12 | color: #303538;
13 | `,
14 |
15 | errorDescription: css`
16 | margin: 20px 0;
17 | font-size: 18px;
18 | line-height: 1.6em;
19 | color: #525a61;
20 | `,
21 |
22 | errorButtonWrapper: css`
23 | display: inline-block;
24 | margin-top: 36px;
25 | `,
26 |
27 | errorButton: css`
28 | display: inline-block;
29 | width: 140px;
30 | padding: 14px 0;
31 | margin: 0 2px;
32 | font-size: 16px;
33 | font-weight: 700;
34 | text-align: center;
35 | cursor: pointer;
36 | transition: background 0.2s, color 0.2s;
37 | outline: none;
38 | box-sizing: border-box;
39 | border-radius: 4px;
40 | appearance: none;
41 | `,
42 |
43 | whiteButton: css`
44 | color: #808991;
45 | background: #fff;
46 | border: 1px solid #d1d5d9;
47 | box-shadow: 0 1px 1px 0 rgba(209, 213, 217, 0.3);
48 | `,
49 |
50 | grayButton: css`
51 | color: #fff;
52 | background: #808991;
53 | border: 1px solid #798086;
54 | box-shadow: 0 1px 1px 0 rgba(209, 213, 217, 0.3);
55 | `,
56 |
57 | icon: css`
58 | width: 94px;
59 | height: 79px;
60 | fill: #d1d5d9;
61 | `,
62 | };
63 | export default styles;
64 |
--------------------------------------------------------------------------------
/src/components/Error/index.jsx:
--------------------------------------------------------------------------------
1 | export * from './BookError';
2 | export * from './NotFoundError';
3 | export * from './InternalError';
4 | export * from './PageLoadError';
5 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/index.jsx:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/browser';
2 | import React from 'react';
3 |
4 | import { PageLoadError } from '../Error';
5 |
6 | export default class ErrorBoundary extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = { hasError: false };
10 | }
11 |
12 | static getDerivedStateFromError() {
13 | return { hasError: true };
14 | }
15 |
16 | componentDidCatch(error, info) {
17 | console.error(error);
18 | Sentry.withScope(scope => {
19 | scope.setExtra('componentsError', info);
20 | Sentry.captureException(error);
21 | });
22 | }
23 |
24 | render() {
25 | const { hasError } = this.state;
26 | const { children } = this.props;
27 | if (hasError) {
28 | return ;
29 | }
30 |
31 | return children;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Filler.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 | import React from 'react';
3 |
4 | const filler = css`
5 | height: 300vh;
6 | `;
7 |
8 | export default function Filler() {
9 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/FlexBar/index.jsx:
--------------------------------------------------------------------------------
1 | import classname from 'classnames';
2 |
3 | import Responsive from '../../pages/base/Responsive';
4 | import * as styles from './styles';
5 |
6 | const FlexBar = ({ hideTools, flexLeft = null, flexRight = null, className }) => (
7 |
8 |
9 | {flexLeft}
10 | {flexRight}
11 |
12 |
13 | );
14 |
15 | export default FlexBar;
16 |
--------------------------------------------------------------------------------
/src/components/FlexBar/styles.js:
--------------------------------------------------------------------------------
1 | export const flexWrapper = {
2 | display: 'flex',
3 | height: 45,
4 | alignItems: 'center',
5 | justifyContent: 'space-between',
6 | };
7 |
--------------------------------------------------------------------------------
/src/components/FullScreenLoading.jsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 | import React from 'react';
3 |
4 | import { disableScroll, enableScroll } from '../utils/scroll';
5 | import LoadingSpinner from './LoadingSpinner';
6 |
7 | const styles = {
8 | background: css`
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | `,
13 | fixed: css`
14 | background-color: rgba(255, 255, 255, 0.9);
15 | position: fixed;
16 | z-index: 9999;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | right: 0;
21 | `,
22 | static: css`
23 | background-color: #f3f4f5;
24 | width: 100vw;
25 | height: 90vh;
26 | `,
27 | spinner: css`
28 | width: 32px;
29 | height: 32px;
30 | `,
31 | };
32 |
33 | export default class FullScreenLoading extends React.Component {
34 | componentDidMount() {
35 | disableScroll();
36 | }
37 |
38 | componentWillUnmount() {
39 | enableScroll();
40 | }
41 |
42 | render() {
43 | const { fixed: isFixed } = this.props;
44 | return (
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/HorizontalRuler.jsx:
--------------------------------------------------------------------------------
1 | const style = backgroundColor => ({
2 | height: 1,
3 | backgroundColor: backgroundColor || '#f3f4f5',
4 | });
5 |
6 | const HorizontalRuler = ({ color }) => ;
7 |
8 | export default HorizontalRuler;
9 |
--------------------------------------------------------------------------------
/src/components/IconButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconButton = ({ a11y, children, ...buttonProps }) => (
4 |
8 | );
9 |
10 | export default IconButton;
11 |
--------------------------------------------------------------------------------
/src/components/LoadingSpinner.jsx:
--------------------------------------------------------------------------------
1 | import { css, keyframes } from '@emotion/core';
2 |
3 | import Loading from '../svgs/Loading.svg';
4 |
5 | const spinAnimation = keyframes`
6 | from {
7 | transform: rotate(0turn);
8 | }
9 | to {
10 | transform: rotate(1turn);
11 | }
12 | `;
13 |
14 | const loading = css`
15 | fill: #808991;
16 | animation: ${spinAnimation} 1s steps(12, end) infinite;
17 | `;
18 |
19 | export default function LoadingSpinner(props) {
20 | const { className } = props;
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Maintenance/index.jsx:
--------------------------------------------------------------------------------
1 | import { Global } from '@emotion/core';
2 | import React from 'react';
3 | import Helmet from 'react-helmet';
4 |
5 | import RIDIIcon from '../../svgs/LogoRidi.svg';
6 | import NoticeIcon from '../../svgs/NoticeFilled.svg';
7 | import * as styles from './styles';
8 |
9 | const ServiceList = ({ services }) => (
10 |
11 | {services.map(service => (
12 |
13 | -
14 |
{service}
15 |
16 |
17 | ))}
18 |
19 | );
20 |
21 | const Maintenance = ({ terms, unavailableServiceList }) => (
22 | <>
23 |
24 | 시스템 점검 중 - 내 서재
25 |
26 |
29 |
30 |
31 |
32 | 시스템 점검 안내
33 | 안녕하세요.
34 | 보다 나은 서비스 제공을 위해 시스템 점검을 실시합니다.
35 |
36 | 점검 중에는 일부 서비스 제공이 어려우니 양해 부탁드립니다.
37 |
38 |
39 |
40 |
41 | - 점검 기간
42 | -
43 |
46 |
47 | - 점검 기간 중 이용이 제한되는 서비스
48 | -
49 |
50 |
51 |
52 |
53 |
58 |
59 | >
60 | );
61 |
62 | export default Maintenance;
63 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ModalBackground } from './ModalBackground';
4 | import * as modalStyles from './styles';
5 |
6 | export const Modal = ({ a11y, isActive, children, style, onClickModalBackground, horizontalAlign, modalRef }) => (
7 | <>
8 |
9 | {a11y && {a11y}
}
10 | {children}
11 |
12 |
13 | >
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalBackground.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from './styles';
2 |
3 | export const ModalBackground = ({ isActive, onClickModalBackground }) => (
4 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalItemGroup.jsx:
--------------------------------------------------------------------------------
1 | import * as modalStyles from './styles';
2 |
3 | export const ModalItemGroup = ({ groupTitle, children, style }) => (
4 |
5 | {groupTitle &&
{groupTitle}
}
6 | {children}
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/src/components/Modal/ShelfOrderModal.jsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalButtonItem, ModalItemGroup } from '.';
2 |
3 | const ShelfOrderModal = ({ order, orderOptions, isActive, onClickModalBackground, onClickOrderOption }) => (
4 |
5 |
6 |
7 | {orderOptions.map(option => (
8 | -
9 | {
12 | onClickOrderOption(option);
13 | }}
14 | >
15 | {option.title}
16 |
17 |
18 | ))}
19 |
20 |
21 |
22 | );
23 |
24 | export default ShelfOrderModal;
25 |
--------------------------------------------------------------------------------
/src/components/Modal/UnitSortModal.jsx:
--------------------------------------------------------------------------------
1 | import { withRouter } from 'react-router-dom';
2 |
3 | import { Modal, ModalItemGroup, ModalLinkItem } from '.';
4 |
5 | function buildTargetLocation(location, option, scroll) {
6 | const params = new URLSearchParams(location.search);
7 | const { orderDirection, orderBy } = option;
8 | orderBy != null && params.set('order_by', orderBy);
9 | orderDirection != null && params.set('order_direction', orderDirection);
10 | const search = params.toString();
11 |
12 | const calculatedScroll = typeof scroll === 'function' ? scroll() : scroll;
13 | return {
14 | ...location,
15 | search: search === '' ? '' : `?${search}`,
16 | state: {
17 | ...(location.state || {}),
18 | scroll: calculatedScroll,
19 | },
20 | };
21 | }
22 |
23 | const UnitSortModal = ({ order, orderOptions, scroll, isActive, location, onClickModalBackground, horizontalAlign }) => (
24 |
25 |
26 |
27 | {orderOptions.map(option => (
28 | -
29 |
30 | {option.title}
31 |
32 |
33 | ))}
34 |
35 |
36 |
37 | );
38 |
39 | export default withRouter(UnitSortModal);
40 |
--------------------------------------------------------------------------------
/src/components/Modal/index.jsx:
--------------------------------------------------------------------------------
1 | export * from './Modal';
2 | export * from './ModalBackground';
3 | export * from './ModalItem';
4 | export * from './ModalItemGroup';
5 |
--------------------------------------------------------------------------------
/src/components/PageAlert/index.jsx:
--------------------------------------------------------------------------------
1 | import AlertIcon from '../../svgs/ExclamationCircleFill.svg';
2 | import Arrow from '../../svgs/NoneDashedArrowRight.svg';
3 | import * as alertStyles from './styles';
4 |
5 | export const PageAlert = ({ alertMessage, linkURL }) => (
6 |
7 |
8 |
9 | {alertMessage}
10 | {linkURL && (
11 |
12 | 자세히 보기
13 |
14 |
15 | )}
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/components/PageAlert/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | export const wrapper = css`
4 | border-radius: 4px;
5 | background-color: #e6e8eb;
6 | padding: 13px 16px;
7 | display: flex;
8 | width: 100%;
9 | box-sizing: border-box;
10 | `;
11 |
12 | const alertIconSize = 16;
13 |
14 | export const alertIcon = css`
15 | width: ${alertIconSize}px;
16 | height: ${alertIconSize}px;
17 | fill: #9ea7ad;
18 | margin-right: 8px;
19 | flex: 0 0 auto;
20 | `;
21 |
22 | export const message = css`
23 | line-height: 1.38em;
24 | color: #525a61;
25 | font-size: 13px;
26 | word-break: keep-all;
27 | `;
28 |
29 | export const link = css`
30 | font-size: 13px;
31 | font-weight: bold;
32 | color: #525a61;
33 | text-decoration: underline;
34 | margin-left: 8px;
35 |
36 | &:link,
37 | &:visited {
38 | text-decoration: underline;
39 | }
40 | `;
41 |
42 | export const linkArrowIcon = css`
43 | width: 8px;
44 | height: 10px;
45 | fill: #9ea7ad;
46 | margin-top: 1px;
47 | margin-left: 2px;
48 | `;
49 |
--------------------------------------------------------------------------------
/src/components/PageRedirect.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Redirect, withRouter } from 'react-router-dom';
3 |
4 | function PageRedirect(props) {
5 | const { currentPage, location, totalPages } = props;
6 | if (totalPages <= 0) {
7 | return null;
8 | }
9 |
10 | const realPage = Math.max(1, Math.min(totalPages, currentPage));
11 | if (currentPage === realPage) {
12 | return null;
13 | }
14 |
15 | const newUrlParams = new URLSearchParams(location.search);
16 | newUrlParams.set('page', realPage);
17 | const newSearch = newUrlParams.toString();
18 | const to = {
19 | ...location,
20 | search: newSearch !== '' ? `?${newSearch}` : '',
21 | };
22 | return ;
23 | }
24 |
25 | export default withRouter(PageRedirect);
26 |
--------------------------------------------------------------------------------
/src/components/Scrollable.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class Scrollable extends React.Component {
4 | componentDidMount() {
5 | window.addEventListener('scroll', this.handleScroll);
6 | }
7 |
8 | componentWillUnmount() {
9 | window.removeEventListener('scroll', this.handleScroll);
10 | }
11 |
12 | handleScroll = () => {
13 | const { hasMore, isLoading, fetch, bottomOffset = 300 } = this.props;
14 |
15 | if (!hasMore || isLoading) {
16 | return;
17 | }
18 |
19 | if (window.innerHeight + window.scrollY >= document.body.offsetHeight - bottomOffset) {
20 | window.requestAnimationFrame(fetch);
21 | }
22 | };
23 |
24 | render() {
25 | const { children, isLoading, showLoader, loader = ...Loading
} = this.props;
26 | return (
27 | <>
28 | {children}
29 | {isLoading && showLoader ? loader : null}
30 | >
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/SearchBar/styles.js:
--------------------------------------------------------------------------------
1 | import { MQ, Responsive } from '../../styles/responsive';
2 |
3 | export const searchBar = {
4 | backgroundColor: '#f3f4f5',
5 | borderBottom: '1px solid #d1d5d9',
6 | boxShadow: '0 2px 10px 0 rgba(0, 0, 0, .04)',
7 | };
8 |
9 | export const searchBoxWrapper = {
10 | flex: 'auto',
11 | maxWidth: 600,
12 | '.hideTools & ': {
13 | ...MQ([Responsive.XSmall, Responsive.Small, Responsive.Medium, Responsive.Large], {
14 | maxWidth: '100%',
15 | }),
16 | },
17 | };
18 |
19 | export const toolsWrapper = {
20 | flex: 'auto',
21 | justifyContent: 'flex-end',
22 | display: 'flex',
23 | alignItems: 'center',
24 | paddingLeft: 2,
25 | whiteSpace: 'nowrap',
26 | '.hideTools & ': {
27 | ...MQ([Responsive.XSmall, Responsive.Small, Responsive.Medium, Responsive.Large], {
28 | display: 'none',
29 | }),
30 | },
31 | };
32 |
33 | export const cancelSearchButton = {
34 | display: 'block',
35 | marginLeft: 14,
36 | borderRadius: 4,
37 | boxShadow: '1px 1px 1px 0 rgba(0, 0, 0, .05)',
38 | backgroundColor: 'white',
39 | border: '1px solid #d1d5d9',
40 | width: 50,
41 | height: 28,
42 | lineHeight: '28px',
43 | fontSize: 13,
44 | fontWeight: 'bold',
45 | textAlign: 'center',
46 | color: '#808991',
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/SerialPreferenceBooks/FullButton.jsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | const fullButtonStyle = css`
4 | display: block;
5 | position: absolute;
6 | left: 0;
7 | top: 0;
8 | width: 100%;
9 | height: 100%;
10 | padding-right: 100px;
11 | box-sizing: border-box;
12 |
13 | a,
14 | button {
15 | display: block;
16 | width: 100%;
17 | height: 100%;
18 | font-size: 0;
19 | line-height: 0;
20 | color: transparent;
21 | }
22 | `;
23 |
24 | const FullButton = ({ children }) => {children}
;
25 |
26 | export default FullButton;
27 |
--------------------------------------------------------------------------------
/src/components/SerialPreferenceBooks/styles.js:
--------------------------------------------------------------------------------
1 | export const authorFieldSeparator = {
2 | display: 'inline-block',
3 | position: 'relative',
4 | width: 9,
5 | height: 16,
6 | verticalAlign: 'top',
7 | '::after': {
8 | content: `''`,
9 | display: 'block',
10 | width: 1,
11 | height: 9,
12 | background: '#d1d5d9',
13 | position: 'absolute',
14 | left: 4,
15 | top: 3,
16 | },
17 | };
18 |
19 | export const preferenceMeta = {
20 | marginTop: 4,
21 | fontSize: 12,
22 | lineHeight: '1.3em',
23 | color: '#808991',
24 | };
25 |
26 | export const unreadDot = {
27 | display: 'inline-block',
28 | width: 4,
29 | height: 4,
30 | background: '#5abf0d',
31 | borderRadius: 4,
32 | verticalAlign: '12%',
33 | marginRight: 4,
34 | };
35 |
36 | export const seriesComplete = {
37 | background: '#b3b3b3',
38 | borderRadius: 2,
39 | marginLeft: 4,
40 |
41 | padding: '0px 2px 0 1px',
42 | boxSizing: 'border-box',
43 | };
44 |
45 | export const seriesCompleteIcon = {
46 | fill: 'white',
47 | width: 16,
48 | height: 8,
49 | };
50 |
51 | export const button = {
52 | display: 'block',
53 | width: 68,
54 | lineHeight: '30px',
55 | borderRadius: 4,
56 | border: '1px solod #0077d9',
57 | boxShadow: '1px 1px 1px 0 rgba(31, 140, 230, 0.3)',
58 | backgroundColor: '#1f8ce6',
59 | fontSize: 12,
60 | fontWeight: 'bold',
61 | color: '#fff',
62 | textAlign: 'center',
63 | zIndex: 10,
64 | };
65 |
66 | export const buttonsWrapper = {
67 | '.LandscapeBook_Buttons': {
68 | zIndex: 10,
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/src/components/SerialPreferenceToolBar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import FlexBar from 'components/FlexBar';
4 | import { Editing } from 'components/Tool';
5 | import TotalCount from 'components/Tool/TotalCount';
6 |
7 | import * as styles from './styles';
8 |
9 | const SerialPreferenceToolBar = props => {
10 | const { toggleEditingMode, totalCount, isFetchingBooks } = props;
11 |
12 | return (
13 | : }
16 | flexRight={}
17 | />
18 | );
19 | };
20 |
21 | export default SerialPreferenceToolBar;
22 |
--------------------------------------------------------------------------------
/src/components/SerialPreferenceToolBar/styles.js:
--------------------------------------------------------------------------------
1 | export const toolBar = {
2 | boxSizing: 'border-box',
3 | borderBottom: '1px solid #d1d5d9',
4 |
5 | width: '100%',
6 | height: 46,
7 |
8 | backgroundColor: '#f3f4f5',
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/SeriesList/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import { ACTION_BAR_HEIGHT } from 'components/ActionBar/styles';
4 |
5 | export const wrapper = css`
6 | padding-bottom: ${ACTION_BAR_HEIGHT};
7 | `;
8 |
--------------------------------------------------------------------------------
/src/components/SeriesToolBar/styles.js:
--------------------------------------------------------------------------------
1 | export const seriesToolBar = {
2 | boxSizing: 'border-box',
3 | borderBottom: '1px solid #d1d5d9',
4 |
5 | width: '100%',
6 | height: 46,
7 |
8 | backgroundColor: '#f3f4f5',
9 | };
10 |
11 | export const buttonWrapper = {
12 | position: 'relative',
13 | };
14 |
15 | export const orderButton = {
16 | fontSize: 14,
17 | letterSpacing: -0.3,
18 | color: '#40474d',
19 | };
20 |
21 | export const arrow = {
22 | marginLeft: 5,
23 | width: 7,
24 | height: 7,
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/Shelf/ShelfDetailLink.jsx:
--------------------------------------------------------------------------------
1 | import { Link, withRouter } from 'react-router-dom';
2 |
3 | import { PageType, URLMap } from '../../constants/urls';
4 | import { makeLinkProps } from '../../utils/uri';
5 | import { shelfStyles } from './styles';
6 |
7 | const ShelfDetailLink = ({ uuid, name, location }) => {
8 | const { as } = URLMap[PageType.SHELF_DETAIL];
9 | const { to } = makeLinkProps({}, as({ uuid }));
10 | return (
11 |
20 | {name} 바로가기
21 |
22 | );
23 | };
24 |
25 | export default withRouter(ShelfDetailLink);
26 |
--------------------------------------------------------------------------------
/src/components/Shelf/ShelfSelectButton.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import { selectionActions } from 'services/selection/reducers';
4 |
5 | import { getIsItemSelected } from '../../services/selection/selectors';
6 | import CheckCircle from '../../svgs/CheckCircle.svg';
7 | import { shelfStyles } from './styles';
8 |
9 | const mapStateToProps = (state, props) => ({
10 | isSelected: getIsItemSelected(state, props.uuid),
11 | });
12 |
13 | const mapDispatchToProps = {
14 | onSelectedChange: selectionActions.toggleItem,
15 | };
16 |
17 | export const ShelfSelectButton = connect(
18 | mapStateToProps,
19 | mapDispatchToProps,
20 | )(props => {
21 | const { uuid, isActive, isSelected, onSelectedChange } = props;
22 | const id = `shelf-checkbox-${uuid}`;
23 | const toggleSelect = () => {
24 | onSelectedChange(uuid);
25 | };
26 | return isActive ? (
27 |
28 |
35 |
36 | ) : null;
37 | });
38 |
--------------------------------------------------------------------------------
/src/components/Shelf/ShelfThumbnail.jsx:
--------------------------------------------------------------------------------
1 | import { shelfStyles } from './styles';
2 |
3 | const THUMBNAIL_TOTAL_COUNT = 3;
4 | export const ShelfThumbnails = ({ thumbnailIds, shelfName }) => (
5 |
6 | {Array.from({ length: THUMBNAIL_TOTAL_COUNT }, (_, index) => {
7 | const thumbnailUrl = thumbnailIds[index] ? `//img.ridicdn.net/cover/${thumbnailIds[index]}/xxlarge` : '';
8 | const hasThumbnail = thumbnailUrl.length > 0;
9 | const key = hasThumbnail ? thumbnailUrl : `empty${index}`;
10 | return (
11 | -
12 |
13 | {hasThumbnail ? (
14 |

15 | ) : (
16 |
책장 구성도서 썸네일 영역
17 | )}
18 |
19 |
20 | );
21 | })}
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/components/Shelves/index.jsx:
--------------------------------------------------------------------------------
1 | import Shelf from '../Shelf';
2 | import { ShelvesWrapper } from '../ShelvesWrapper';
3 |
4 | export const Shelves = ({ shelfIds, selectMode, renderLink }) => (
5 |
6 | {shelfIds.map(uuid => (
7 |
8 |
9 |
10 | ))}
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/components/ShelvesWrapper/index.jsx:
--------------------------------------------------------------------------------
1 | import { responsiveStyles } from './styles';
2 |
3 | export const ShelvesWrapper = ({ children }) => {children}
;
4 |
--------------------------------------------------------------------------------
/src/components/SimpleShelvesWrapper/SimpleShelvesItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import * as styles from './styles';
4 |
5 | const SimpleShelvesItem = ({ children }) => {children};
6 |
7 | export default SimpleShelvesItem;
8 |
--------------------------------------------------------------------------------
/src/components/SimpleShelvesWrapper/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import * as styles from './styles';
4 |
5 | const SimpleShelvesWrapper = ({ children }) => ;
6 |
7 | export default SimpleShelvesWrapper;
8 |
--------------------------------------------------------------------------------
/src/components/SimpleShelvesWrapper/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import { MQ, Responsive } from 'commonStyles/responsive';
4 |
5 | export const simpleShelves = css`
6 | display: flex;
7 | flex-wrap: wrap;
8 | padding: 0 10px 16px 10px;
9 |
10 | ${MQ(
11 | [Responsive.XSmall],
12 | `
13 | padding-left: 6px;
14 | padding-right: 6px;
15 | `,
16 | )}
17 | ${MQ(
18 | [Responsive.XLarge],
19 | `
20 | width: 800px;
21 | margin: 0 auto;
22 | `,
23 | )}
24 | ${MQ(
25 | [Responsive.XXLarge, Responsive.Full],
26 | `
27 | width: 1000px;
28 | margin: 0 auto;
29 | `,
30 | )}
31 | `;
32 |
33 | export const simpleShelvesItem = css`
34 | box-sizing: border-box;
35 | padding: 16px 10px 0 10px;
36 | width: 100%;
37 | ${MQ(
38 | [Responsive.Large, Responsive.XLarge, Responsive.XXLarge, Responsive.Full],
39 | `
40 | width: 50%;
41 | `,
42 | )}
43 | `;
44 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonBooks/LandscapeBook.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from './landscapeBookStyles';
2 |
3 | const LandscapeBook = () => (
4 |
13 | );
14 | export default LandscapeBook;
15 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonBooks/PortraitBook.jsx:
--------------------------------------------------------------------------------
1 | const styles = {
2 | portraitBook: {
3 | width: '100%',
4 | height: '100%',
5 | display: 'flex',
6 | alignItems: 'flex-end',
7 | },
8 | thumbnail: {
9 | width: '100%',
10 | height: 'auto',
11 | paddingBottom: '162%',
12 | backgroundImage: 'linear-gradient(147deg, #e6e8eb, #edeff2 55%, #e6e8eb)',
13 | },
14 | };
15 |
16 | const PortraitBook = () => (
17 |
20 | );
21 | export default PortraitBook;
22 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonBooks/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ViewType from '../../../constants/viewType';
4 | import * as styles from '../../../styles/books';
5 | import BooksWrapper from '../../BooksWrapper';
6 | import LandscapeBook from './LandscapeBook';
7 | import PortraitBook from './PortraitBook';
8 |
9 | const SkeletonBookCount = 48;
10 |
11 | const SkeletonBooks = ({ viewType }) => (
12 |
16 | viewType === ViewType.PORTRAIT ? (
17 |
20 | ) : (
21 |
22 |
23 |
24 | )
25 | }
26 | />
27 | );
28 |
29 | export default React.memo(SkeletonBooks);
30 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonBooks/landscapeBookStyles.js:
--------------------------------------------------------------------------------
1 | const backgroundImage = 'linear-gradient(147deg, #e6e8eb, #edeff2 55%, #e6e8eb)';
2 |
3 | export const landscapeBook = {
4 | width: '100%',
5 | height: '100%',
6 | position: 'relative',
7 | display: 'flex',
8 | alignItems: 'center',
9 | padding: '20px 24px 0 24px',
10 | borderBottom: '1px solid #eee',
11 | paddingTop: 20,
12 | };
13 |
14 | export const thumbnailWrapper = {
15 | paddingRight: 16,
16 | alignSelf: 'flex-end',
17 | flex: '0 0 auto',
18 | };
19 |
20 | export const thumbnail = {
21 | width: 60,
22 | height: 88,
23 | backgroundImage,
24 | };
25 |
26 | export const metadataWrapper = {
27 | flex: 'auto',
28 | padding: '10px 0',
29 | };
30 |
31 | export const title = {
32 | width: 180,
33 | height: 19,
34 | backgroundImage,
35 | };
36 |
37 | export const author = {
38 | width: 150,
39 | height: 16,
40 | marginTop: 4,
41 | backgroundImage,
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonShelves/index.jsx:
--------------------------------------------------------------------------------
1 | import { ShelvesWrapper } from '../../ShelvesWrapper';
2 | import { skeletonShelvesStyle } from './styles';
3 |
4 | const SKELETON_TOTAL_COUNT = 24;
5 | export const SkeletonShelves = () => (
6 |
7 | {Array.from({ length: SKELETON_TOTAL_COUNT }, (_, index) => (
8 |
9 |
10 | Now Loading
11 |
12 |
13 | ))}
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonShelves/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | export const skeletonShelvesStyle = css`
4 | width: inherit;
5 | min-height: inherit;
6 | background-image: linear-gradient(132deg, #e6e8eb, #edeff2 55%, #e6e8eb);
7 | `;
8 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonSimpleShelves/SkeletonSimpleShelf.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { css } from '@emotion/core';
3 |
4 | const shelfStyle = css`
5 | width: 100%;
6 | height: 82px;
7 | background-image: linear-gradient(to top, #e6e8eb, #edeff2, #e6e8eb);
8 | `;
9 |
10 | const SkeletonSimpleShelf = () => ;
11 |
12 | export default SkeletonSimpleShelf;
13 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonSimpleShelves/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SimpleShelvesWrapper from 'components/SimpleShelvesWrapper';
4 | import SimpleShelvesItem from 'components/SimpleShelvesWrapper/SimpleShelvesItem';
5 |
6 | import SkeletonSimpleShelf from './SkeletonSimpleShelf';
7 |
8 | const SKELETON_TOTAL_COUNT = 40;
9 | const skeletonIndex = Array.from({ length: SKELETON_TOTAL_COUNT }, (_, index) => index);
10 |
11 | const SkeletonSimpleShelves = () => {
12 | const renderSkeletonShelf = () =>
13 | skeletonIndex.map(index => (
14 |
15 |
16 |
17 | ));
18 |
19 | return {renderSkeletonShelf()};
20 | };
21 |
22 | export default SkeletonSimpleShelves;
23 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonUnitDetailView/index.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from './styles';
2 |
3 | const SkeletonUnitDetailView = () => (
4 |
15 | );
16 |
17 | export default SkeletonUnitDetailView;
18 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonUnitDetailView/styles.js:
--------------------------------------------------------------------------------
1 | import { MQ, Responsive } from '../../../styles/responsive';
2 | import * as defaultLayout from '../../../styles/unitDetailViewHeader';
3 |
4 | const backgroundImage = 'linear-gradient(147deg, #e6e8eb, #edeff2 55%, #e6e8eb)';
5 |
6 | export const header = {
7 | ...defaultLayout.header,
8 | };
9 | export const thumbnailWrapper = {
10 | ...defaultLayout.thumbnailWrapper,
11 | };
12 | export const thumbnail = {
13 | ...defaultLayout.thumbnail,
14 | backgroundImage,
15 | width: 130,
16 | height: 191,
17 | ...MQ([Responsive.XLarge, Responsive.XXLarge, Responsive.Full], {
18 | width: 180,
19 | height: 265,
20 | }),
21 | };
22 |
23 | export const outerTextLink = {
24 | ...defaultLayout.outerTextLink,
25 | backgroundImage,
26 | width: 130,
27 | height: 20,
28 | };
29 | export const infoWrapper = {
30 | ...defaultLayout.infoWrapper,
31 | };
32 | export const title = {
33 | backgroundImage,
34 | width: 300,
35 | height: 30,
36 | ...MQ([Responsive.XLarge, Responsive.XXLarge, Responsive.Full], {
37 | width: 400,
38 | height: 30,
39 | }),
40 | };
41 | export const authorList = {
42 | ...defaultLayout.authorList,
43 | backgroundImage,
44 | width: 250,
45 | height: 19,
46 | };
47 | export const fileInfo = {
48 | ...defaultLayout.fileInfo,
49 | backgroundImage,
50 | width: 250,
51 | height: 20,
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/TabBar/TabItem.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import { makeLinkProps } from '../../utils/uri';
4 | import * as styles from './styles';
5 |
6 | export const TabItem = ({ name, onClick, isActive }) => (
7 |
8 |
12 |
13 | );
14 |
15 | export const TabLinkItem = ({ name, as, query, isActive, icon }) => (
16 |
17 |
18 | {name}
19 | {icon && icon(isActive)}
20 |
21 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/components/TabBar/index.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from './styles';
2 |
3 | export const TabBar = ({ children }) => (
4 |
7 | );
8 |
9 | export * from './TabItem';
10 |
--------------------------------------------------------------------------------
/src/components/TabBar/styles.js:
--------------------------------------------------------------------------------
1 | import { Hoverable } from '../../styles/responsive';
2 |
3 | export const tabBar = {
4 | width: '100%',
5 | };
6 |
7 | export const tabItem = {
8 | display: 'inline-block',
9 | position: 'relative',
10 | verticalAlign: 'top',
11 | marginRight: 16,
12 | };
13 |
14 | export const tabButton = {
15 | position: 'relative',
16 | display: 'block',
17 | padding: '0 2px',
18 | height: 40,
19 | lineHeight: '40px',
20 | color: '#808991',
21 | fontSize: 16,
22 | textAlign: 'center',
23 | ...Hoverable({
24 | color: '#40474d',
25 | }),
26 | };
27 |
28 | export const tabButtonToggle = isActive =>
29 | isActive
30 | ? {
31 | color: '#40474d',
32 | fontWeight: 700,
33 | }
34 | : {};
35 |
36 | export const activeBar = {
37 | display: 'block',
38 | position: 'absolute',
39 | left: 0,
40 | bottom: 0,
41 | width: '100%',
42 | height: 2,
43 | background: 'transparent',
44 | transition: 'background .3s',
45 | };
46 |
47 | export const activeBarToggle = isActive =>
48 | isActive
49 | ? {
50 | background: '#9ea7ad',
51 | }
52 | : {};
53 |
--------------------------------------------------------------------------------
/src/components/TitleBar/Title.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import ArrowLeft from 'svgs/ArrowLeft.svg';
4 | import { thousandsSeperator } from 'utils/number';
5 |
6 | import * as styles from './styles';
7 |
8 | const Title = ({ title, showCount, totalCount, to, onBackClick, invertColor }) => {
9 | const BackButton = ({ WrapperComponent, ...wrapperProps }) => (
10 |
11 |
12 | 뒤로가기
13 |
14 | );
15 |
16 | return (
17 |
18 | {to && }
19 | {onBackClick && }
20 |
21 | {title}
22 | {showCount ? {totalCount ? thousandsSeperator(totalCount) : ''} : null}
23 |
24 |
25 | );
26 | };
27 |
28 | export default Title;
29 |
--------------------------------------------------------------------------------
/src/components/TitleBar/index.jsx:
--------------------------------------------------------------------------------
1 | import FlexBar from 'components/FlexBar';
2 | import * as searchBarStyles from 'components/SearchBar/styles';
3 | import Editing from 'components/Tool/Editing';
4 | import More from 'components/Tool/More';
5 |
6 | import * as styles from './styles';
7 | import Title from './Title';
8 |
9 | const TitleBar = ({
10 | backLocation,
11 | title,
12 | showCount,
13 | totalCount,
14 | showTools,
15 | toggleEditingMode,
16 | onBackClick,
17 | right,
18 | invertColor = false,
19 | }) => {
20 | const renderLeft = () => (
21 |
29 | );
30 | const renderRight = () => {
31 | if (showTools) {
32 | return (
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | if (right) return right;
40 | return null;
41 | };
42 | return (
43 |
49 | );
50 | };
51 |
52 | export default TitleBar;
53 |
--------------------------------------------------------------------------------
/src/components/Toast.jsx:
--------------------------------------------------------------------------------
1 | import isAfter from 'date-fns/is_after';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 |
5 | import { showToast } from '../services/toast/actions';
6 | import { Duration, ToastStyle } from '../services/toast/constants';
7 | import settings from '../utils/settings';
8 |
9 | class Toast extends React.Component {
10 | componentDidMount() {
11 | const {
12 | name,
13 | expires,
14 | showToast: dispatchShowToast,
15 | message,
16 | linkName,
17 | linkProps,
18 | outLink,
19 | duration = Duration.NORMAL,
20 | toastStyle = ToastStyle.GREEN,
21 | } = this.props;
22 |
23 | if (expires && isAfter(new Date(), expires)) {
24 | return;
25 | }
26 |
27 | const _val = settings.get(name);
28 | if (!_val) {
29 | settings.set(name, true, { path: '/', expires });
30 | dispatchShowToast({
31 | message,
32 | linkName,
33 | linkProps,
34 | outLink,
35 | duration,
36 | toastStyle,
37 | });
38 | }
39 | }
40 |
41 | render() {
42 | return null;
43 | }
44 | }
45 |
46 | const mapDispatchToProps = { showToast };
47 | export default connect(null, mapDispatchToProps)(Toast);
48 |
--------------------------------------------------------------------------------
/src/components/Tool/Add.jsx:
--------------------------------------------------------------------------------
1 | import PlusIcon from '../../svgs/Plus.svg';
2 | import IconButton from '../IconButton';
3 | import * as styles from './styles';
4 |
5 | export const Add = props => {
6 | const { onClickAddButton, children } = props;
7 | return (
8 |
9 |
10 |
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Tool/Editing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Edit from '../../svgs/Edit.svg';
4 | import IconButton from '../IconButton';
5 | import * as styles from './styles';
6 |
7 | const Editing = props => {
8 | const { toggleEditingMode } = props;
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Editing;
22 |
--------------------------------------------------------------------------------
/src/components/Tool/Filter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CategoryFilter from '../../svgs/CategoryFilter.svg';
4 | import On from '../../svgs/On.svg';
5 | import IconButton from '../IconButton';
6 | import FilterModal from '../Modal/FilterModal';
7 | import * as styles from './styles';
8 |
9 | export default function Filter({ filter, filterOptions, onFilterChange }) {
10 | const [isFilterModalOpen, setFilterModalOpen] = React.useState(false);
11 |
12 | React.useEffect(() => {
13 | setFilterModalOpen(false);
14 | }, [filter]);
15 |
16 | return (
17 |
18 |
{
22 | setFilterModalOpen(true);
23 | }}
24 | >
25 |
26 |
27 |
28 | {filter && (
29 |
30 |
31 |
32 | )}
33 |
34 | {isFilterModalOpen && (
35 |
{
40 | setFilterModalOpen(false);
41 | }}
42 | />
43 | )}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Tool/More.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import IconButton from 'components/IconButton';
4 | import MoreModal from 'components/Modal/MoreModal';
5 | import ThreeDotsVertical from 'svgs/ThreeDotsVertical.svg';
6 |
7 | import * as styles from './styles';
8 |
9 | const More = ({ orderOptions, orderBy, orderDirection, query, showViewType, showOrder, showHidden }) => {
10 | const [modalVisiblity, setModalVisibility] = React.useState(false);
11 | return (
12 |
13 |
{
17 | setModalVisibility(true);
18 | }}
19 | >
20 |
21 |
22 |
23 |
24 | {modalVisiblity && (
25 |
{
32 | setModalVisibility(false);
33 | }}
34 | showViewType={showViewType}
35 | showOrder={showOrder}
36 | showHidden={showHidden}
37 | />
38 | )}
39 |
40 | );
41 | };
42 |
43 | export default More;
44 |
--------------------------------------------------------------------------------
/src/components/Tool/TotalCount.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from './styles';
2 |
3 | const TotalCount = ({ count, unit }) =>
4 | count && count > 0 ? (
5 | 총 {`${count}${unit}`}
6 | ) : (
7 |
8 | 0{unit}
9 |
10 | );
11 |
12 | export default TotalCount;
13 |
--------------------------------------------------------------------------------
/src/components/Tool/index.jsx:
--------------------------------------------------------------------------------
1 | import { Add } from './Add';
2 | import Editing from './Editing';
3 | import Filter from './Filter';
4 | import More from './More';
5 | import ShelfEdit from './ShelfEdit';
6 | import ShelfOrder from './ShelfOrder';
7 |
8 | export { Add, Editing, Filter, More, ShelfEdit, ShelfOrder };
9 |
--------------------------------------------------------------------------------
/src/components/Tool/styles.js:
--------------------------------------------------------------------------------
1 | import { Hoverable } from '../../styles/responsive';
2 |
3 | const buttonSize = 24;
4 |
5 | const defaultStyle = {
6 | position: 'absolute',
7 | left: '50%',
8 | top: '50%',
9 | transform: 'translate3d(-50%, -50%, 0)',
10 | };
11 |
12 | export const buttonWrapper = {
13 | position: 'relative',
14 | marginLeft: 12,
15 | };
16 |
17 | export const iconButton = isActive => ({
18 | position: 'relative',
19 | padding: 3,
20 | borderRadius: 3,
21 | lineHeight: 0,
22 | whiteSpace: 'nowrap',
23 | background: isActive ? '#e6e8eb' : null,
24 | fill: '#40474d',
25 | ...Hoverable({
26 | background: '#e6e8eb',
27 | }),
28 | });
29 |
30 | export const iconWrapper = {
31 | width: buttonSize,
32 | height: buttonSize,
33 | display: 'inline-block',
34 | position: 'relative',
35 | };
36 |
37 | export const categoryFilterIcon = {
38 | ...defaultStyle,
39 | width: 18,
40 | height: 12,
41 | };
42 |
43 | export const editIcon = {
44 | ...defaultStyle,
45 | width: 20,
46 | height: 20,
47 | };
48 |
49 | export const threeDotsIcon = {
50 | ...defaultStyle,
51 | width: 4,
52 | height: 16,
53 | };
54 |
55 | export const onIcon = {
56 | ...defaultStyle,
57 | width: 24,
58 | height: 16,
59 | marginRignt: 2,
60 | fill: '#008deb',
61 | };
62 |
63 | export const totalCount = {
64 | fontSize: 13,
65 | color: '#525A61',
66 | fontWeight: 'normal',
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/Tooltip/TooltipBackground.jsx:
--------------------------------------------------------------------------------
1 | import * as styles from './styles';
2 |
3 | export const TooltipBackground = ({ isActive, onClickTooltipBackground }) => (
4 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Tooltip/index.jsx:
--------------------------------------------------------------------------------
1 | import isBefore from 'date-fns/is_before';
2 | import React from 'react';
3 |
4 | import CheckIcon from '../../svgs/Check.svg';
5 | import settings from '../../utils/settings';
6 | import * as toolTipStyles from './styles';
7 | import { TooltipBackground } from './TooltipBackground';
8 |
9 | export const Tooltip = ({ children, name, expires, style, horizontalAlign }) => {
10 | const [isActive, setActive] = React.useState(false);
11 |
12 | React.useEffect(() => {
13 | const isTooltipActive = !settings.get(name);
14 | if (expires && isBefore(new Date(), expires) && isTooltipActive) {
15 | settings.set(name, true, { path: '/', expires });
16 | setActive(isTooltipActive);
17 | }
18 | }, [name]);
19 |
20 | React.useEffect(() => {
21 | function handleScroll() {
22 | setActive(false);
23 | window.removeEventListener('scroll', handleScroll);
24 | }
25 |
26 | if (isActive) {
27 | window.addEventListener('scroll', handleScroll);
28 | return () => window.removeEventListener('scroll', handleScroll);
29 | }
30 | return null;
31 | }, [isActive]);
32 |
33 | const onClickTooltipBackground = React.useCallback(() => setActive(false), []);
34 |
35 | return isActive ? (
36 | <>
37 |
38 | {children}
39 |
40 |
41 |
42 |
43 |
44 | >
45 | ) : null;
46 | };
47 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /* global __CONFIG__ */
2 | export default __CONFIG__;
3 |
--------------------------------------------------------------------------------
/src/constants/authorRole.ts:
--------------------------------------------------------------------------------
1 | enum AuthorRole {
2 | AUTHOR = 'author',
3 | COMIC_AUTHOR = 'comic_author',
4 | TRANSLATOR = 'translator',
5 | PHOTO = 'author_photo',
6 | BIBLIO_INTRO = 'bibliographical_introduction',
7 | COMMENTATOR = 'commentator',
8 | EDITOR = 'editor',
9 | ILLUSTRATOR = 'illustrator',
10 | ORIG_AUTHOR = 'original_author',
11 | PLANNER = 'planner',
12 | STORY_WRITER = 'story_writer',
13 | SUPERVISOR = 'supervise',
14 | PERFORMER = 'performer',
15 | COMPILER = 'compiler',
16 | ORIG_ILLUSTRATOR = 'original_illustrator',
17 | }
18 |
19 | export const ROLE_DESCRIPTIONS = {
20 | [AuthorRole.COMIC_AUTHOR]: '글, 그림',
21 | [AuthorRole.AUTHOR]: '저',
22 | [AuthorRole.STORY_WRITER]: '글',
23 | [AuthorRole.ILLUSTRATOR]: '그림',
24 | [AuthorRole.TRANSLATOR]: '역',
25 | [AuthorRole.PHOTO]: '사진',
26 | [AuthorRole.BIBLIO_INTRO]: '해제',
27 | [AuthorRole.COMMENTATOR]: '해설',
28 | [AuthorRole.EDITOR]: '편집',
29 | [AuthorRole.ORIG_AUTHOR]: '원작',
30 | [AuthorRole.ORIG_ILLUSTRATOR]: '원화',
31 | [AuthorRole.PLANNER]: '기획',
32 | [AuthorRole.SUPERVISOR]: '감수',
33 | [AuthorRole.PERFORMER]: '연주자',
34 | [AuthorRole.COMPILER]: '엮음',
35 | };
36 |
37 | export default AuthorRole;
38 |
--------------------------------------------------------------------------------
/src/constants/category.js:
--------------------------------------------------------------------------------
1 | export default class Genre {
2 | static get COMIC() {
3 | return 'comic';
4 | }
5 |
6 | static get FANTASY() {
7 | return 'fantasy';
8 | }
9 |
10 | static get ROMANCE() {
11 | return 'romance';
12 | }
13 |
14 | static get GENERAL() {
15 | return 'general';
16 | }
17 |
18 | static get BL() {
19 | return 'bl';
20 | }
21 |
22 | static convertToString(key) {
23 | const stringMap = {
24 | [this.COMIC]: '만화',
25 | [this.FANTASY]: '판타지',
26 | [this.ROMANCE]: '로맨스',
27 | [this.GENERAL]: '일반',
28 | [this.BL]: 'BL',
29 | };
30 | return stringMap[key];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/constants/environment.ts:
--------------------------------------------------------------------------------
1 | export enum ENV {
2 | LOCAL = 'local',
3 | DEV = 'development',
4 | STAGING = 'staging',
5 | PRODUCTION = 'production',
6 | }
7 |
--------------------------------------------------------------------------------
/src/constants/featureIds.js:
--------------------------------------------------------------------------------
1 | export const SYNC_SHELF = '29fda26b-94fe-4b97-8953-b4024a388fc8';
2 |
--------------------------------------------------------------------------------
/src/constants/listInstructions.js:
--------------------------------------------------------------------------------
1 | export const ListInstructions = {
2 | SHOW: 'show',
3 | SKELETON: 'skeleton',
4 | EMPTY: 'empty',
5 | };
6 |
--------------------------------------------------------------------------------
/src/constants/page.js:
--------------------------------------------------------------------------------
1 | export const PAGE_COUNT = 10;
2 | export const MOBILE_PAGE_COUNT = 5;
3 |
4 | export const LIBRARY_ITEMS_LIMIT_PER_PAGE = 48;
5 | export const SERIAL_PREFERENCE_ITEMS_LIMIT_PER_PAGE = 48;
6 | export const SHELVES_LIMIT_PER_PAGE = 24;
7 |
--------------------------------------------------------------------------------
/src/constants/serviceType.js:
--------------------------------------------------------------------------------
1 | export class ServiceType {
2 | static get NORMAL() {
3 | return 'normal';
4 | }
5 |
6 | static get RENT() {
7 | return 'rent';
8 | }
9 |
10 | static get FLATRATE() {
11 | return 'flatrate';
12 | }
13 |
14 | static get SELECT() {
15 | return 'ridiselect';
16 | }
17 |
18 | static get STORE() {
19 | return 'ridibooks';
20 | }
21 |
22 | static isExpirable(serviceType) {
23 | return serviceType === this.RENT || serviceType === this.FLATRATE || serviceType === this.SELECT;
24 | }
25 |
26 | static isRidiselect(serviceType) {
27 | return serviceType === this.SELECT;
28 | }
29 |
30 | static includes(value) {
31 | return value === this.NORMAL || value === this.RENT || value === this.FLATRATE || value === this.SELECT || value === this.STORE;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/constants/shelves.js:
--------------------------------------------------------------------------------
1 | export const SHELVES_LIMIT = 100;
2 | export const ITEMS_LIMIT_PER_SHELF = 1000;
3 | export const SHELF_NAME_LIMIT = 50;
4 |
5 | export const SHELF_OPERATION_LIMIT = 10;
6 | export const SHELF_ITEM_OPERATION_LIMIT = 100;
7 |
--------------------------------------------------------------------------------
/src/constants/unitType.js:
--------------------------------------------------------------------------------
1 | export class UnitType {
2 | static get BOOK() {
3 | return 'book';
4 | }
5 |
6 | static get SERIES() {
7 | return 'series';
8 | }
9 |
10 | static get SHELF() {
11 | return 'shelf';
12 | }
13 |
14 | static isBook(unitType) {
15 | return unitType === this.BOOK;
16 | }
17 |
18 | static isCollection(unitType) {
19 | // 사내에서 커뮤니케이션시 '콜렉션 도서' 라고 부르는 도서들의 type 이 'shelf' 로 넘어옴.
20 | return unitType === this.SHELF;
21 | }
22 |
23 | static isSeries(unitType) {
24 | return unitType === this.SERIES;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/constants/viewType.js:
--------------------------------------------------------------------------------
1 | export default class ViewType {
2 | static get LANDSCAPE() {
3 | return 'landscape';
4 | }
5 |
6 | static get PORTRAIT() {
7 | return 'portrait';
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import 'core-js/stable';
2 | import 'regenerator-runtime/runtime';
3 |
4 | import createCache from '@emotion/cache';
5 | import { CacheProvider } from '@emotion/core';
6 | import React from 'react';
7 | import ReactDOM from 'react-dom';
8 | import { Provider } from 'react-redux';
9 | import { BrowserRouter } from 'react-router-dom';
10 |
11 | import App from './App';
12 | import ScrollManager from './ScrollManager';
13 | import { makeStoreWithApi } from './store';
14 | import TrackManager from './TrackManager';
15 |
16 | const store = makeStoreWithApi({}, {});
17 | const styleCache = createCache();
18 |
19 | const container = document.getElementById('app');
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ,
30 | container,
31 | );
32 | container.style.height = '';
33 |
--------------------------------------------------------------------------------
/src/pages/base/Environment/index.jsx:
--------------------------------------------------------------------------------
1 | import { ENV } from 'constants/environment';
2 |
3 | import config from '../../../config';
4 |
5 | const environmentBandStyle = environment => {
6 | let background = '';
7 | if (environment === ENV.LOCAL) {
8 | background = 'darkgray';
9 | } else if (environment === ENV.DEV) {
10 | background = 'green';
11 | } else if (environment === ENV.STAGING) {
12 | background = 'red';
13 | }
14 |
15 | return {
16 | display: 'block',
17 | textAlign: 'center',
18 | padding: '4px',
19 | fontSize: '12px',
20 | fontWeight: 900,
21 | color: 'white',
22 | background,
23 | };
24 | };
25 |
26 | export const Environment = () => {
27 | const { ENVIRONMENT: environment } = config;
28 | switch (environment) {
29 | case ENV.LOCAL:
30 | case ENV.DEV:
31 | return {environment}
;
32 | case ENV.STAGING:
33 | return (
34 |
35 | {environment}
36 |
37 | );
38 | default:
39 | return null;
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/pages/base/GNB/FamilyServices.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import config from '../../../config';
4 | import { Hidden } from '../../../styles';
5 | import LogoRidibooks from '../../../svgs/LogoRidibooks.svg';
6 | import LogoRidiselect from '../../../svgs/LogoRidiselect.svg';
7 | import * as styles from './styles';
8 |
9 | const FamilyServices = () => (
10 |
24 | );
25 |
26 | export default React.memo(FamilyServices);
27 |
--------------------------------------------------------------------------------
/src/pages/base/LNB/SearchAndEditingBar.jsx:
--------------------------------------------------------------------------------
1 | import EditingBar from '../../../components/EditingBar';
2 | import SearchBar from '../../../components/SearchBar';
3 | import * as styles from './styles';
4 |
5 | export const SearchAndEditingBar = ({ searchBarProps, editingBarProps }) => (
6 |
7 |
8 | {editingBarProps.isEditing && }
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/pages/base/LNB/TabBar.jsx:
--------------------------------------------------------------------------------
1 | import { TabBar as LNBTabBar, TabLinkItem } from '../../../components/TabBar';
2 | import { URLMap } from '../../../constants/urls';
3 | import Responsive from '../Responsive';
4 |
5 | export const TabMenuTypes = {
6 | ALL_BOOKS: 'ALL BOOKS',
7 | SHELVES: 'SHELVES',
8 | SERIAL_PREFERENCE: 'SERIAL_PREFERENCE',
9 | SHELF_LIST: 'SHELF_LIST',
10 | };
11 |
12 | const styles = {
13 | LNBTabBarWrapper: {
14 | height: 40,
15 | backgroundColor: '#ffffff',
16 | borderBottom: '1px solid #d1d5d9',
17 | },
18 | };
19 |
20 | const TabMenus = [
21 | {
22 | type: TabMenuTypes.ALL_BOOKS,
23 | name: '모든 책',
24 | linkInfo: {
25 | href: URLMap.main.href,
26 | as: URLMap.main.as,
27 | },
28 | },
29 | {
30 | type: TabMenuTypes.SHELVES,
31 | name: '책장',
32 | linkInfo: {
33 | href: URLMap.shelves.href,
34 | as: URLMap.shelves.as,
35 | },
36 | },
37 | {
38 | type: TabMenuTypes.SERIAL_PREFERENCE,
39 | name: '선호 작품',
40 | linkInfo: {
41 | href: URLMap.serialPreference.href,
42 | as: URLMap.serialPreference.as,
43 | },
44 | },
45 | ];
46 |
47 | export const TabBar = ({ activeMenu }) => {
48 | const menus = TabMenus;
49 | const menuNodes = menus.map(menu => (
50 |
51 | ));
52 | return (
53 |
54 | {menuNodes}
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/pages/base/LNB/TitleAndEditingBar.jsx:
--------------------------------------------------------------------------------
1 | import EditingBar from '../../../components/EditingBar';
2 | import TitleBar from '../../../components/TitleBar';
3 | import * as styles from './styles';
4 |
5 | export const TitleAndEditingBar = ({ titleBarProps, editingBarProps }) => (
6 |
7 |
8 | {editingBarProps.isEditing && }
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/pages/base/LNB/index.jsx:
--------------------------------------------------------------------------------
1 | export * from './SearchAndEditingBar';
2 | export * from './TabBar';
3 | export * from './TitleAndEditingBar';
4 |
--------------------------------------------------------------------------------
/src/pages/base/LNB/styles.js:
--------------------------------------------------------------------------------
1 | export const flexWrapper = {
2 | display: 'flex',
3 | height: 44,
4 | alignItems: 'center',
5 | justifyContent: 'space-between',
6 | };
7 |
8 | export const LNBWrapper = {
9 | position: 'relative',
10 | };
11 |
--------------------------------------------------------------------------------
/src/pages/base/PageLoadingSpinner/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Filler from 'components/Filler';
4 | import FullScreenLoading from 'components/FullScreenLoading';
5 |
6 | const PageLoadingSpinner = () => {
7 | const [visible, setVisible] = React.useState(false);
8 | React.useEffect(() => {
9 | const timer = setTimeout(() => setVisible(true), 300);
10 | return () => clearTimeout(timer);
11 | }, []);
12 |
13 | return (
14 | <>
15 |
16 | {visible && }
17 | >
18 | );
19 | };
20 |
21 | export default PageLoadingSpinner;
22 |
--------------------------------------------------------------------------------
/src/pages/base/Responsive/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import * as styles from './styles';
4 |
5 | const Responsive: React.FC<{ className?: string }> = ({ className, children }) => {
6 | const hasPadding = true;
7 | const minHeight = false;
8 | return (
9 |
12 | );
13 | };
14 |
15 | export const ResponsiveBooks: React.FC<{ className?: string }> = ({ className, children }) => {
16 | const hasPadding = false;
17 | const minHeight = true;
18 | return (
19 |
22 | );
23 | };
24 |
25 | export default Responsive;
26 |
--------------------------------------------------------------------------------
/src/pages/base/Responsive/styles.js:
--------------------------------------------------------------------------------
1 | import { maxWidthWrapper } from '../../../styles';
2 |
3 | export const responsive = (hasPadding, minHeight) => ({
4 | position: 'relative',
5 | width: '100%',
6 | padding: hasPadding ? '0 16px' : '0',
7 | minHeight: minHeight ? 350 : 'auto',
8 | margin: '0 auto',
9 | boxSizing: 'border-box',
10 | ...maxWidthWrapper,
11 | });
12 |
--------------------------------------------------------------------------------
/src/pages/base/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import { reset } from '../../styles';
4 |
5 | const libraryBodyStyles = css`
6 | body {
7 | position: relative;
8 | min-width: 320px;
9 | min-height: 100vh;
10 | box-sizing: border-box;
11 | background: white;
12 |
13 | &.focus-free * {
14 | outline: none;
15 | -webkit-tap-highlight-color: transparent;
16 | }
17 |
18 | &.disable-scroll {
19 | overflow: hidden;
20 | }
21 |
22 | &::after {
23 | content: '';
24 | display: block;
25 | position: absolute;
26 | left: 0;
27 | top: 0;
28 | z-index: -1;
29 | width: 100%;
30 | height: 100%;
31 | background: #f3f4f5;
32 | }
33 | }
34 | `;
35 |
36 | export const globalStyles = css([reset, libraryBodyStyles]);
37 |
--------------------------------------------------------------------------------
/src/pages/errors/notFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { NotFoundError } from '../../components/Error';
4 | import HorizontalRuler from '../../components/HorizontalRuler';
5 |
6 | const NotFound = () => (
7 | <>
8 |
9 |
10 | >
11 | );
12 |
13 | export default NotFound;
14 |
--------------------------------------------------------------------------------
/src/pages/errors/pageLoad.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { PageLoadError } from '../../components/Error';
4 | import HorizontalRuler from '../../components/HorizontalRuler';
5 |
6 | const PageLoad = () => (
7 | <>
8 |
9 |
10 | >
11 | );
12 |
13 | export default PageLoad;
14 |
--------------------------------------------------------------------------------
/src/pages/purchased/hidden/styles.js:
--------------------------------------------------------------------------------
1 | export const hiddenButtonActionLeft = {
2 | color: '#e64938',
3 | float: 'left',
4 | };
5 |
6 | export const hiddenButtonActionRight = {
7 | float: 'right',
8 | };
9 |
--------------------------------------------------------------------------------
/src/pages/purchased/main/styles.js:
--------------------------------------------------------------------------------
1 | export const mainButtonActionLeft = {
2 | float: 'left',
3 | };
4 |
5 | export const mainButtonActionRight = {
6 | float: 'right',
7 | };
8 |
9 | export const book = {
10 | width: 80,
11 | };
12 |
--------------------------------------------------------------------------------
/src/pages/serialPreference/styles.js:
--------------------------------------------------------------------------------
1 | export const mainButtonActionLeft = {
2 | float: 'left',
3 | };
4 |
5 | export const mainButtonActionRight = {
6 | float: 'right',
7 | };
8 |
9 | export const book = {
10 | width: 80,
11 | };
12 |
--------------------------------------------------------------------------------
/src/pages/shelves/detail/SelectShelf/SimpleShelves/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 |
4 | import SimpleShelvesWrapper from 'components/SimpleShelvesWrapper';
5 | import SimpleShelvesItem from 'components/SimpleShelvesWrapper/SimpleShelvesItem';
6 | import { selectionActions } from 'services/selection/reducers';
7 | import { getSelectedShelfIds } from 'services/selection/selectors';
8 |
9 | import SimpleShelf from './SimpleShelf';
10 |
11 | const SimpleShelves = ({ shelfIds }) => {
12 | const dispatch = useDispatch();
13 | const selectedShelfIds = useSelector(getSelectedShelfIds);
14 | const handleShelfSelectChange = shelfId => {
15 | dispatch(selectionActions.clearSelectedShelves());
16 | dispatch(selectionActions.selectShelves([shelfId]));
17 | };
18 | const renderShelves = () =>
19 | shelfIds.map(shelfId => (
20 |
21 |
22 |
23 | ));
24 |
25 | return {renderShelves()};
26 | };
27 |
28 | export default SimpleShelves;
29 |
--------------------------------------------------------------------------------
/src/pages/shelves/detail/SelectShelf/styles.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/pages/shelves/detail/SelectShelf/styles.ts
--------------------------------------------------------------------------------
/src/pages/shelves/detail/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | export const shelfBar = css`
4 | background-color: white;
5 | border-top: 1px solid #f3f4f5;
6 | border-bottom: 1px solid #d1d5d9;
7 | margin-top: -1px;
8 | `;
9 |
10 | export const toolsWrapper = css`
11 | flex: auto;
12 | justify-content: flex-end;
13 | display: flex;
14 | align-items: center;
15 | padding-left: 2px;
16 | white-space: nowrap;
17 | `;
18 |
19 | export const toolBar = css`
20 | border-bottom: 1px solid #d1d5d9;
21 | box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.04);
22 | background-color: #f3f4f5;
23 | `;
24 |
--------------------------------------------------------------------------------
/src/pages/shelves/list/BetaAlert.jsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import { PageAlert } from 'components/PageAlert';
4 | import { responsiveStylesWidth } from 'components/ShelvesWrapper/styles';
5 | import { MQ, Responsive } from 'commonStyles/responsive';
6 |
7 | import config from '../../../config';
8 |
9 | const styles = {
10 | alertWrapper: css`
11 | margin-top: 20px;
12 | ${MQ([Responsive.XSmall, Responsive.Small], 'margin-top: 16px;')}
13 | `,
14 | };
15 |
16 | export const BetaAlert = () => (
17 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/pages/shelves/list/styles.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | export const toolBar = css`
4 | border-bottom: 1px solid #d1d5d9;
5 | box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.04);
6 | background-color: #f3f4f5;
7 | `;
8 |
9 | export const toolsWrapper = css`
10 | display: flex;
11 | `;
12 |
--------------------------------------------------------------------------------
/src/services/account/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_NEED_LOGIN = 'SET_NEED_LOGIN';
2 | export const SET_USER_INFO = 'SET_USER_INFO';
3 |
4 | export const setUserInfo = userInfo => ({
5 | type: SET_USER_INFO,
6 | payload: {
7 | userInfo,
8 | },
9 | });
10 |
11 | export const setNeedLogin = () => ({
12 | type: SET_NEED_LOGIN,
13 | });
14 |
--------------------------------------------------------------------------------
/src/services/account/errors.js:
--------------------------------------------------------------------------------
1 | export class NotAuthorizedError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/services/account/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { SET_NEED_LOGIN, SET_USER_INFO } from './actions';
4 |
5 | const initialState = {
6 | userInfo: null,
7 | needLogin: false,
8 | };
9 |
10 | const accountReducer = produce((draft, action) => {
11 | switch (action.type) {
12 | case SET_USER_INFO:
13 | draft.userInfo = action.payload.userInfo;
14 | draft.needLogin = false;
15 | break;
16 | case SET_NEED_LOGIN:
17 | draft.needLogin = true;
18 | break;
19 | default:
20 | break;
21 | }
22 | }, initialState);
23 |
24 | export default accountReducer;
25 |
--------------------------------------------------------------------------------
/src/services/account/requests.js:
--------------------------------------------------------------------------------
1 | import { put } from 'redux-saga/effects';
2 |
3 | import { getAPI } from '../../api/actions';
4 | import config from '../../config';
5 | import { makeURI } from '../../utils/uri';
6 | import { NotAuthorizedError } from './errors';
7 |
8 | export function* fetchUserInfo() {
9 | const api = yield put(getAPI());
10 |
11 | try {
12 | const response = yield api.get(makeURI('/accounts/me', {}, config.ACCOUNT_BASE_URL));
13 | return response.data.result;
14 | } catch (err) {
15 | throw new NotAuthorizedError();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/services/account/sagas.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/browser';
2 | import { all, call, delay, put, select } from 'redux-saga/effects';
3 |
4 | import { setNeedLogin, setUserInfo } from './actions';
5 | import { fetchUserInfo } from './requests';
6 |
7 | export function* loadUserInfo() {
8 | let userInfo;
9 | try {
10 | userInfo = yield call(fetchUserInfo);
11 | } catch (e) {
12 | yield put(setNeedLogin());
13 | return null;
14 | }
15 | Sentry.configureScope(scope => {
16 | scope.setUser({
17 | id: userInfo.idx,
18 | username: userInfo.id,
19 | email: userInfo.email,
20 | });
21 | });
22 | yield put(setUserInfo(userInfo));
23 | return userInfo;
24 | }
25 |
26 | function* accountTracker() {
27 | const TRACK_DELAY_MILLISECS = 1000 * 60 * 3;
28 |
29 | if (yield call(loadUserInfo)) {
30 | while (true) {
31 | yield delay(TRACK_DELAY_MILLISECS);
32 |
33 | let newUserInfo;
34 | try {
35 | newUserInfo = yield call(fetchUserInfo);
36 | } catch (e) {
37 | return;
38 | }
39 |
40 | const userInfo = yield select(state => state.account.userInfo);
41 | if (userInfo && userInfo.idx !== newUserInfo.idx) {
42 | window.location.reload();
43 | }
44 | }
45 | }
46 | }
47 |
48 | export default function* accountRootSaga() {
49 | yield all([accountTracker()]);
50 | }
51 |
--------------------------------------------------------------------------------
/src/services/account/selectors.js:
--------------------------------------------------------------------------------
1 | export const getUserInfo = state => state.account.userInfo;
2 | export const getNeedLogin = state => state.account.needLogin;
3 | export const getAdultVerification = state => Boolean(state.account.userInfo?.is_verified_adult);
4 |
--------------------------------------------------------------------------------
/src/services/book/constants.js:
--------------------------------------------------------------------------------
1 | export class BookFileType {
2 | static get EPUB() {
3 | return 'epub';
4 | }
5 |
6 | static get BOM() {
7 | return 'bom';
8 | }
9 |
10 | static get PDF() {
11 | return 'pdf';
12 | }
13 |
14 | static get WEBTOON() {
15 | return 'webtoon';
16 | }
17 |
18 | static convertToString(fileType) {
19 | const stringMap = {
20 | [this.EPUB]: 'EPUB',
21 | [this.BOM]: 'BOM',
22 | [this.PDF]: 'PDF',
23 | [this.WEBTOON]: '웹툰',
24 | };
25 | return stringMap[fileType];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/services/bookDownload/errors.ts:
--------------------------------------------------------------------------------
1 | export class DownloadError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/services/bookDownload/reducers.ts:
--------------------------------------------------------------------------------
1 | import { createActionCreators, createReducerFunction, ImmerReducer } from 'immer-reducer';
2 |
3 | interface BookdownloadState {
4 | src: null | string;
5 | }
6 |
7 | const bookDownloadState: BookdownloadState = {
8 | src: null,
9 | };
10 |
11 | export type BookIds = string[];
12 |
13 | export class BookDownloadReducer extends ImmerReducer {
14 | public downloadBooks(bookIds: BookIds) {}
15 | public downloadSelectedBooks() {}
16 | public downloadBooksByUnitIds(unidIds: BookIds) {}
17 |
18 | public setBookDownloadSrc(payload: BookdownloadState) {
19 | this.draftState.src = payload.src;
20 | }
21 | }
22 |
23 | export const bookDownloadReducer = createReducerFunction(BookDownloadReducer, bookDownloadState);
24 | export const bookDownloadActions = createActionCreators(BookDownloadReducer);
25 |
--------------------------------------------------------------------------------
/src/services/bookDownload/requests.ts:
--------------------------------------------------------------------------------
1 | import { stringify } from 'qs';
2 | import { put } from 'redux-saga/effects';
3 | import * as R from 'runtypes';
4 |
5 | import { makeURI } from 'utils/uri';
6 |
7 | import { getAPI } from '../../api/actions';
8 | import config from '../../config';
9 | import { BookIds } from './reducers';
10 |
11 | const RDownload = R.Record({
12 | b_ids: R.Array(R.String),
13 | result: R.Boolean,
14 | url: R.String,
15 | });
16 |
17 | export function* triggerDownload(bookIds: BookIds) {
18 | const query = {
19 | b_ids: bookIds,
20 | preprocess: true,
21 | };
22 |
23 | const api = yield put(getAPI());
24 | const response = yield api.post(makeURI('/api/user_books/trigger_download', {}, config.STORE_API_BASE_URL), stringify(query));
25 | const data = RDownload.check(response.data);
26 |
27 | return data;
28 | }
29 |
--------------------------------------------------------------------------------
/src/services/bookDownload/selectors.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const getState = state => state.bookDownload;
4 |
5 | export const getBookDownloadSrc = createSelector(getState, state => state.src);
6 |
--------------------------------------------------------------------------------
/src/services/common/actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_ACTUAL_PAGE = 'LOAD_ACTUAL_PAGE';
2 |
3 | export const loadActualPage = () => ({
4 | type: LOAD_ACTUAL_PAGE,
5 | });
6 |
--------------------------------------------------------------------------------
/src/services/common/constants.js:
--------------------------------------------------------------------------------
1 | export const DELETE_API_CHUNK_COUNT = 64;
2 |
--------------------------------------------------------------------------------
/src/services/common/errors.js:
--------------------------------------------------------------------------------
1 | export class HideError extends Error {}
2 | export class UnhideError extends Error {}
3 |
4 | export class MakeBookIdsError extends Error {}
5 |
--------------------------------------------------------------------------------
/src/services/confirm/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_CONFIRM = 'SET_CONFIRM';
2 | export const UNSET_CONFIRM = 'UNSET_CONFIRM';
3 |
4 | export const showConfirm = ({ title, message, confirmLabel, onClickConfirmButton }) => ({
5 | type: SET_CONFIRM,
6 | payload: {
7 | confirm: {
8 | title,
9 | message,
10 | confirmLabel,
11 | onClickConfirmButton,
12 | },
13 | },
14 | });
15 |
16 | export const closeConfirm = () => ({
17 | type: UNSET_CONFIRM,
18 | });
19 |
--------------------------------------------------------------------------------
/src/services/confirm/reducers.js:
--------------------------------------------------------------------------------
1 | import { SET_CONFIRM, UNSET_CONFIRM } from './actions';
2 | import { initialState } from './state';
3 |
4 | const confirmReducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case SET_CONFIRM:
7 | return action.payload.confirm;
8 | case UNSET_CONFIRM:
9 | return null;
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | export default confirmReducer;
16 |
--------------------------------------------------------------------------------
/src/services/confirm/state.js:
--------------------------------------------------------------------------------
1 | export const initialState = null;
2 |
--------------------------------------------------------------------------------
/src/services/dialog/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_DIALOG = 'SET_DIALOG';
2 | export const UNSET_DIALOG = 'UNSET_DIALOG';
3 |
4 | export const showDialog = (title, message) => ({
5 | type: SET_DIALOG,
6 | payload: {
7 | dialog: {
8 | title,
9 | message,
10 | },
11 | },
12 | });
13 |
14 | export const closeDialog = () => ({
15 | type: UNSET_DIALOG,
16 | });
17 |
--------------------------------------------------------------------------------
/src/services/dialog/reducers.js:
--------------------------------------------------------------------------------
1 | import { SET_DIALOG, UNSET_DIALOG } from './actions';
2 | import { initialState } from './state';
3 |
4 | const dialogReducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case SET_DIALOG:
7 | return action.payload.dialog;
8 | case UNSET_DIALOG:
9 | return null;
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | export default dialogReducer;
16 |
--------------------------------------------------------------------------------
/src/services/dialog/state.js:
--------------------------------------------------------------------------------
1 | export const initialState = null;
2 |
--------------------------------------------------------------------------------
/src/services/excelDownload/actions.js:
--------------------------------------------------------------------------------
1 | export const START_EXCEL_DOWNLOAD = 'START_EXCEL_DOWNLOAD';
2 | export const SET_EXCEL_DOWNLOAD_STATUS = 'SET_EXCEL_DOWNLOAD_STATUS';
3 |
4 | export const startExcelDownload = () => ({
5 | type: START_EXCEL_DOWNLOAD,
6 | });
7 |
8 | export const setExcelDownloadStatus = isDownloading => ({
9 | type: SET_EXCEL_DOWNLOAD_STATUS,
10 | payload: {
11 | isDownloading,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/services/excelDownload/constants.js:
--------------------------------------------------------------------------------
1 | export const CHECK_EXCEL_DOWNLOAD_STATUS_RETRY_DELAY = 1000;
2 |
3 | export const ExcelDownloadStatusCode = {
4 | UNDONE: 0,
5 | FAIL: 2,
6 | DONE: 1000,
7 | };
8 |
9 | export const EXCEL_FILE_NAME = 'ridi_book_list.xlsx';
10 |
--------------------------------------------------------------------------------
/src/services/excelDownload/reducers.js:
--------------------------------------------------------------------------------
1 | import { SET_EXCEL_DOWNLOAD_STATUS } from './actions';
2 | import { initialState } from './state';
3 |
4 | const excelDownloadReducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case SET_EXCEL_DOWNLOAD_STATUS:
7 | return {
8 | ...state,
9 | isExcelDownloading: action.payload.isDownloading,
10 | };
11 | default:
12 | return state;
13 | }
14 | };
15 |
16 | export default excelDownloadReducer;
17 |
--------------------------------------------------------------------------------
/src/services/excelDownload/requests.js:
--------------------------------------------------------------------------------
1 | import { put } from 'redux-saga/effects';
2 |
3 | import { getAPI } from '../../api/actions';
4 | import config from '../../config';
5 | import { makeURI } from '../../utils/uri';
6 |
7 | export function* fetchStartExcelDownload() {
8 | const options = {
9 | t: +new Date(),
10 | };
11 |
12 | const api = yield put(getAPI());
13 | const response = yield api.post(makeURI('/items/excel', options, config.LIBRARY_API_BASE_URL));
14 | return response.data;
15 | }
16 |
17 | export function* fetchCheckExcelDownload(queueId) {
18 | const options = {
19 | t: +new Date(),
20 | };
21 |
22 | const api = yield put(getAPI());
23 | const response = yield api.get(makeURI(`/items/excel/${queueId}`, options, config.LIBRARY_API_BASE_URL));
24 | return response.data;
25 | }
26 |
--------------------------------------------------------------------------------
/src/services/excelDownload/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const getState = state => state.excelDownload;
4 |
5 | export const getIsExcelDownloading = createSelector(getState, state => state.isExcelDownloading);
6 |
--------------------------------------------------------------------------------
/src/services/excelDownload/state.js:
--------------------------------------------------------------------------------
1 | export const initialState = {
2 | isExcelDownloading: false,
3 | };
4 |
--------------------------------------------------------------------------------
/src/services/feature/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_FEATURE = 'SET_FEATURE';
2 | export const CHECK_ALL_FEATURES = 'CHECK_ALL_FEATURES';
3 |
4 | export const setFeature = (featureId, value) => ({
5 | type: SET_FEATURE,
6 | payload: {
7 | featureId,
8 | value,
9 | },
10 | });
11 |
12 | export const checkAllFeatures = () => ({
13 | type: CHECK_ALL_FEATURES,
14 | });
15 |
--------------------------------------------------------------------------------
/src/services/feature/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { SET_FEATURE } from './actions';
4 |
5 | const initialState = {};
6 |
7 | const featureReducer = produce((draft, action) => {
8 | switch (action.type) {
9 | case SET_FEATURE:
10 | draft[action.payload.featureId] = action.payload.value;
11 | break;
12 | default:
13 | break;
14 | }
15 | }, initialState);
16 |
17 | export default featureReducer;
18 |
--------------------------------------------------------------------------------
/src/services/feature/requests.ts:
--------------------------------------------------------------------------------
1 | import * as R from 'runtypes';
2 |
3 | import { getApi } from '../../api';
4 | import config from '../../config';
5 | import { makeURI } from '../../utils/uri';
6 |
7 | const RFeatureCheckResponse = R.Record({
8 | is_tester: R.Boolean,
9 | });
10 |
11 | export async function fetchIsFeatureEnabled(featureId) {
12 | const api = getApi();
13 | const response = await api.get(makeURI(`/tests/u/${featureId}/check/`, {}, config.LIBRARY_API_BASE_URL));
14 | const data = RFeatureCheckResponse.check(response.data);
15 | return data.is_tester;
16 | }
17 |
--------------------------------------------------------------------------------
/src/services/feature/sagas.js:
--------------------------------------------------------------------------------
1 | import { call, put } from 'redux-saga/effects';
2 |
3 | import * as featureIds from '../../constants/featureIds';
4 | import * as actions from './actions';
5 | import * as requests from './requests';
6 |
7 | function* checkFeature(featureId) {
8 | try {
9 | const result = yield call(requests.fetchIsFeatureEnabled, featureId);
10 | yield put(actions.setFeature(featureId, result));
11 | } catch (_) {
12 | yield put(actions.setFeature(featureId, false));
13 | }
14 | }
15 |
16 | function* checkAllFeatures() {
17 | yield call(checkFeature, featureIds.SYNC_SHELF);
18 | }
19 |
20 | export default function* featureRootSaga() {
21 | yield call(checkAllFeatures);
22 | }
23 |
--------------------------------------------------------------------------------
/src/services/feature/selectors.js:
--------------------------------------------------------------------------------
1 | import createCachedSelector from 're-reselect';
2 |
3 | export const getIsFeatureEnabled = createCachedSelector(
4 | state => state.feature,
5 | (_, featureId) => featureId,
6 | (featureState, featureId) => Boolean(featureState[featureId]),
7 | )((_, id) => id);
8 |
--------------------------------------------------------------------------------
/src/services/maintenance/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_MAINTENANCE = 'SET_MAINTENANCE';
2 |
3 | export const setMaintenance = ({ visible, terms, unavailableServiceList }) => ({
4 | type: SET_MAINTENANCE,
5 | payload: {
6 | visible,
7 | terms,
8 | unavailableServiceList,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/src/services/maintenance/reducers.js:
--------------------------------------------------------------------------------
1 | import { SET_MAINTENANCE } from './actions';
2 | import { initialState } from './state';
3 |
4 | const maintenanceReducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case SET_MAINTENANCE:
7 | return {
8 | ...action.payload,
9 | };
10 | default:
11 | return state;
12 | }
13 | };
14 | export default maintenanceReducer;
15 |
--------------------------------------------------------------------------------
/src/services/maintenance/requests.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import config from '../../config';
4 | import { notifySentry } from '../../utils/sentry';
5 |
6 | const maintenanceStatus = (visible = false, terms = '', unavailableServiceList = []) => ({
7 | visible,
8 | terms,
9 | unavailableServiceList,
10 | });
11 |
12 | export const getSorryRidibooksStatus = () =>
13 | axios
14 | .get(config.RIDI_STATUS_URL)
15 | .then(({ data }) => {
16 | const maintenanceData = data || {};
17 | const { status, period, unavailableService } = maintenanceData;
18 | const visible = status === 'maintenance';
19 | return maintenanceStatus(visible, period, unavailableService);
20 | })
21 | .catch(error => {
22 | notifySentry(error);
23 | return maintenanceStatus();
24 | });
25 |
26 | export const getMaintenanceStatus = async () => {
27 | const sorryRidibooksStatus = await getSorryRidibooksStatus();
28 | return sorryRidibooksStatus;
29 | };
30 |
--------------------------------------------------------------------------------
/src/services/maintenance/state.js:
--------------------------------------------------------------------------------
1 | export const initialState = {
2 | visible: false,
3 | terms: '',
4 | unavailableServiceList: [],
5 | };
6 |
--------------------------------------------------------------------------------
/src/services/prompt/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_PROMPT = 'SET_PROMPT';
2 | export const UNSET_PROMPT = 'UNSET_PROMPT';
3 |
4 | export const showPrompt = ({
5 | title,
6 | message,
7 | confirmLabel,
8 | placeHolder,
9 | initialValue,
10 | emptyInputAlertMessage,
11 | onClickConfirmButton,
12 | limit,
13 | }) => ({
14 | type: SET_PROMPT,
15 | payload: {
16 | prompt: {
17 | title,
18 | message,
19 | confirmLabel,
20 | placeHolder,
21 | initialValue,
22 | emptyInputAlertMessage,
23 | onClickConfirmButton,
24 | limit,
25 | },
26 | },
27 | });
28 |
29 | export const closePrompt = () => ({
30 | type: UNSET_PROMPT,
31 | });
32 |
--------------------------------------------------------------------------------
/src/services/prompt/reducers.js:
--------------------------------------------------------------------------------
1 | import { SET_PROMPT, UNSET_PROMPT } from './actions';
2 | import { initialState } from './state';
3 |
4 | const promptReducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case SET_PROMPT:
7 | return action.payload.prompt;
8 | case UNSET_PROMPT:
9 | return null;
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | export default promptReducer;
16 |
--------------------------------------------------------------------------------
/src/services/prompt/state.js:
--------------------------------------------------------------------------------
1 | export const initialState = null;
2 |
--------------------------------------------------------------------------------
/src/services/purchased/common/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_READ_LATEST_BOOK_ID = 'SET_READ_LATEST_BOOK_ID';
2 |
3 | export const HIDE_ALL_EXPIRED_BOOKS = 'HIDE_ALL_EXPIRED_BOOKS';
4 | export const CONFIRM_HIDE_ALL_EXPIRED_BOOKS = 'CONFIRM_HIDE_ALL_EXPIRED_BOOKS';
5 |
6 | export const SET_FETCHING_READ_LATEST = 'SET_FETCHING_READ_LATEST';
7 | export const SET_RECENTLY_UPDATED_DATA = 'SET_RECENTLY_UPDATED_DATA';
8 | export const SET_PRIMARY_BOOK_ID = 'SET_PRIMARY_BOOK_ID';
9 |
10 | export const setReadLatestBookId = (unitId, bookId) => ({
11 | type: SET_READ_LATEST_BOOK_ID,
12 | payload: {
13 | unitId,
14 | bookId,
15 | },
16 | });
17 |
18 | export const setFetchingReadLatest = fetchingReadLatest => ({
19 | type: SET_FETCHING_READ_LATEST,
20 | payload: {
21 | fetchingReadLatest,
22 | },
23 | });
24 |
25 | export const setRecentlyUpdatedData = recentlyUpdatedData => ({
26 | type: SET_RECENTLY_UPDATED_DATA,
27 | payload: {
28 | recentlyUpdatedData,
29 | },
30 | });
31 |
32 | export const confirmHideAllExpiredBooks = history => ({
33 | type: CONFIRM_HIDE_ALL_EXPIRED_BOOKS,
34 | payload: {
35 | history,
36 | },
37 | });
38 |
39 | export const setPrimaryBookId = (unitId, primaryBookId) => ({
40 | type: SET_PRIMARY_BOOK_ID,
41 | payload: {
42 | unitId,
43 | primaryBookId,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/src/services/purchased/common/constants.js:
--------------------------------------------------------------------------------
1 | export const HIDE_ALL_EXPIRED_DONE_CHECK_MAX_RETRY_COUNT = 100;
2 | export const HIDE_ALL_EXPIRED_MAX_COUNT_PER_ITER = 5000;
3 |
--------------------------------------------------------------------------------
/src/services/purchased/common/errors.js:
--------------------------------------------------------------------------------
1 | export class NotFoundReadLatestError extends Error {}
2 |
3 | export class NotFoundExpiredBooksError extends Error {}
4 |
--------------------------------------------------------------------------------
/src/services/purchased/common/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { SET_FETCHING_READ_LATEST, SET_PRIMARY_BOOK_ID, SET_READ_LATEST_BOOK_ID, SET_RECENTLY_UPDATED_DATA } from './actions';
4 |
5 | const initializeState = {
6 | readLatestBookIds: {},
7 | fetchingReadLatest: false,
8 | recentlyUpdatedData: {},
9 | primaryBookIds: {},
10 | };
11 |
12 | const purchasedCommonReducer = produce((draft, action) => {
13 | switch (action.type) {
14 | case SET_READ_LATEST_BOOK_ID:
15 | draft.readLatestBookIds[action.payload.unitId] = { loaded: true, bookId: action.payload.bookId };
16 | break;
17 | case SET_FETCHING_READ_LATEST:
18 | draft.fetchingReadLatest = action.payload.fetchingReadLatest;
19 | break;
20 | case SET_RECENTLY_UPDATED_DATA:
21 | draft.recentlyUpdatedData = { ...draft.recentlyUpdatedData, ...action.payload.recentlyUpdatedData };
22 | break;
23 | case SET_PRIMARY_BOOK_ID:
24 | draft.primaryBookIds[action.payload.unitId] = action.payload.primaryBookId;
25 | break;
26 | default:
27 | break;
28 | }
29 | }, initializeState);
30 |
31 | export default purchasedCommonReducer;
32 |
--------------------------------------------------------------------------------
/src/services/purchased/common/requests.js:
--------------------------------------------------------------------------------
1 | import { put } from 'redux-saga/effects';
2 |
3 | import { getAPI } from '../../../api/actions';
4 | import { HttpStatusCode } from '../../../api/constants';
5 | import config from '../../../config';
6 | import { makeURI } from '../../../utils/uri';
7 | import { NotFoundReadLatestError } from './errors';
8 |
9 | export function* fetchReadLatestBookId(seriesId) {
10 | const api = yield put(getAPI());
11 |
12 | try {
13 | const response = yield api.get(
14 | makeURI(`/api/user/reading-histories/series/${seriesId}/latest`, { simple: true }, config.VIEWER_API_BASE_URL),
15 | );
16 | return response.data.result;
17 | } catch (err) {
18 | if (err.response.status === HttpStatusCode.HTTP_404_NOT_FOUND) {
19 | throw new NotFoundReadLatestError();
20 | }
21 | throw err;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/services/purchased/common/selectors.js:
--------------------------------------------------------------------------------
1 | import createCachedSelector from 're-reselect';
2 | import { createSelector } from 'reselect';
3 |
4 | const getPurchasedCommonState = state => state.purchasedCommon;
5 |
6 | export const getReadLatestData = (state, unitId) =>
7 | createSelector(getPurchasedCommonState, _state => _state.readLatestBookIds[unitId])(state);
8 |
9 | export const getFetchingReadLatest = createSelector(getPurchasedCommonState, state => state.fetchingReadLatest);
10 |
11 | export const getIsRecentlyUpdated = (state, bookId) => Boolean(state.purchasedCommon.recentlyUpdatedData[bookId]);
12 |
13 | export const getRecentlyUpdatedData = createCachedSelector(
14 | state => state.purchasedCommon.recentlyUpdatedData,
15 | (state, bookIds) => bookIds,
16 | (recentlyUpdatedData, bookIds) =>
17 | bookIds.reduce((obj, bookId) => {
18 | obj[bookId] = recentlyUpdatedData[bookId];
19 | return obj;
20 | }, {}),
21 | )((state, bookIds) => [...bookIds].sort().join(','));
22 |
23 | export const getPrimaryBookId = (state, unitId) =>
24 | createSelector(getPurchasedCommonState, commonState => commonState.primaryBookIds[unitId])(state);
25 |
--------------------------------------------------------------------------------
/src/services/purchased/filter/actions.js:
--------------------------------------------------------------------------------
1 | export const UPDATE_CATEGORIES = 'UPDATE_CATEGORIES';
2 | export const UPDATE_SERVICE_TYPES = 'UPDATE_SERVICE_TYPES';
3 | export const SET_ALL_CATEGORY_COUNT = 'SET_ALL_CATEGORY_COUNT';
4 | export const SET_CATEGORY_FILTER_OPTIONS = 'SET_CATEGORY_FILTER_OPTIONS';
5 | export const SET_SERVICE_TYPE_FILTER_OPTIONS = 'SET_SERVICE_TYPE_FILTER_OPTIONS';
6 |
7 | export const updateCategories = () => ({
8 | type: UPDATE_CATEGORIES,
9 | });
10 |
11 | export const updateServiceTypes = () => ({
12 | type: UPDATE_SERVICE_TYPES,
13 | });
14 |
15 | export const setAllCategoryCount = count => ({
16 | type: SET_ALL_CATEGORY_COUNT,
17 | payload: {
18 | count,
19 | },
20 | });
21 |
22 | export const setCategoryFilterOptions = categoryFilterOptions => ({
23 | type: SET_CATEGORY_FILTER_OPTIONS,
24 | payload: {
25 | categoryFilterOptions,
26 | },
27 | });
28 |
29 | export const setServiceTypeFilterOptions = serviceTypeFilterOptions => ({
30 | type: SET_SERVICE_TYPE_FILTER_OPTIONS,
31 | payload: {
32 | serviceTypeFilterOptions,
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/src/services/purchased/filter/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { SET_ALL_CATEGORY_COUNT, SET_CATEGORY_FILTER_OPTIONS, SET_SERVICE_TYPE_FILTER_OPTIONS } from './actions';
4 |
5 | const initialState = {
6 | allCategoryOption: {
7 | title: '전체',
8 | value: null,
9 | count: 0,
10 | },
11 | categoryOptions: null,
12 | serviceTypeOptions: null,
13 | };
14 |
15 | const filterReducer = produce((draft, action) => {
16 | switch (action.type) {
17 | case SET_ALL_CATEGORY_COUNT:
18 | draft.allCategoryOption.count = action.payload.count;
19 | break;
20 | case SET_CATEGORY_FILTER_OPTIONS:
21 | draft.categoryOptions = action.payload.categoryFilterOptions;
22 | break;
23 | case SET_SERVICE_TYPE_FILTER_OPTIONS:
24 | draft.serviceTypeOptions = action.payload.serviceTypeFilterOptions;
25 | break;
26 | default:
27 | break;
28 | }
29 | }, initialState);
30 |
31 | export default filterReducer;
32 |
--------------------------------------------------------------------------------
/src/services/purchased/filter/requests.js:
--------------------------------------------------------------------------------
1 | import { getApi } from '../../../api';
2 | import config from '../../../config';
3 | import { makeURI } from '../../../utils/uri';
4 |
5 | const createFilterOption = (title, value, count = 0, hasChildren = false) => ({
6 | title,
7 | value,
8 | count,
9 | hasChildren,
10 | });
11 |
12 | const reformatCategories = categories =>
13 | categories.reduce((previous, value) => {
14 | const hasChildren = value.children && value.children.length > 0;
15 | const filterOption = createFilterOption(value.name, value.id, value.count, hasChildren);
16 | filterOption.children = hasChildren ? reformatCategories(value.children) : null;
17 |
18 | previous.push(filterOption);
19 | return previous;
20 | }, []);
21 |
22 | const countAllCategory = categories => categories.reduce((previous, value) => previous + value.count, 0);
23 |
24 | export async function fetchPurchaseCategories() {
25 | const api = getApi();
26 | const response = await api.get(makeURI('/items/categories', {}, config.LIBRARY_API_BASE_URL));
27 | return {
28 | allCategoryCount: countAllCategory(response.data.categories),
29 | categories: [...reformatCategories(response.data.categories)],
30 | };
31 | }
32 |
33 | export async function fetchPurchaseServiceTypesCount(serviceType) {
34 | const api = getApi();
35 | const response = await api.get(makeURI('/items/main/count/', { service_type: serviceType }, config.LIBRARY_API_BASE_URL));
36 | return response.data;
37 | }
38 |
--------------------------------------------------------------------------------
/src/services/purchased/filter/sagas.ts:
--------------------------------------------------------------------------------
1 | import { all, call, put, takeLatest } from 'redux-saga/effects';
2 |
3 | import { ServiceType } from 'constants/serviceType';
4 |
5 | import * as actions from './actions';
6 | import * as requests from './requests';
7 |
8 | export function* updateCategories() {
9 | const { allCategoryCount, categories } = yield call(requests.fetchPurchaseCategories);
10 | yield all([put(actions.setCategoryFilterOptions(categories)), put(actions.setAllCategoryCount(allCategoryCount))]);
11 | }
12 |
13 | const ServiceTypeFilter = [
14 | {
15 | title: '구매',
16 | value: ServiceType.NORMAL,
17 | count: 0,
18 | },
19 | {
20 | title: '대여',
21 | value: ServiceType.RENT,
22 | count: 0,
23 | },
24 | {
25 | title: '리디셀렉트',
26 | value: ServiceType.SELECT,
27 | count: 0,
28 | },
29 | ];
30 |
31 | export function* updateServiceTypes() {
32 | const serviceTypeCounts = yield all(ServiceTypeFilter.map(filter => call(requests.fetchPurchaseServiceTypesCount, filter.value)));
33 |
34 | // 서비스 타입에 해당하는 도서가 1권 이상일 때에만 노출
35 | yield put(
36 | actions.setServiceTypeFilterOptions(
37 | ServiceTypeFilter.map((filter, index) => ({
38 | ...filter,
39 | count: serviceTypeCounts[index] ? serviceTypeCounts[index].item_total_count : 0,
40 | })).filter(filter => filter.count > 0),
41 | ),
42 | );
43 | }
44 |
45 | export default function* filterRootSaga() {
46 | yield all([takeLatest(actions.UPDATE_CATEGORIES, updateCategories), takeLatest(actions.UPDATE_SERVICE_TYPES, updateServiceTypes)]);
47 | }
48 |
--------------------------------------------------------------------------------
/src/services/purchased/filter/selectors.js:
--------------------------------------------------------------------------------
1 | export const getFilterOptions = state => state.filter;
2 |
--------------------------------------------------------------------------------
/src/services/purchased/hidden/actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_HIDDEN_ITEMS = 'LOAD_HIDDEN_ITEMS';
2 |
3 | export const SET_HIDDEN_ITEMS = 'SET_HIDDEN_ITEMS';
4 | export const SET_HIDDEN_TOTAL_COUNT = 'SET_HIDDEN_TOTAL_COUNT';
5 |
6 | export const UNHIDE_SELECTED_HIDDEN_BOOKS = 'UNHIDE_SELECTED_HIDDEN_BOOKS';
7 | export const DELETE_SELECTED_HIDDEN_BOOKS = 'DELETE_SELECTED_HIDDEN_BOOKS';
8 | export const SELECT_ALL_HIDDEN_BOOKS = 'SELECT_ALL_HIDDEN_BOOKS';
9 |
10 | export const SET_HIDDEN_IS_FETCHING_BOOKS = 'SET_HIDDEN_IS_FETCHING_BOOKS';
11 |
12 | export const loadItems = page => ({
13 | type: LOAD_HIDDEN_ITEMS,
14 | payload: {
15 | page,
16 | },
17 | });
18 |
19 | export const setItems = (items, page) => ({
20 | type: SET_HIDDEN_ITEMS,
21 | payload: {
22 | items,
23 | page,
24 | },
25 | });
26 |
27 | export const setTotalCount = (unitTotalCount, itemTotalCount) => ({
28 | type: SET_HIDDEN_TOTAL_COUNT,
29 | payload: {
30 | unitTotalCount,
31 | itemTotalCount,
32 | },
33 | });
34 |
35 | export const selectAllBooks = page => ({
36 | type: SELECT_ALL_HIDDEN_BOOKS,
37 | payload: {
38 | page,
39 | },
40 | });
41 |
42 | export const unhideSelectedBooks = page => ({
43 | type: UNHIDE_SELECTED_HIDDEN_BOOKS,
44 | payload: {
45 | page,
46 | },
47 | });
48 |
49 | export const deleteSelectedBooks = page => ({
50 | type: DELETE_SELECTED_HIDDEN_BOOKS,
51 | payload: {
52 | page,
53 | },
54 | });
55 |
56 | export const setHiddenIsFetchingBooks = isFetchingBooks => ({
57 | type: SET_HIDDEN_IS_FETCHING_BOOKS,
58 | payload: {
59 | isFetchingBooks,
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/src/services/purchased/hidden/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { toDict, toFlatten } from '../../../utils/array';
4 | import { SET_HIDDEN_IS_FETCHING_BOOKS, SET_HIDDEN_ITEMS, SET_HIDDEN_TOTAL_COUNT } from './actions';
5 | import { initialState } from './state';
6 |
7 | const purchasedHiddenReducer = produce((draft, action) => {
8 | switch (action.type) {
9 | case SET_HIDDEN_ITEMS:
10 | draft.items = {
11 | ...draft.items,
12 | ...toDict(action.payload.items, 'b_id'),
13 | };
14 | draft.itemIdsForPage[action.payload.page] = toFlatten(action.payload.items, 'b_id');
15 | break;
16 | case SET_HIDDEN_TOTAL_COUNT:
17 | draft.unitTotalCount = action.payload.unitTotalCount;
18 | draft.itemTotalCount = action.payload.itemTotalCount;
19 | break;
20 | case SET_HIDDEN_IS_FETCHING_BOOKS:
21 | draft.isFetchingBooks = action.payload.isFetchingBooks;
22 | break;
23 | default:
24 | break;
25 | }
26 | }, initialState);
27 |
28 | export default purchasedHiddenReducer;
29 |
--------------------------------------------------------------------------------
/src/services/purchased/hidden/requests.js:
--------------------------------------------------------------------------------
1 | import { put } from 'redux-saga/effects';
2 |
3 | import { getAPI } from '../../../api/actions';
4 | import config from '../../../config';
5 | import { LIBRARY_ITEMS_LIMIT_PER_PAGE } from '../../../constants/page';
6 | import { calcOffset } from '../../../utils/pagination';
7 | import { makeURI } from '../../../utils/uri';
8 |
9 | export function* fetchHiddenItems(page) {
10 | const options = {
11 | offset: calcOffset(page, LIBRARY_ITEMS_LIMIT_PER_PAGE),
12 | limit: LIBRARY_ITEMS_LIMIT_PER_PAGE,
13 | };
14 |
15 | const api = yield put(getAPI());
16 | const response = yield api.get(makeURI('/items/hidden', options, config.LIBRARY_API_BASE_URL));
17 | return response.data;
18 | }
19 |
20 | export function* fetchHiddenItemsTotalCount() {
21 | const api = yield put(getAPI());
22 | const response = yield api.get(makeURI('/items/hidden/count', {}, config.LIBRARY_API_BASE_URL));
23 | return response.data;
24 | }
25 |
--------------------------------------------------------------------------------
/src/services/purchased/hidden/selectors.js:
--------------------------------------------------------------------------------
1 | import createCachedSelector from 're-reselect';
2 | import { createSelector } from 'reselect';
3 |
4 | import { LIBRARY_ITEMS_LIMIT_PER_PAGE } from '../../../constants/page';
5 | import { calcPage } from '../../../utils/pagination';
6 |
7 | const getState = state => state.purchasedHidden;
8 |
9 | export const getItems = createSelector(getState, state => state.items);
10 |
11 | export const getItemsByPage = createCachedSelector(
12 | state => state.purchasedHidden.itemIdsForPage,
13 | state => state.purchasedHidden.items,
14 | (_, page) => page,
15 | (itemIdsForPage, items, page) => {
16 | const itemIds = itemIdsForPage[page] || [];
17 | return itemIds.map(itemId => items[itemId]);
18 | },
19 | )((_, page) => page);
20 |
21 | export const getTotalPages = createSelector(
22 | state => state.purchasedHidden.unitTotalCount,
23 | unitTotalCount => calcPage(unitTotalCount, LIBRARY_ITEMS_LIMIT_PER_PAGE),
24 | );
25 |
26 | export const getTotalCount = createSelector(getState, state => ({
27 | unitTotalCount: state.unitTotalCount,
28 | itemTotalCount: state.itemTotalCount,
29 | }));
30 |
31 | export const getIsFetchingBooks = createSelector(getState, state => state.isFetchingBooks);
32 |
--------------------------------------------------------------------------------
/src/services/purchased/hidden/state.js:
--------------------------------------------------------------------------------
1 | export const initialState = {
2 | itemIdsForPage: {},
3 | items: {},
4 |
5 | unitTotalCount: 0,
6 | itemTotalCount: 0,
7 |
8 | isFetchingBooks: false,
9 | };
10 |
--------------------------------------------------------------------------------
/src/services/purchased/main/actions.js:
--------------------------------------------------------------------------------
1 | export const LOAD_MAIN_ITEMS = 'LOAD_MAIN_ITEMS';
2 |
3 | export const UPDATE_MAIN_ITEMS = 'UPDATE_MAIN_ITEMS';
4 |
5 | export const SELECT_ALL_MAIN_BOOKS = 'SELECT_ALL_MAIN_BOOKS';
6 | export const HIDE_SELECTED_MAIN_BOOKS = 'HIDE_SELECTED_MAIN_BOOKS';
7 | export const DOWNLOAD_SELECTED_MAIN_BOOKS = 'DOWNLOAD_SELECTED_MAIN_BOOKS';
8 |
9 | export const SET_MAIN_IS_FETCHING_BOOKS = 'SET_MAIN_IS_FETCHING_BOOKS';
10 |
11 | export const loadItems = pageOptions => ({
12 | type: LOAD_MAIN_ITEMS,
13 | payload: {
14 | pageOptions,
15 | },
16 | });
17 |
18 | export const updateItems = ({ pageOptions, items, unitTotalCount, itemTotalCount }) => ({
19 | type: UPDATE_MAIN_ITEMS,
20 | payload: {
21 | pageOptions,
22 | items,
23 | unitTotalCount,
24 | itemTotalCount,
25 | },
26 | });
27 |
28 | export const selectAllBooks = pageOptions => ({
29 | type: SELECT_ALL_MAIN_BOOKS,
30 | payload: {
31 | pageOptions,
32 | },
33 | });
34 |
35 | export const hideSelectedBooks = pageOptions => ({
36 | type: HIDE_SELECTED_MAIN_BOOKS,
37 | payload: {
38 | pageOptions,
39 | },
40 | });
41 |
42 | export const downloadSelectedBooks = pageOptions => ({
43 | type: DOWNLOAD_SELECTED_MAIN_BOOKS,
44 | payload: {
45 | pageOptions,
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/src/services/purchased/main/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { toDict, toFlatten } from '../../../utils/array';
4 | import { LOAD_MAIN_ITEMS, UPDATE_MAIN_ITEMS } from './actions';
5 | import { createInitialDataState, initialState, mapPageOptionsToKey } from './state';
6 |
7 | const mainReducer = produce((draft, action) => {
8 | let key;
9 | if ([LOAD_MAIN_ITEMS, UPDATE_MAIN_ITEMS].includes(action.type)) {
10 | const { pageOptions } = action.payload;
11 | key = mapPageOptionsToKey(pageOptions);
12 | if (draft.data[key] == null) {
13 | draft.data[key] = createInitialDataState();
14 | }
15 | }
16 |
17 | switch (action.type) {
18 | case UPDATE_MAIN_ITEMS: {
19 | const { page } = action.payload.pageOptions;
20 | draft.data[key].items = { ...draft.data[key].items, ...toDict(action.payload.items, 'b_id') };
21 | draft.data[key].itemIdsForPage[page] = toFlatten(action.payload.items, 'b_id');
22 | draft.data[key].unitTotalCount = action.payload.unitTotalCount;
23 | draft.data[key].itemTotalCount = action.payload.itemTotalCount;
24 | break;
25 | }
26 | default:
27 | break;
28 | }
29 | }, initialState);
30 |
31 | export default mainReducer;
32 |
--------------------------------------------------------------------------------
/src/services/purchased/main/state.js:
--------------------------------------------------------------------------------
1 | import { OrderOptions } from 'constants/orderOptions';
2 | import { BooksPageKind } from 'constants/urls';
3 |
4 | export const initialState = {
5 | data: {},
6 | };
7 |
8 | export const createInitialDataState = () => ({
9 | itemIdsForPage: {},
10 | items: {},
11 | unitTotalCount: 0,
12 | itemTotalCount: 0,
13 | });
14 |
15 | export function mapPageOptionsToKey(pageOptions) {
16 | const { kind, keyword, orderBy, orderDirection, filter } = pageOptions;
17 | let key = '';
18 | if (kind === BooksPageKind.MAIN) {
19 | const order = OrderOptions.toKey(orderBy, orderDirection);
20 | key = order;
21 | } else if (kind === BooksPageKind.SEARCH) {
22 | key = keyword;
23 | }
24 | return `${kind}_${filter}_${key}`;
25 | }
26 |
--------------------------------------------------------------------------------
/src/services/selection/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getSelectedItems = state => state.selection.ids;
4 |
5 | export const getSelectedItemIds = createSelector(getSelectedItems, selection =>
6 | Object.entries(selection)
7 | .filter(([, value]) => value)
8 | .map(([itemId]) => itemId),
9 | );
10 |
11 | export const getIsItemSelected = (state, itemId) => Boolean(getSelectedItems(state)[itemId]);
12 |
13 | export const getTotalSelectedCount = createSelector(getSelectedItems, items => Object.values(items).filter(x => x).length);
14 |
15 | export const getSelectedShelves = state => state.selection.shelfUuids;
16 |
17 | export const getSelectedShelfIds = createSelector(getSelectedShelves, selection =>
18 | Object.entries(selection)
19 | .filter(([, value]) => value)
20 | .map(([itemId]) => itemId),
21 | );
22 |
--------------------------------------------------------------------------------
/src/services/serialPreference/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { toDict, toFlatten } from '../../utils/array';
4 | import {
5 | SET_SERIAL_IS_FETCHING_BOOKS,
6 | SET_SERIAL_PREFERENCE_ITEMS,
7 | SET_SERIAL_PREFERENCE_TOTAL_COUNT,
8 | SET_SERIAL_UNIT_ID_MAP,
9 | } from './actions';
10 | import { createInitialDataState, getKey, initialState } from './state';
11 |
12 | const serialPreferenceReducer = produce((draft, action) => {
13 | const key = getKey(draft);
14 | if (draft.data[key] == null) {
15 | draft.data[key] = createInitialDataState();
16 | }
17 | switch (action.type) {
18 | case SET_SERIAL_PREFERENCE_ITEMS:
19 | draft.data[key].items = {
20 | ...draft.data[key].items,
21 | ...toDict(action.payload.items, 'series_id'),
22 | };
23 | draft.data[key].itemIdsForPage[action.payload.page] = toFlatten(action.payload.items, 'series_id');
24 | break;
25 | case SET_SERIAL_PREFERENCE_TOTAL_COUNT:
26 | draft.data[key].totalCount = action.payload.totalCount;
27 | break;
28 | case SET_SERIAL_UNIT_ID_MAP:
29 | draft.unitIdMap = {
30 | ...draft.unitIdMap,
31 | ...action.payload.unitIdMap,
32 | };
33 | break;
34 | case SET_SERIAL_IS_FETCHING_BOOKS:
35 | draft.isFetchingBooks = action.payload.isFetchingBooks;
36 | break;
37 | default:
38 | break;
39 | }
40 | }, initialState);
41 |
42 | export default serialPreferenceReducer;
43 |
--------------------------------------------------------------------------------
/src/services/serialPreference/requests.js:
--------------------------------------------------------------------------------
1 | import { put } from 'redux-saga/effects';
2 |
3 | import { getAPI } from '../../api/actions';
4 | import config from '../../config';
5 | import { SERIAL_PREFERENCE_ITEMS_LIMIT_PER_PAGE } from '../../constants/page';
6 | import { calcOffset } from '../../utils/pagination';
7 | import { makeURI } from '../../utils/uri';
8 |
9 | export function* fetchSerialPreferenceItems(page) {
10 | const options = {
11 | offset: calcOffset(page, SERIAL_PREFERENCE_ITEMS_LIMIT_PER_PAGE),
12 | limit: SERIAL_PREFERENCE_ITEMS_LIMIT_PER_PAGE,
13 | };
14 |
15 | const api = yield put(getAPI());
16 | const response = yield api.get(makeURI('/serial-preference', options, config.BOOK_FEEDBACK_API_BASE_URL));
17 | return response.data;
18 | }
19 |
20 | export function* deleteSerialPreferenceItems(bookSeriesIds) {
21 | const options = {
22 | series_ids: bookSeriesIds,
23 | };
24 | const api = yield put(getAPI());
25 | const response = yield api.delete(makeURI('/serial-preference', options, config.BOOK_FEEDBACK_API_BASE_URL));
26 | return response.data;
27 | }
28 |
--------------------------------------------------------------------------------
/src/services/serialPreference/selectors.js:
--------------------------------------------------------------------------------
1 | import createCachedSelector from 're-reselect';
2 | import { createSelector } from 'reselect';
3 |
4 | import { createInitialDataState, getKey } from './state';
5 |
6 | const getDataState = state => {
7 | const mainState = state.serialPreference;
8 | const key = getKey(mainState);
9 | return mainState.data[key] || createInitialDataState();
10 | };
11 |
12 | export const getItemsByPage = createCachedSelector(
13 | getDataState,
14 | (_, page) => page,
15 | (dataState, page) => {
16 | const { itemIdsForPage, items } = dataState;
17 | const itemIds = itemIdsForPage[page] || [];
18 | return itemIds.map(itemId => items[itemId]);
19 | },
20 | )((_, page) => page);
21 |
22 | export const getTotalCount = createSelector(getDataState, dataState => dataState.totalCount);
23 |
24 | export const getIsFetchingBooks = state => state.serialPreference.isFetchingBooks;
25 |
26 | export const getUnitIdsMap = createCachedSelector(
27 | state => state.serialPreference.unitIdMap,
28 | (_, bookIds) => bookIds,
29 | (unitIdMap, bookIds) =>
30 | bookIds.reduce((previous, bookId) => {
31 | previous[bookId] = unitIdMap[bookId];
32 | return previous;
33 | }, {}),
34 | )((_, bookIds) => [...bookIds].sort().join(','));
35 |
--------------------------------------------------------------------------------
/src/services/serialPreference/state.js:
--------------------------------------------------------------------------------
1 | const _DEFAULT_KEY = 'ALL';
2 |
3 | export const initialState = {
4 | data: {},
5 | unitIdMap: {},
6 |
7 | isFetchingBooks: false,
8 | };
9 |
10 | export const createInitialDataState = () => ({
11 | itemIdsForPage: {},
12 | items: {},
13 | totalCount: 0,
14 | });
15 |
16 | export const getKey = () => _DEFAULT_KEY;
17 |
--------------------------------------------------------------------------------
/src/services/shelf/constants.ts:
--------------------------------------------------------------------------------
1 | export const OperationStatus = {
2 | UNDONE: 'undone',
3 | DONE: 'done',
4 | FORBIDDEN: 'forbidden',
5 | FAILURE: 'failure',
6 | };
7 |
--------------------------------------------------------------------------------
/src/services/toast/actions.ts:
--------------------------------------------------------------------------------
1 | import { Duration, ToastStyle } from './constants';
2 |
3 | export const SET_TOAST = 'SET_TOAST';
4 | export const UNSET_TOAST = 'UNSET_TOAST';
5 |
6 | export const SHOW_TOAST = 'SHOW_TOAST';
7 | export const CLOSE_TOAST = 'CLOSE_TOAST';
8 |
9 | export const CLOSE_WITH_DELAY = 'CLOSE_WITH_DELAY';
10 | export const CANCEL_CLOSE = 'CANCEL';
11 |
12 | export const setToast = toastState => ({
13 | type: SET_TOAST,
14 | payload: {
15 | ...toastState,
16 | },
17 | });
18 |
19 | export const unsetToast = () => ({
20 | type: UNSET_TOAST,
21 | });
22 |
23 | interface ShowToastPayload {
24 | message: string;
25 | linkName?: string;
26 | linkProps?: any;
27 | outLink?: string;
28 | duration?: Duration | number;
29 | toastStyle?: ToastStyle;
30 | withBottomFixedButton?: boolean;
31 | }
32 |
33 | export const showToast = ({ message, linkName, linkProps, outLink, duration, toastStyle, withBottomFixedButton }: ShowToastPayload) => ({
34 | type: SHOW_TOAST,
35 | payload: {
36 | message,
37 | linkName,
38 | linkProps,
39 | outLink,
40 | withBottomFixedButton: withBottomFixedButton || false,
41 | duration: duration || Duration.NORMAL,
42 | toastStyle: toastStyle || ToastStyle.GREEN,
43 | },
44 | });
45 |
46 | export const closeToast = () => ({
47 | type: CLOSE_TOAST,
48 | });
49 |
50 | export const closeWithDelay = duration => ({
51 | type: CLOSE_WITH_DELAY,
52 | payload: {
53 | duration,
54 | },
55 | });
56 |
57 | export const cancelClose = () => ({
58 | type: CANCEL_CLOSE,
59 | });
60 |
--------------------------------------------------------------------------------
/src/services/toast/constants.ts:
--------------------------------------------------------------------------------
1 | export enum Duration {
2 | SHORT = 1500,
3 | NORMAL = 3000,
4 | LONG = 5000,
5 | VERY_LONG = 8000,
6 | }
7 |
8 | export enum ToastStyle {
9 | GREEN = 'green',
10 | BLUE = 'blue',
11 | RED = 'red',
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/toast/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { SET_TOAST, UNSET_TOAST } from './actions';
4 |
5 | const initialState = {
6 | visible: false,
7 | };
8 |
9 | const toastReducer = produce((draft, action) => {
10 | switch (action.type) {
11 | case SET_TOAST:
12 | return {
13 | ...action.payload,
14 | visible: true,
15 | };
16 | case UNSET_TOAST:
17 | draft.visible = false;
18 | break;
19 | default:
20 | break;
21 | }
22 | return draft;
23 | }, initialState);
24 |
25 | export default toastReducer;
26 |
--------------------------------------------------------------------------------
/src/services/toast/sagas.js:
--------------------------------------------------------------------------------
1 | import { all, call, delay, put, race, take, takeEvery } from 'redux-saga/effects';
2 |
3 | import { CANCEL_CLOSE, cancelClose, CLOSE_TOAST, CLOSE_WITH_DELAY, setToast, SHOW_TOAST, unsetToast } from './actions';
4 |
5 | function* closeToast() {
6 | yield put(unsetToast());
7 | }
8 |
9 | function* _delayClose(duration) {
10 | const { cancel } = yield race({
11 | cancel: take(CANCEL_CLOSE),
12 | timeout: delay(duration),
13 | });
14 |
15 | if (cancel) {
16 | return;
17 | }
18 |
19 | yield call(closeToast);
20 | }
21 |
22 | function* showToast(action) {
23 | yield put(cancelClose());
24 | yield call(closeToast);
25 |
26 | const { duration } = action.payload;
27 | yield put(setToast({ ...action.payload }));
28 | yield call(_delayClose, duration);
29 | }
30 |
31 | function* closeWithDelay(action) {
32 | yield call(_delayClose, action.payload.duration);
33 | }
34 |
35 | export default function* toastRootSaga() {
36 | yield all([takeEvery(SHOW_TOAST, showToast), takeEvery(CLOSE_TOAST, closeToast), takeEvery(CLOSE_WITH_DELAY, closeWithDelay)]);
37 | }
38 |
--------------------------------------------------------------------------------
/src/services/toast/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const getToastState = state => state.toast;
4 |
5 | export const getToast = createSelector(getToastState, toastState => toastState);
6 |
--------------------------------------------------------------------------------
/src/services/tracking/actions.js:
--------------------------------------------------------------------------------
1 | export const INIT_TRACKER = 'INIT_TRACKER';
2 | export const TRACK_PAGE = 'TRACK_PAGE';
3 | export const TRACK_EVENT = 'TRACK_EVENT';
4 |
5 | export const initTracker = () => ({
6 | type: 'INIT_TRACKER',
7 | });
8 |
9 | export const trackPage = pathName => ({
10 | type: 'TRACK_PAGE',
11 | payload: {
12 | pathName,
13 | },
14 | });
15 |
16 | export const trackEvent = ({ eventName, trackingParams }) => ({
17 | type: 'TRACK_EVENT',
18 | payload: {
19 | eventName,
20 | trackingParams,
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/src/services/tracking/constants.js:
--------------------------------------------------------------------------------
1 | export const EventNames = {
2 | MAKE_SHELF: 'make_shelf',
3 | MODIFY_SHELF_NAME: 'modify_shelf_name',
4 | DELETE_SHELF: 'delete_shelf',
5 | ADD_BOOK: 'add_book',
6 | DELETE_BOOK: 'delete_book',
7 | };
8 |
--------------------------------------------------------------------------------
/src/services/tracking/sagas.js:
--------------------------------------------------------------------------------
1 | import { DeviceType, Tracker } from '@ridi/event-tracker';
2 | import { all, call, select, takeEvery } from 'redux-saga/effects';
3 |
4 | import { ENV } from 'constants/environment';
5 |
6 | import config from '../../config';
7 | import * as actions from './actions';
8 |
9 | let tracker;
10 | let previousUrl;
11 |
12 | function* initializeTracker() {
13 | const account = yield select(state => state.account);
14 | const userId = account?.userInfo?.id;
15 | const deviceBP = 840;
16 | const deviceType = document.body.clientWidth < deviceBP ? DeviceType.Mobile : DeviceType.PC;
17 |
18 | tracker = new Tracker({
19 | debug: config.ENVIRONMENT !== ENV.PRODUCTION,
20 | deviceType,
21 | userId,
22 | tagManagerOptions: {
23 | trackingId: 'GTM-5XSZZGH',
24 | },
25 | });
26 | tracker.initialize();
27 | }
28 |
29 | function* watchTrackPage({ payload }) {
30 | const { pathName } = payload;
31 | const referrer = previousUrl || document.referrer;
32 | previousUrl = pathName;
33 |
34 | if (!tracker) yield call(initializeTracker);
35 | if (referrer) {
36 | tracker.sendPageView(pathName, referrer);
37 | } else {
38 | tracker.sendPageView(pathName);
39 | }
40 | }
41 |
42 | function* watchTrackEvent({ payload }) {
43 | const { eventName, trackingParams } = payload;
44 | if (!tracker) yield call(initializeTracker);
45 | tracker.sendEvent(eventName, trackingParams);
46 | }
47 |
48 | export default function* trackingRootSaga() {
49 | yield all([
50 | takeEvery(actions.INIT_TRACKER, initializeTracker),
51 | takeEvery(actions.TRACK_PAGE, watchTrackPage),
52 | takeEvery(actions.TRACK_EVENT, watchTrackEvent),
53 | ]);
54 | }
55 |
--------------------------------------------------------------------------------
/src/services/ui/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_VIEW_TYPE = 'SET_VIEW_TYPE';
2 | export const SET_FULL_SCREEN_LOADING = 'SET_FULL_SCREEN_LOADING';
3 | export const SET_IS_ERROR = 'SET_IS_ERROR';
4 |
5 | export const setFullScreenLoading = isLoading => ({
6 | type: SET_FULL_SCREEN_LOADING,
7 | payload: {
8 | isLoading,
9 | },
10 | });
11 |
12 | export const setViewType = viewType => ({
13 | type: SET_VIEW_TYPE,
14 | payload: {
15 | viewType,
16 | },
17 | });
18 |
19 | export const setError = isError => ({
20 | type: SET_IS_ERROR,
21 | payload: {
22 | isError,
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/src/services/ui/reducers.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import { SET_FULL_SCREEN_LOADING, SET_IS_ERROR, SET_VIEW_TYPE } from './actions';
4 | import { initialState } from './state';
5 |
6 | const uiReducer = produce((draft, action) => {
7 | switch (action.type) {
8 | case SET_FULL_SCREEN_LOADING:
9 | draft.fullScreenLoading = action.payload.isLoading;
10 | break;
11 | case SET_VIEW_TYPE:
12 | draft.viewType = action.payload.viewType;
13 | break;
14 | case SET_IS_ERROR:
15 | draft.isError = action.payload.isError;
16 | break;
17 | default:
18 | break;
19 | }
20 | }, initialState);
21 |
22 | export default uiReducer;
23 |
--------------------------------------------------------------------------------
/src/services/ui/sagas.js:
--------------------------------------------------------------------------------
1 | import { put, takeEvery } from 'redux-saga/effects';
2 |
3 | import settings from '../../utils/settings';
4 | import * as uiActions from './actions';
5 |
6 | function saveViewType(action) {
7 | settings.viewType = action.payload.viewType;
8 | }
9 |
10 | export default function* uiRootSaga() {
11 | // FIXME: SSR 적용 시 서버 렌더 결과와 일치하도록 잘 처리해야 함
12 | if (settings.viewType) {
13 | yield put(uiActions.setViewType(settings.viewType));
14 | }
15 | yield takeEvery(uiActions.SET_VIEW_TYPE, saveViewType);
16 | }
17 |
--------------------------------------------------------------------------------
/src/services/ui/state.js:
--------------------------------------------------------------------------------
1 | import ViewType from '../../constants/viewType';
2 |
3 | export const initialState = {
4 | viewType: ViewType.PORTRAIT,
5 | fullScreenLoading: false,
6 | isError: false,
7 | };
8 |
--------------------------------------------------------------------------------
/src/services/unitPage/state.js:
--------------------------------------------------------------------------------
1 | import { concat } from '../../utils/array';
2 |
3 | export const initialState = {
4 | primaryItems: {},
5 | data: {},
6 | isFetchingBook: false,
7 | };
8 |
9 | export const createInitialDataState = () => ({
10 | items: {},
11 | itemIdsForPage: {},
12 |
13 | page: 1,
14 | itemTotalCount: 0,
15 | });
16 |
17 | export const getKey = state => concat([state.unitId, state.order]);
18 |
--------------------------------------------------------------------------------
/src/services/unitPage/utils.js:
--------------------------------------------------------------------------------
1 | import { ServiceType } from '../../constants/serviceType';
2 |
3 | // TODO: 컴포넌트 업데이트 전까지 임시적으로 처리한다.
4 | export function getRemainTime(libraryItem) {
5 | if (!libraryItem) {
6 | return '';
7 | }
8 |
9 | if (libraryItem.remain_time !== '') {
10 | return libraryItem.remain_time;
11 | }
12 |
13 | if (libraryItem.service_type === ServiceType.SELECT) {
14 | return '';
15 | }
16 |
17 | // 사용기간이 있으면
18 | return libraryItem.expire_date === '9999-12-31T23:59:59+09:00' ? '구매한 책' : '대여했던 책';
19 | }
20 |
--------------------------------------------------------------------------------
/src/static/OG/library.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/OG/library.jpg
--------------------------------------------------------------------------------
/src/static/cover/adult.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/cover/adult.png
--------------------------------------------------------------------------------
/src/static/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/static/favicon/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/android-chrome-384x384.png
--------------------------------------------------------------------------------
/src/static/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/static/favicon/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/static/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/src/static/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/src/static/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/favicon.ico
--------------------------------------------------------------------------------
/src/static/favicon/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/favicon/mstile-150x150.png
--------------------------------------------------------------------------------
/src/static/favicon/site.webmanifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "내 서재",
3 | "short_name": "내 서재",
4 | "icons": [
5 | {
6 | "src": "./android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "./android-chrome-384x384.png",
12 | "sizes": "384x384",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "browser"
19 | }
20 |
--------------------------------------------------------------------------------
/src/static/separator/portrait_book_w100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/separator/portrait_book_w100.png
--------------------------------------------------------------------------------
/src/static/separator/portrait_book_w110.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/separator/portrait_book_w110.png
--------------------------------------------------------------------------------
/src/static/separator/portrait_book_w140.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/separator/portrait_book_w140.png
--------------------------------------------------------------------------------
/src/static/separator/portrait_book_w86.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/separator/portrait_book_w86.png
--------------------------------------------------------------------------------
/src/static/spinner/blue_spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ridi/library-frontend-old/cef2500f7977d6f61798f4cb29e52ebd1bea9200/src/static/spinner/blue_spinner.gif
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux';
2 | import createSagaMiddleware from 'redux-saga';
3 |
4 | import { initializeApi, initializePublicApi } from '../api';
5 | import createApiMiddleware from '../api/middleware';
6 | import rootReducer from './reducers';
7 | import rootSaga from './sagas';
8 |
9 | export const makeStoreWithApi = (initialState, context) => {
10 | const devTools = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
11 | const composeEnhancers = devTools || compose;
12 | const apiMiddleware = createApiMiddleware(context);
13 | const sagaMiddleware = createSagaMiddleware();
14 | const middlewares = [apiMiddleware, sagaMiddleware];
15 |
16 | const store = createStore(rootReducer, initialState, composeEnhancers(applyMiddleware(...middlewares)));
17 | initializeApi(null, store);
18 | initializePublicApi(store);
19 |
20 | store.runSagaTask = () => {
21 | store.sagaTask = sagaMiddleware.run(rootSaga);
22 | };
23 | store.runSagaTask();
24 |
25 | return store;
26 | };
27 |
--------------------------------------------------------------------------------
/src/store/sagas.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 |
3 | import accountRootSaga from 'services/account/sagas';
4 | import bookRootSaga from 'services/book/sagas';
5 | import bookDownloadRootSaga from 'services/bookDownload/sagas';
6 | import excelDownloadRootSaga from 'services/excelDownload/sagas';
7 | // import featureRootSaga from 'services/feature/sagas';
8 | import purchasedCommonRootSaga from 'services/purchased/common/sagas/rootSagas';
9 | import filterRootSaga from 'services/purchased/filter/sagas';
10 | import purchasedHiddenSaga from 'services/purchased/hidden/sagas';
11 | import purchasedMainRootSaga from 'services/purchased/main/sagas';
12 | import serialPreferenceRootSaga from 'services/serialPreference/sagas';
13 | import shelfRootSaga from 'services/shelf/sagas';
14 | import toastRootSaga from 'services/toast/sagas';
15 | import trackingRootSaga from 'services/tracking/sagas';
16 | import uiRootSaga from 'services/ui/sagas';
17 | import unitPageRootSaga from 'services/unitPage/sagas';
18 |
19 | export default function* rootSaga() {
20 | yield all([
21 | accountRootSaga(),
22 | bookRootSaga(),
23 | excelDownloadRootSaga(),
24 | // featureRootSaga(),
25 | filterRootSaga(),
26 | purchasedCommonRootSaga(),
27 | purchasedMainRootSaga(),
28 | purchasedHiddenSaga(),
29 | serialPreferenceRootSaga(),
30 | uiRootSaga(),
31 | unitPageRootSaga(),
32 | shelfRootSaga(),
33 | toastRootSaga(),
34 | trackingRootSaga(),
35 | bookDownloadRootSaga(),
36 | ]);
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/constants.js:
--------------------------------------------------------------------------------
1 | export const Width = {
2 | W360: 360,
3 | W414: 414,
4 | W600: 600,
5 | W834: 834,
6 | W1280: 1280,
7 | W1440: 1440,
8 | };
9 |
10 | export const XLARGE_MAX_WIDTH = 800;
11 | export const XXLARGE_MAX_WIDTH = 1000;
12 | export const FULL_MAX_WIDTH = Width.W1280;
13 |
14 | export const BookSize = {
15 | XSmall: {
16 | width: 86,
17 | height: 165,
18 | },
19 | Small: {
20 | width: 100,
21 | height: 185,
22 | },
23 | Medium: {
24 | width: 110,
25 | height: 201,
26 | },
27 | Large: {
28 | width: 140,
29 | height: 255,
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/styles/index.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core';
2 |
3 | import { FULL_MAX_WIDTH, XLARGE_MAX_WIDTH, XXLARGE_MAX_WIDTH } from './constants';
4 | import { Responsive } from './responsive';
5 |
6 | export * from './reset';
7 |
8 | export const Hidden = css({
9 | fontSize: 0,
10 | width: 0,
11 | height: 0,
12 | color: 'transparent',
13 | overflow: 'none',
14 | });
15 |
16 | export const maxWidthWrapper = {
17 | ...Responsive.XLarge({
18 | maxWidth: XLARGE_MAX_WIDTH,
19 | }),
20 | ...Responsive.XXLarge({
21 | maxWidth: XXLARGE_MAX_WIDTH,
22 | }),
23 | ...Responsive.Full({
24 | maxWidth: FULL_MAX_WIDTH,
25 | }),
26 | };
27 |
--------------------------------------------------------------------------------
/src/svgs/ArrowLeft.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/src/svgs/ArrowTriangleDown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/ArrowTriangleRight.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/BookOutline.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/CategoryFilter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/src/svgs/Check.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/CheckCircle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/CheckCircleFill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/src/svgs/Close.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/Download.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/Edit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/src/svgs/ExclamationCircleFill.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/FeedbackIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svgs/FooterNewIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svgs/FooterPaperIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svgs/HeartOutline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/LogoRidi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/Logout.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/NoneDashedArrowDown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/NoneDashedArrowLeft.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/NoneDashedArrowRight.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/NoneDashedDoubleArrowRight.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/Note.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/NoticeFilled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/On.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/src/svgs/Plus.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/svgs/Review.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/Search.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/SeriesCompleteIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/Shelves.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svgs/Star.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/Sync.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/ThreeDotsHorizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svgs/ThreeDotsVertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/src/types/svgr-svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const SvgElement: React.ReactElement>;
3 | export = SvgElement;
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/array.js:
--------------------------------------------------------------------------------
1 | export const toDict = (arr, key, extractor = null) =>
2 | Object.fromEntries(arr.map(current => [current[key], extractor ? extractor(current) : current]));
3 |
4 | export const toFlatten = (arr, key, skipNull = false) => {
5 | const splited = key.split('.');
6 | const _key = splited.shift();
7 |
8 | let data;
9 | if (skipNull) {
10 | data = arr.filter(value => !!value).map(value => value[_key]);
11 | } else {
12 | data = arr.map(value => value[_key]);
13 | }
14 |
15 | if (splited.length === 0) {
16 | return data;
17 | }
18 |
19 | return toFlatten(data, splited.join('.'), skipNull);
20 | };
21 |
22 | export const makeRange = (start, end) => Array.from({ length: end - start }, (_, k) => k + start);
23 |
24 | export const concat = (arr, glue = '_') => arr.join(glue);
25 |
26 | export const arrayChunk = (array, chunkSize) =>
27 | Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, idx) => {
28 | const begin = idx * chunkSize;
29 | return array.slice(begin, begin + chunkSize);
30 | });
31 |
32 | export const makeUnique = array => [...new Set(array)];
33 |
--------------------------------------------------------------------------------
/src/utils/cookies.js:
--------------------------------------------------------------------------------
1 | import JSCookie from 'js-cookie';
2 |
3 | const Cookies = {
4 | get: key => JSCookie.get(key),
5 | set: (key, value, options) => {
6 | JSCookie.set(key, value, options);
7 | },
8 | delete: (key, options) => {
9 | JSCookie.remove(key, options);
10 | },
11 | };
12 |
13 | export default Cookies;
14 |
--------------------------------------------------------------------------------
/src/utils/dataObject.js:
--------------------------------------------------------------------------------
1 | import { UnitType } from '../constants/unitType';
2 |
3 | export const EmptyUnit = { title: '', type: UnitType.BOOK };
4 |
5 | export const EmptySeries = {
6 | property: {
7 | is_serial_complete: true,
8 | opened_book_count: 1,
9 | },
10 | volume: 1,
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/datetime.js:
--------------------------------------------------------------------------------
1 | export const isNowBetween = (start, end) => {
2 | const now = new Date();
3 | // 비교할 때 Date가 타임스탬프로 변함
4 | return start <= now && now < end;
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/delay.js:
--------------------------------------------------------------------------------
1 | export const delay = duration => new Promise(resolve => setTimeout(resolve, duration));
2 |
--------------------------------------------------------------------------------
/src/utils/device.js:
--------------------------------------------------------------------------------
1 | export const getDeviceInfo = () => {
2 | const { appVersion } = navigator;
3 | const isIos = /iphone|ipad|ipod/gi.test(appVersion);
4 | const isAndroid = /android/gi.test(appVersion);
5 | const isIE = /MSIE|Trident\//g.test(appVersion);
6 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
7 |
8 | return {
9 | appVersion,
10 | isIos,
11 | isAndroid,
12 | isIE,
13 | isFirefox,
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/file.js:
--------------------------------------------------------------------------------
1 | export const downloadFile = (downloadUrl, fileName) => {
2 | const downloadButton = document.createElement('a');
3 | downloadButton.href = downloadUrl;
4 | downloadButton.download = fileName;
5 | document.body.appendChild(downloadButton);
6 | downloadButton.click();
7 | document.body.removeChild(downloadButton);
8 | };
9 |
10 | export const formatFileSize = size => {
11 | if (!size) {
12 | return '';
13 | }
14 | const oneMegabyte = 1024;
15 | return `${(size / oneMegabyte).toFixed(1)}MB`;
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/number.js:
--------------------------------------------------------------------------------
1 | export const thousandsSeperator = value => value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
2 |
3 | export const numberWithUnit = value => {
4 | let floorNums = 0;
5 | let unit = '';
6 |
7 | if (value < 1000) {
8 | floorNums = value;
9 | } else if (value < 10000) {
10 | floorNums = Math.round(value / 100) / 10;
11 | unit = '천';
12 | } else if (value < 100000000) {
13 | floorNums = Math.round(value / 1000) / 10;
14 | unit = '만';
15 | }
16 |
17 | return floorNums > 0 ? `${floorNums}${unit}` : null;
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/order.js:
--------------------------------------------------------------------------------
1 | import { OrderBy } from 'constants/orderOptions';
2 |
3 | export const getOrderParams = (orderBy, orderDirection) => {
4 | if (orderBy === OrderBy.EXPIRED_BOOKS_ONLY) {
5 | return {
6 | expiredBooksOnly: true,
7 | };
8 | }
9 | return {
10 | orderType: orderBy,
11 | orderBy: orderDirection,
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/pagination.js:
--------------------------------------------------------------------------------
1 | import { makeRange } from './array';
2 |
3 | export const calcOffset = (page, limit) => (page - 1) * limit;
4 | export const calcPage = (totalCount, limit) => Math.ceil(totalCount / limit);
5 |
6 | export const calcPageBlock = (currentPage, pageCount) => Math.ceil(currentPage / pageCount);
7 | export const makePageRange = (currentPage, totalPages, pageCount) => {
8 | const block = calcPageBlock(currentPage, pageCount);
9 | const start = (block - 1) * pageCount + 1;
10 | const end = Math.min(start + pageCount, totalPages + 1);
11 | return makeRange(start, end);
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/retry.js:
--------------------------------------------------------------------------------
1 | import { delay } from './delay';
2 |
3 | export class NoMoreRetryError extends Error {
4 | constructor(error) {
5 | super();
6 | this.error = error;
7 | }
8 | }
9 |
10 | export const retry = async (options, fn, ...params) => {
11 | const { retryCount, retryDelay } = options;
12 |
13 | try {
14 | return await fn(...params);
15 | } catch (err) {
16 | if (err instanceof NoMoreRetryError) {
17 | throw err.error;
18 | }
19 |
20 | if (retryCount === 1) {
21 | throw err;
22 | }
23 |
24 | await delay(retryDelay);
25 | return retry({ retryCount: retryCount - 1, retryDelay }, fn, ...params);
26 | }
27 | };
28 |
29 | export const throwNetworkError = fn => async (...params) => {
30 | try {
31 | return await fn(...params);
32 | } catch (err) {
33 | // 응답이 있으면 재시도 하지 않는다. 네트워크 에러만 재시도 한다.
34 | if (err.response) {
35 | throw new NoMoreRetryError(err);
36 | }
37 | throw err;
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/src/utils/scroll.js:
--------------------------------------------------------------------------------
1 | const disableScrollClass = 'disable-scroll';
2 |
3 | export const enableScroll = () => {
4 | const body = document.querySelector('body');
5 | body.classList.remove(disableScrollClass);
6 | };
7 |
8 | export const disableScroll = () => {
9 | const body = document.querySelector('body');
10 | body.classList.add(disableScrollClass);
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/sentry.js:
--------------------------------------------------------------------------------
1 | /* global SENTRY_RELEASE_VERSION */
2 | import { captureEvent, captureException, captureMessage, init, Severity } from '@sentry/browser';
3 |
4 | import config from '../config';
5 |
6 | export const initializeSentry = () => {
7 | init({
8 | dsn: config.SENTRY_DSN,
9 | environment: config.SENTRY_ENV,
10 | release: SENTRY_RELEASE_VERSION || undefined,
11 | sampleRate: 0.4,
12 | });
13 | };
14 |
15 | export const notifySentry = err => {
16 | if (!err) {
17 | return;
18 | }
19 |
20 | captureException(err);
21 | };
22 |
23 | export const notifyMessage = msg => {
24 | captureMessage(msg, Severity.Info);
25 | };
26 |
27 | export default {
28 | captureMessage,
29 | captureException,
30 | captureEvent,
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/snakelize.js:
--------------------------------------------------------------------------------
1 | const _camelToSnake = name => name.replace(/([A-Z])/g, (x, y) => `_${y.toLowerCase()}`).replace(/^_/, '');
2 |
3 | export const snakelize = dict =>
4 | Object.keys(dict).reduce((previous, key) => {
5 | previous[_camelToSnake(key)] = dict[key];
6 | return previous;
7 | }, {});
8 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | const STORAGE_KEY_PREFIX = 'library.web';
2 |
3 | const makeKey = key => `${STORAGE_KEY_PREFIX}.${key}`;
4 |
5 | export const StorageKey = {
6 | BOOKS: 'books',
7 | UNITS: 'units',
8 | };
9 |
10 | export default {
11 | load: key => {
12 | const _key = makeKey(key);
13 | const storage = window.localStorage;
14 | if (!storage) {
15 | return [];
16 | }
17 |
18 | const data = storage.getItem(_key);
19 | if (!data) {
20 | return [];
21 | }
22 |
23 | return JSON.parse(data);
24 | },
25 | save: (key, state) => {
26 | const _key = makeKey(key);
27 | const storage = window.localStorage;
28 | if (!storage) {
29 | return;
30 | }
31 |
32 | try {
33 | storage.setItem(_key, JSON.stringify(state));
34 | } catch (e) {
35 | storage.removeItem(_key);
36 | }
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/utils/tabFocus.js:
--------------------------------------------------------------------------------
1 | const focusFreeClass = 'focus-free';
2 |
3 | export function registerTabKeyUpEvent(element) {
4 | const keyupHandler = event => {
5 | const code = event.keyCode || event.which;
6 | if (Number(code) === 9) {
7 | element.classList.remove(focusFreeClass);
8 | }
9 | };
10 |
11 | window.addEventListener('keyup', keyupHandler);
12 | return () => window.removeEventListener('keyup', keyupHandler);
13 | }
14 |
15 | export function registerMouseDownEvent(element) {
16 | const mouseDownHandler = () => {
17 | element.classList.add(focusFreeClass);
18 | };
19 |
20 | window.addEventListener('mousedown', mouseDownHandler);
21 | return () => window.removeEventListener('mousedown', mouseDownHandler);
22 | }
23 |
24 | export function initializeTabKeyFocus() {
25 | const body = document.querySelector('body');
26 | if (!body) {
27 | return;
28 | }
29 | body.classList.add(focusFreeClass);
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/ttl.js:
--------------------------------------------------------------------------------
1 | const _TTL_MINS = 10;
2 | const _DEFAULT_OFFSET_MINS = 1;
3 |
4 | export const makeTTL = () => {
5 | const now = new Date();
6 | now.setMinutes(now.getMinutes() + _TTL_MINS);
7 | return parseInt(now.getTime() / 1000, 10);
8 | };
9 |
10 | export const getCriterion = (offsetMins = _DEFAULT_OFFSET_MINS) => {
11 | const now = new Date();
12 | if (!offsetMins) {
13 | return parseInt(now.getTime() / 1000, 10);
14 | }
15 |
16 | if (offsetMins > _TTL_MINS) {
17 | throw Error('offset은 TTL값을 초과할 수 없습니다.');
18 | }
19 |
20 | now.setMinutes(now.getMinutes() - offsetMins);
21 | return parseInt(now.getTime() / 1000, 10);
22 | };
23 |
24 | export const attatchTTL = items => {
25 | const ttl = makeTTL();
26 | return items.map(item => {
27 | item.ttl = ttl;
28 | return item;
29 | });
30 | };
31 |
32 | export const isExpiredTTL = (item, criterion) => {
33 | const _criterion = criterion || getCriterion();
34 | return item.ttl <= _criterion;
35 | };
36 |
--------------------------------------------------------------------------------
/traefik/library.toml:
--------------------------------------------------------------------------------
1 | [frontends.library]
2 | backend = "library"
3 | passHostHeader = true
4 | [frontends.library.routes.web]
5 | rule = "Host:library.local.ridi.io"
6 |
7 | [backends.library.servers.web]
8 | url = "http://host.docker.internal:3000"
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "target": "esnext",
8 | "jsx": "preserve",
9 | "noImplicitReturns": true,
10 | "sourceMap": true,
11 | "noEmit": true,
12 | "baseUrl": ".",
13 | "paths": {
14 | "commonStyles/*": ["src/styles/*"],
15 | "components/*": ["src/components/*"],
16 | "constants/*": ["src/constants/*"],
17 | "pages/*": ["src/pages/*"],
18 | "services/*": ["src/services/*"],
19 | "static/*": ["src/static/*"],
20 | "svgs/*": ["src/svgs/*"],
21 | "utils/*": ["src/utils/*"]
22 | }
23 | },
24 | "exclude": ["node_modules"],
25 | "include": ["./src/**/*"]
26 | }
27 |
--------------------------------------------------------------------------------