Merge branch 'develop' into 'master'
eDrive v1.0.0 Merge See merge request !17
Showing
37 changed files
with
1491 additions
and
0 deletions
.gitignore
0 → 100644
1 | +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
2 | + | ||
3 | +# dependencies | ||
4 | +frontend/node_modules | ||
5 | +frontend/.pnp | ||
6 | +.pnp.js | ||
7 | + | ||
8 | +# testing | ||
9 | +frontend/coverage | ||
10 | + | ||
11 | +# production | ||
12 | +frontend/build | ||
13 | + | ||
14 | +# misc | ||
15 | +.DS_Store | ||
16 | +.env.local | ||
17 | +.env.development.local | ||
18 | +.env.test.local | ||
19 | +.env.production.local | ||
20 | + | ||
21 | +npm-debug.log* | ||
22 | +yarn-debug.log* | ||
23 | +yarn-error.log* | ||
24 | + | ||
25 | +.vscode | ||
26 | + | ||
27 | +.env | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
frontend/.env.example
0 → 100644
frontend/.eslintrc.json
0 → 100644
1 | +{ | ||
2 | + "env": { | ||
3 | + "browser": true, | ||
4 | + "node": true, | ||
5 | + "es6": true | ||
6 | + }, | ||
7 | + "plugins": ["prettier"], | ||
8 | + "extends": ["airbnb", "prettier"], | ||
9 | + "rules": { | ||
10 | + "react/jsx-filename-extension": 0, | ||
11 | + "prettier/prettier": [2, { | ||
12 | + "endOfLine": "auto" | ||
13 | + }], | ||
14 | + "arrow-body-style": 1, | ||
15 | + "react/jsx-fragments": 0, | ||
16 | + "react/prop-types": 0, | ||
17 | + "import/prefer-default-export": 0, | ||
18 | + "no-param-reassign": 0, | ||
19 | + "arrow-body-style": 0 | ||
20 | + }, | ||
21 | + "settings": { | ||
22 | + "react": { | ||
23 | + "version": "latest" | ||
24 | + } | ||
25 | + } | ||
26 | +} | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
frontend/.prettierrc
0 → 100644
frontend/package.json
0 → 100644
1 | +{ | ||
2 | + "name": "frontend", | ||
3 | + "version": "0.1.0", | ||
4 | + "private": true, | ||
5 | + "dependencies": { | ||
6 | + "@mantine/core": "^1.0.6", | ||
7 | + "@mantine/hooks": "^1.0.6", | ||
8 | + "@reduxjs/toolkit": "^1.6.0", | ||
9 | + "@testing-library/jest-dom": "^5.11.4", | ||
10 | + "@testing-library/react": "^11.1.0", | ||
11 | + "@testing-library/user-event": "^12.1.10", | ||
12 | + "axios": "^0.21.1", | ||
13 | + "dotenv": "^10.0.0", | ||
14 | + "react": "^17.0.2", | ||
15 | + "react-dom": "^17.0.2", | ||
16 | + "react-icons": "^4.2.0", | ||
17 | + "react-jss": "^10.6.0", | ||
18 | + "react-redux": "^7.2.3", | ||
19 | + "react-router-dom": "^5.2.0", | ||
20 | + "react-scripts": "4.0.3", | ||
21 | + "redux": "^4.0.5", | ||
22 | + "styled-components": "^5.2.3", | ||
23 | + "web-vitals": "^1.0.1" | ||
24 | + }, | ||
25 | + "scripts": { | ||
26 | + "start": "react-scripts start", | ||
27 | + "build": "react-scripts build", | ||
28 | + "test": "react-scripts test", | ||
29 | + "eject": "react-scripts eject" | ||
30 | + }, | ||
31 | + "eslintConfig": { | ||
32 | + "extends": [ | ||
33 | + "react-app", | ||
34 | + "react-app/jest" | ||
35 | + ] | ||
36 | + }, | ||
37 | + "browserslist": { | ||
38 | + "production": [ | ||
39 | + ">0.2%", | ||
40 | + "not dead", | ||
41 | + "not op_mini all" | ||
42 | + ], | ||
43 | + "development": [ | ||
44 | + "last 1 chrome version", | ||
45 | + "last 1 firefox version", | ||
46 | + "last 1 safari version" | ||
47 | + ] | ||
48 | + }, | ||
49 | + "devDependencies": { | ||
50 | + "eslint-config-airbnb": "18.2.1", | ||
51 | + "eslint-config-prettier": "^8.1.0", | ||
52 | + "eslint-plugin-import": "2.22.1", | ||
53 | + "eslint-plugin-jsx-a11y": "6.4.1", | ||
54 | + "eslint-plugin-prettier": "^3.3.1", | ||
55 | + "eslint-plugin-react": "7.21.5", | ||
56 | + "eslint-plugin-react-hooks": "1.7.0", | ||
57 | + "prettier": "^2.2.1" | ||
58 | + } | ||
59 | +} |
frontend/public/eDrive_logo.png
0 → 100644
7.84 KB
frontend/public/eDrive_logo_v2.png
0 → 100644
5 KB
frontend/public/favicon.ico
0 → 100644
No preview for this file type
frontend/public/index.html
0 → 100644
1 | +<!DOCTYPE html> | ||
2 | +<html lang="en"> | ||
3 | + | ||
4 | +<head> | ||
5 | + <meta charset="utf-8" /> | ||
6 | + <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | ||
7 | + <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
8 | + <meta name="theme-color" content="#000000" /> | ||
9 | + <meta name="description" content="Web site created using create-react-app" /> | ||
10 | + <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | ||
11 | + <!-- | ||
12 | + manifest.json provides metadata used when your web app is installed on a | ||
13 | + user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | ||
14 | + --> | ||
15 | + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | ||
16 | + <!-- | ||
17 | + Notice the use of %PUBLIC_URL% in the tags above. | ||
18 | + It will be replaced with the URL of the `public` folder during the build. | ||
19 | + Only files inside the `public` folder can be referenced from the HTML. | ||
20 | + | ||
21 | + Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | ||
22 | + work correctly both with client-side routing and a non-root public URL. | ||
23 | + Learn how to configure a non-root public URL by running `npm run build`. | ||
24 | + --> | ||
25 | + <title>eDrive</title> | ||
26 | +</head> | ||
27 | + | ||
28 | +<body> | ||
29 | + <noscript>You need to enable JavaScript to run this app.</noscript> | ||
30 | + <div id="root"></div> | ||
31 | + <!-- | ||
32 | + This HTML file is a template. | ||
33 | + If you open it directly in the browser, you will see an empty page. | ||
34 | + | ||
35 | + You can add webfonts, meta tags, or analytics to this file. | ||
36 | + The build step will place the bundled scripts into the <body> tag. | ||
37 | + | ||
38 | + To begin the development, run `npm start` or `yarn start`. | ||
39 | + To create a production bundle, use `npm run build` or `yarn build`. | ||
40 | + --> | ||
41 | +</body> | ||
42 | + | ||
43 | +</html> | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
frontend/public/logo192.png
0 → 100644
5.22 KB
frontend/public/logo512.png
0 → 100644
9.44 KB
frontend/public/manifest.json
0 → 100644
1 | +{ | ||
2 | + "short_name": "React App", | ||
3 | + "name": "Create React App Sample", | ||
4 | + "icons": [ | ||
5 | + { | ||
6 | + "src": "favicon.ico", | ||
7 | + "sizes": "64x64 32x32 24x24 16x16", | ||
8 | + "type": "image/x-icon" | ||
9 | + }, | ||
10 | + { | ||
11 | + "src": "logo192.png", | ||
12 | + "type": "image/png", | ||
13 | + "sizes": "192x192" | ||
14 | + }, | ||
15 | + { | ||
16 | + "src": "logo512.png", | ||
17 | + "type": "image/png", | ||
18 | + "sizes": "512x512" | ||
19 | + } | ||
20 | + ], | ||
21 | + "start_url": ".", | ||
22 | + "display": "standalone", | ||
23 | + "theme_color": "#000000", | ||
24 | + "background_color": "#ffffff" | ||
25 | +} |
frontend/public/robots.txt
0 → 100644
frontend/src/App.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import { createGlobalStyle } from 'styled-components'; | ||
3 | +import { BrowserRouter, Route, Switch } from 'react-router-dom'; | ||
4 | +import dotenv from 'dotenv'; | ||
5 | +import HomePage from './pages/HomePage'; | ||
6 | +import LoginPage from './pages/LoginPage'; | ||
7 | +import SearchPage from './pages/SearchPage'; | ||
8 | + | ||
9 | +dotenv.config(); | ||
10 | + | ||
11 | +const GlobalStyle = createGlobalStyle` | ||
12 | + html, | ||
13 | + body, | ||
14 | + #root { | ||
15 | + margin: 0; | ||
16 | + padding: 0; | ||
17 | + height: 100%; | ||
18 | + } | ||
19 | + html { | ||
20 | + box-sizing: border-box; | ||
21 | + | ||
22 | + * { | ||
23 | + box-sizing: inherit; | ||
24 | + } | ||
25 | + } | ||
26 | +`; | ||
27 | + | ||
28 | +const App = () => ( | ||
29 | + <BrowserRouter> | ||
30 | + <GlobalStyle /> | ||
31 | + <Switch> | ||
32 | + <Route path="/" exact component={HomePage} /> | ||
33 | + <Route path="/login" exact component={LoginPage} /> | ||
34 | + <Route path="/search" exact component={SearchPage} /> | ||
35 | + </Switch> | ||
36 | + </BrowserRouter> | ||
37 | +); | ||
38 | + | ||
39 | +export default App; |
frontend/src/components/Detail.js
0 → 100644
1 | +import { Card, Center, Divider, Table, Text } from '@mantine/core'; | ||
2 | +import React from 'react'; | ||
3 | + | ||
4 | +function Detail({ filename, writer, filePath, createdDate }) { | ||
5 | + const rows = ( | ||
6 | + <> | ||
7 | + <tr> | ||
8 | + <td> | ||
9 | + <Text style={{ width: 50 }} weight={700}> | ||
10 | + 문서명 | ||
11 | + </Text> | ||
12 | + </td> | ||
13 | + <td> | ||
14 | + <Text>{filename}</Text> | ||
15 | + </td> | ||
16 | + </tr> | ||
17 | + <tr> | ||
18 | + <td> | ||
19 | + <Text weight={700}>작성자</Text> | ||
20 | + </td> | ||
21 | + <td> | ||
22 | + <Text>{writer}</Text> | ||
23 | + </td> | ||
24 | + </tr> | ||
25 | + <tr> | ||
26 | + <td> | ||
27 | + <Text weight={700}>파일 경로</Text> | ||
28 | + </td> | ||
29 | + <td> | ||
30 | + <Text>{filePath}</Text> | ||
31 | + </td> | ||
32 | + </tr> | ||
33 | + <tr> | ||
34 | + <td> | ||
35 | + <Text weight={700}>파일 생성일</Text> | ||
36 | + </td> | ||
37 | + <td> | ||
38 | + <Text>{createdDate}</Text> | ||
39 | + </td> | ||
40 | + </tr> | ||
41 | + </> | ||
42 | + ); | ||
43 | + return ( | ||
44 | + <Card> | ||
45 | + <Center> | ||
46 | + <Text weight={700}>파일 정보</Text> | ||
47 | + </Center> | ||
48 | + <Divider /> | ||
49 | + <Table striped> | ||
50 | + <thead> | ||
51 | + <tbody>{rows}</tbody> | ||
52 | + </thead> | ||
53 | + </Table> | ||
54 | + </Card> | ||
55 | + ); | ||
56 | +} | ||
57 | + | ||
58 | +export default Detail; |
frontend/src/components/Documents.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import Document from './document'; | ||
3 | + | ||
4 | +const Documents = ({ datas }) => { | ||
5 | + return ( | ||
6 | + <div> | ||
7 | + {datas.map(data => { | ||
8 | + const { | ||
9 | + content, | ||
10 | + created_date: createdDate, | ||
11 | + file_path: filePath, | ||
12 | + filename, | ||
13 | + writer, | ||
14 | + images, | ||
15 | + } = data; | ||
16 | + return ( | ||
17 | + <Document | ||
18 | + content={content.raw} | ||
19 | + createdDate={createdDate.raw} | ||
20 | + filePath={filePath.raw} | ||
21 | + filename={filename.raw} | ||
22 | + writer={writer.raw} | ||
23 | + images={images.raw} | ||
24 | + /> | ||
25 | + ); | ||
26 | + })} | ||
27 | + </div> | ||
28 | + ); | ||
29 | +}; | ||
30 | + | ||
31 | +export default Documents; |
frontend/src/components/Header.js
0 → 100644
1 | +import React, { useState } from 'react'; | ||
2 | +import { NavLink, useHistory } from 'react-router-dom'; | ||
3 | +import { FaSearch } from 'react-icons/fa'; | ||
4 | +import styled from 'styled-components'; | ||
5 | +import InputBlock from './common/Input'; | ||
6 | +import DropDownButton from './common/DropdownButton'; | ||
7 | +import Button from './common/Button'; | ||
8 | +import Modal from './SearchOptionModal'; | ||
9 | +import palette from '../lib/styles/palette'; | ||
10 | + | ||
11 | +// 헤더 사이즈 | ||
12 | +const HeaderHeight = '170px'; | ||
13 | + | ||
14 | +const HeaderTop = styled.div` | ||
15 | + background-color: black; | ||
16 | + height: 20px; | ||
17 | +`; | ||
18 | +const MainContainer = styled.div` | ||
19 | + height: ${HeaderHeight}; | ||
20 | + position: fixed; | ||
21 | + top: 0; | ||
22 | + width: 100%; | ||
23 | + z-index: 999; | ||
24 | + background-color: white; | ||
25 | + border-bottom: 1px solid ${palette.gray4}; | ||
26 | +`; | ||
27 | +const MenuContainer = styled.div` | ||
28 | + position: absolute; | ||
29 | + top: 96px; | ||
30 | + left: 0px; | ||
31 | +`; | ||
32 | + | ||
33 | +const LogoContainer = styled.div` | ||
34 | + padding-right: 20px; | ||
35 | + padding-left: 20px; | ||
36 | + padding-top: 1%; | ||
37 | + img { | ||
38 | + width: 130px; | ||
39 | + | ||
40 | + vertical-align: bottom; | ||
41 | + } | ||
42 | + cursor: pointer; | ||
43 | +`; | ||
44 | + | ||
45 | +const SearchContainer = styled.div` | ||
46 | + display: flex; | ||
47 | + width: 70%; | ||
48 | + position: relative; | ||
49 | + margin-top: 20px; | ||
50 | +`; | ||
51 | + | ||
52 | +const SLink = styled(NavLink)` | ||
53 | + list-style-type: none; | ||
54 | + color: black; | ||
55 | + line-height: 55px; | ||
56 | + vertical-align: middle; | ||
57 | + text-align: center; | ||
58 | + padding-left: 2em; | ||
59 | + padding-right: 2em; | ||
60 | + text-decoration: none !important; | ||
61 | + &:hover { | ||
62 | + background-color: ${palette.gray4}; | ||
63 | + border-bottom: 3px solid ${palette.gray4}; | ||
64 | + } | ||
65 | + &.active { | ||
66 | + font-weight: 600; | ||
67 | + border-bottom: 3px solid ${palette.gray6}; | ||
68 | + float: left; | ||
69 | + line-height: 55px; | ||
70 | + vertical-align: middle; | ||
71 | + text-align: center; | ||
72 | + padding-left: 2em; | ||
73 | + padding-right: 2em; | ||
74 | + color: black; | ||
75 | + text-decoration: none !important; | ||
76 | + } | ||
77 | +`; | ||
78 | +const DropDownWrap = styled.div``; | ||
79 | +const SearchOptionContainer = styled.div``; | ||
80 | + | ||
81 | +const SortOptionContainer = styled.div``; | ||
82 | + | ||
83 | +const OptionContainer = styled.div` | ||
84 | + position: absolute; | ||
85 | + display: flex; | ||
86 | + top: 132px; | ||
87 | + left: 750px; | ||
88 | +`; | ||
89 | + | ||
90 | +const UserContainer = styled.div` | ||
91 | + position: fixed; | ||
92 | + top: 35px; | ||
93 | + right: 0; | ||
94 | + padding-right: 20px; | ||
95 | +`; | ||
96 | + | ||
97 | +const AirContainer = styled.div` | ||
98 | + height: ${HeaderHeight}; | ||
99 | +`; | ||
100 | + | ||
101 | +const DropDownContainer = styled.div``; | ||
102 | + | ||
103 | +const Header = () => { | ||
104 | + const [showModal, setShowModal] = useState(false); | ||
105 | + const openModal = () => { | ||
106 | + setShowModal(prev => !prev); | ||
107 | + }; | ||
108 | + const history = useHistory(); | ||
109 | + const onMainClick = () => { | ||
110 | + history.push(''); | ||
111 | + }; | ||
112 | + return ( | ||
113 | + <> | ||
114 | + <MainContainer> | ||
115 | + <HeaderTop /> | ||
116 | + <SearchContainer> | ||
117 | + <LogoContainer onClick={onMainClick}> | ||
118 | + <img src="eDrive_logo_v2.png" alt="" /> | ||
119 | + </LogoContainer> | ||
120 | + <DropDownContainer> | ||
121 | + <DropDownWrap> | ||
122 | + <DropDownButton | ||
123 | + menuPosition={{ top: 40 }} | ||
124 | + size={100} | ||
125 | + color="blue" | ||
126 | + float="left" | ||
127 | + fontsize="20px" | ||
128 | + height="50px" | ||
129 | + options={[ | ||
130 | + { id: 0, name: '전체' }, | ||
131 | + { id: 1, name: '작성자' }, | ||
132 | + { id: 2, name: '내용' }, | ||
133 | + ]} | ||
134 | + /> | ||
135 | + </DropDownWrap> | ||
136 | + </DropDownContainer> | ||
137 | + <InputBlock color="blue" fontsize="20px" width="70%" display /> | ||
138 | + </SearchContainer> | ||
139 | + <MenuContainer> | ||
140 | + <ul> | ||
141 | + <SLink activeClassName="active" to="/search"> | ||
142 | + 전체 | ||
143 | + </SLink> | ||
144 | + </ul> | ||
145 | + </MenuContainer> | ||
146 | + <OptionContainer> | ||
147 | + <SortOptionContainer> | ||
148 | + <DropDownButton | ||
149 | + color="white" | ||
150 | + width="100px" | ||
151 | + fontsize="15px" | ||
152 | + height="36px" | ||
153 | + title="정렬(기본)" | ||
154 | + menuPosition={{ top: 29 }} | ||
155 | + size="100" | ||
156 | + options={[ | ||
157 | + { id: 0, name: '정렬(기본)' }, | ||
158 | + { id: 1, name: '날짜빠른순' }, | ||
159 | + { id: 2, name: '크기높은순' }, | ||
160 | + { id: 3, name: '크기낮은순' }, | ||
161 | + ]} | ||
162 | + />{' '} | ||
163 | + </SortOptionContainer> | ||
164 | + <SearchOptionContainer onClick={openModal}> | ||
165 | + <Button color="gray" size="md" icon={<FaSearch />}> | ||
166 | + 고급 검색 | ||
167 | + </Button> | ||
168 | + </SearchOptionContainer> | ||
169 | + <Modal showModal={showModal} setShowModal={setShowModal} /> | ||
170 | + </OptionContainer> | ||
171 | + <UserContainer> | ||
172 | + <Button color="blue" width="100px" height="50px" fontsize="20px"> | ||
173 | + 사용자 님 | ||
174 | + </Button> | ||
175 | + </UserContainer> | ||
176 | + </MainContainer> | ||
177 | + <AirContainer /> | ||
178 | + </> | ||
179 | + ); | ||
180 | +}; | ||
181 | +export default Header; |
frontend/src/components/Logo.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | + | ||
4 | +const LogoWrap = styled.div` | ||
5 | + display: flex; | ||
6 | + margin-top: 11%; | ||
7 | + width: 100%; | ||
8 | + align-items: center; | ||
9 | + justify-content: center; | ||
10 | +`; | ||
11 | + | ||
12 | +function Logo() { | ||
13 | + return ( | ||
14 | + <LogoWrap> | ||
15 | + <img src="eDrive_logo.png" alt="" /> | ||
16 | + </LogoWrap> | ||
17 | + ); | ||
18 | +} | ||
19 | + | ||
20 | +export default Logo; |
frontend/src/components/SearchOptionModal.js
0 → 100644
1 | +import React, { useRef, useState } from 'react'; | ||
2 | +import { Card, TextInput } from '@mantine/core'; | ||
3 | +import { useHistory } from 'react-router-dom'; | ||
4 | +import styled from 'styled-components'; | ||
5 | +import Button from './common/Button'; | ||
6 | + | ||
7 | +const Background = styled.div``; | ||
8 | + | ||
9 | +const ModalWrapper = styled.div` | ||
10 | + width: 400px; | ||
11 | + height: 194px; | ||
12 | +`; | ||
13 | + | ||
14 | +const ModalContent = styled.div` | ||
15 | + line-height: 1.8; | ||
16 | + color: #141414; | ||
17 | + p { | ||
18 | + margin-bottom: 1rem; | ||
19 | + } | ||
20 | + align-content: left; | ||
21 | +`; | ||
22 | + | ||
23 | +const SearchWrap = styled.div` | ||
24 | + position: absolute; | ||
25 | + top: 80%; | ||
26 | + right: 20%; | ||
27 | +`; | ||
28 | + | ||
29 | +const CloseWrap = styled.div` | ||
30 | + position: absolute; | ||
31 | + top: 80%; | ||
32 | + left: 20%; | ||
33 | +`; | ||
34 | + | ||
35 | +const StandardWrap = styled.div` | ||
36 | + position: absolute; | ||
37 | + width: 250px; | ||
38 | + top: 15px; | ||
39 | + left: 30%; | ||
40 | +`; | ||
41 | + | ||
42 | +const AdvancedWrap = styled.div` | ||
43 | + width: 250px; | ||
44 | + position: absolute; | ||
45 | + top: 75px; | ||
46 | + left: 30%; | ||
47 | +`; | ||
48 | + | ||
49 | +const TextWrap = styled.div` | ||
50 | + position: absolute; | ||
51 | + width: 360px; | ||
52 | + top: ${props => props.top}; | ||
53 | + right: ${props => props.right}; | ||
54 | + padding-left: 20px; | ||
55 | + padding-bottom: ${props => props.bottom}; | ||
56 | +`; | ||
57 | + | ||
58 | +const Modal = ({ showModal, setShowModal }) => { | ||
59 | + const [query, setQuery] = useState(''); | ||
60 | + const [keywordQuery, setKeywordQuery] = useState(''); | ||
61 | + const [writer, setWriter] = useState(''); | ||
62 | + const history = useHistory(); | ||
63 | + const modalRef = useRef(); | ||
64 | + const closeModal = e => { | ||
65 | + if (modalRef.current === e.target) { | ||
66 | + setShowModal(false); | ||
67 | + } | ||
68 | + }; | ||
69 | + | ||
70 | + return ( | ||
71 | + <> | ||
72 | + {showModal ? ( | ||
73 | + <Background onClick={closeModal}> | ||
74 | + <Card shadow="lg"> | ||
75 | + <ModalWrapper> | ||
76 | + <ModalContent> | ||
77 | + <TextWrap top="8%" right="10%" bottom="4%" height="40px"> | ||
78 | + 기본검색 | ||
79 | + </TextWrap> | ||
80 | + <StandardWrap> | ||
81 | + <TextInput | ||
82 | + inputStyle={{ | ||
83 | + marginBottom: 18, | ||
84 | + fontSize: 15, | ||
85 | + }} | ||
86 | + placeholder="내용을 입력해 주세요." | ||
87 | + type="text" | ||
88 | + value={decodeURIComponent(query)} | ||
89 | + onChange={e => setQuery(e.target.value)} | ||
90 | + onKeyPress={e => { | ||
91 | + if (e.key === 'Enter') { | ||
92 | + if (query === '') { | ||
93 | + alert('검색어를 입력 해 주세요.'); | ||
94 | + return; | ||
95 | + } | ||
96 | + const params = new URLSearchParams({ query }); | ||
97 | + history.push( | ||
98 | + `search?${decodeURIComponent(params.toString())}` | ||
99 | + ); | ||
100 | + } | ||
101 | + }} | ||
102 | + /> | ||
103 | + </StandardWrap> | ||
104 | + <TextWrap top="34%" right="10%" bottom="31%"> | ||
105 | + 고급검색 | ||
106 | + </TextWrap> | ||
107 | + <AdvancedWrap> | ||
108 | + <TextInput | ||
109 | + inputStyle={{ | ||
110 | + marginBottom: 18, | ||
111 | + fontSize: 15, | ||
112 | + }} | ||
113 | + placeholder="단어/문장 검색" | ||
114 | + type="text" | ||
115 | + value={decodeURIComponent(keywordQuery)} | ||
116 | + onChange={e => setKeywordQuery(e.target.value)} | ||
117 | + /> | ||
118 | + <TextInput | ||
119 | + inputStyle={{ | ||
120 | + marginBottom: 18, | ||
121 | + fontSize: 15, | ||
122 | + }} | ||
123 | + placeholder="작성자" | ||
124 | + type="text" | ||
125 | + value={decodeURIComponent(writer)} | ||
126 | + onChange={e => setWriter(e.target.value)} | ||
127 | + /> | ||
128 | + </AdvancedWrap> | ||
129 | + </ModalContent> | ||
130 | + <CloseWrap onClick={() => setShowModal(prev => !prev)}> | ||
131 | + <Button width="100px" color="gray"> | ||
132 | + 닫기 | ||
133 | + </Button> | ||
134 | + </CloseWrap> | ||
135 | + <SearchWrap | ||
136 | + onClick={() => { | ||
137 | + const searchQuery = {}; | ||
138 | + if (query !== '') searchQuery.query = query; | ||
139 | + if (keywordQuery !== '') searchQuery.keyword = keywordQuery; | ||
140 | + if (writer !== '') searchQuery.writer = writer; | ||
141 | + const params = new URLSearchParams(searchQuery); | ||
142 | + history.push( | ||
143 | + `search?${decodeURIComponent(params.toString())}` | ||
144 | + ); | ||
145 | + }} | ||
146 | + > | ||
147 | + <Button width="100px" color="blue"> | ||
148 | + 검색 | ||
149 | + </Button> | ||
150 | + </SearchWrap> | ||
151 | + </ModalWrapper> | ||
152 | + </Card> | ||
153 | + </Background> | ||
154 | + ) : null} | ||
155 | + </> | ||
156 | + ); | ||
157 | +}; | ||
158 | +export default Modal; |
frontend/src/components/common/Button.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import { Button } from '@mantine/core'; | ||
4 | + | ||
5 | +const ButtonBlock = styled.div` | ||
6 | + width: ${props => props.width || '80px'}; | ||
7 | + font-size: ${props => props.fontsize || '15px'}; | ||
8 | + float: ${props => props.float || ''}; | ||
9 | + border-radius: 0.3em; | ||
10 | + cursor: pointer; | ||
11 | +`; | ||
12 | + | ||
13 | +const Buttons = ({ | ||
14 | + children, | ||
15 | + size = 'lg', | ||
16 | + color, | ||
17 | + float, | ||
18 | + width, | ||
19 | + fontsize, | ||
20 | + icon = '', | ||
21 | + onClick, | ||
22 | +}) => { | ||
23 | + return ( | ||
24 | + <ButtonBlock float={float} width={width} fontsize={fontsize}> | ||
25 | + <Button | ||
26 | + onClick={onClick} | ||
27 | + size={size} | ||
28 | + color={color || 'blue'} | ||
29 | + rightIcon={icon} | ||
30 | + > | ||
31 | + {children} | ||
32 | + </Button> | ||
33 | + </ButtonBlock> | ||
34 | + ); | ||
35 | +}; | ||
36 | + | ||
37 | +export default Buttons; |
1 | +import React, { useState, useEffect } from 'react'; | ||
2 | +import { TiArrowSortedDown } from 'react-icons/ti'; | ||
3 | +import { Menu, MenuItem } from '@mantine/core'; | ||
4 | +import { useDispatch } from 'react-redux'; | ||
5 | +import styled from 'styled-components'; | ||
6 | +import { dropdownHeaderColorMap } from '../../lib/styles/palette'; | ||
7 | +import { setSearchOption } from '../../features/searchOption'; | ||
8 | + | ||
9 | +const DropDownBlock = styled.div` | ||
10 | + margin: 0 auto; | ||
11 | + float: ${props => props.float || ''}; | ||
12 | +`; | ||
13 | +const DropDownHeader = styled(Menu)``; | ||
14 | + | ||
15 | +const DropDownWrap = styled.button` | ||
16 | + display: flex; | ||
17 | + justify-content: space-around; | ||
18 | + align-items: center; | ||
19 | + color: ${props => dropdownHeaderColorMap[props.color].color}; | ||
20 | + background-color: ${props => dropdownHeaderColorMap[props.color].background}; | ||
21 | + cursor: pointer; | ||
22 | + &:hover { | ||
23 | + background-color: ${props => | ||
24 | + dropdownHeaderColorMap[props.color].hoverBackground}; | ||
25 | + } | ||
26 | + margin-bottom: 0.8em; | ||
27 | + width: ${props => props.width || '100px'}; | ||
28 | + height: ${props => props.height || '30px'}; | ||
29 | + padding-right: 7%; | ||
30 | + font-size: ${props => props.fontsize || '20px'}; | ||
31 | + border: 1px ${props => dropdownHeaderColorMap[props.color].background}; | ||
32 | +`; | ||
33 | + | ||
34 | +const DropDown = ({ | ||
35 | + options, | ||
36 | + float, | ||
37 | + color = 'blue', | ||
38 | + fontsize, | ||
39 | + width, | ||
40 | + height, | ||
41 | + title = '전체', | ||
42 | + menuPosition, | ||
43 | + size, | ||
44 | +}) => { | ||
45 | + const [menuTitle, setTitle] = useState(''); | ||
46 | + const dispatch = useDispatch(); | ||
47 | + useEffect(() => { | ||
48 | + setTitle(title); | ||
49 | + }, []); | ||
50 | + return ( | ||
51 | + <DropDownBlock float={float} color={color} title={title}> | ||
52 | + <DropDownHeader | ||
53 | + menuPosition={menuPosition} | ||
54 | + size={size} | ||
55 | + control={ | ||
56 | + <DropDownWrap | ||
57 | + options={options} | ||
58 | + color={color} | ||
59 | + fontsize={fontsize} | ||
60 | + width={width} | ||
61 | + height={height} | ||
62 | + > | ||
63 | + {menuTitle} | ||
64 | + <TiArrowSortedDown /> | ||
65 | + </DropDownWrap> | ||
66 | + } | ||
67 | + > | ||
68 | + {options.map(option => ( | ||
69 | + <MenuItem | ||
70 | + value={option.id} | ||
71 | + onClick={() => { | ||
72 | + dispatch(setSearchOption(option.id)); | ||
73 | + setTitle(option.name); | ||
74 | + }} | ||
75 | + > | ||
76 | + {option.name} | ||
77 | + </MenuItem> | ||
78 | + ))} | ||
79 | + </DropDownHeader> | ||
80 | + </DropDownBlock> | ||
81 | + ); | ||
82 | +}; | ||
83 | + | ||
84 | +export default DropDown; |
frontend/src/components/common/Input.js
0 → 100644
1 | +import React, { useState, useEffect } from 'react'; | ||
2 | +import { TextInput } from '@mantine/core'; | ||
3 | +import styled from 'styled-components'; | ||
4 | +import { useHistory, useLocation } from 'react-router-dom'; | ||
5 | +import { useDispatch, useSelector } from 'react-redux'; | ||
6 | +import SearchBox from './SearchBox'; | ||
7 | +import { inputColorMap } from '../../lib/styles/palette'; | ||
8 | +import { esApi } from '../../lib/api/elasticsearch'; | ||
9 | +import { setParsedDocuments } from '../../features/parsedDocumentsSlice'; | ||
10 | + | ||
11 | +const InputBlock = styled.div` | ||
12 | + width: ${props => props.width}; | ||
13 | + height: ${props => props.height}; | ||
14 | + position: relative; | ||
15 | +`; | ||
16 | + | ||
17 | +const InputWrap = styled.div` | ||
18 | + padding-top: ${props => props.paddingsize}; | ||
19 | + position: relative; | ||
20 | + padding-left: 10px; | ||
21 | + height: 100%; | ||
22 | + color: ${props => inputColorMap[props.color].color}; | ||
23 | + outline: none; | ||
24 | + font-size: ${props => props.size}; | ||
25 | + border: 3px solid ${props => inputColorMap[props.color].borderColor}; | ||
26 | + width: 100%; | ||
27 | +`; | ||
28 | +const SearchIconWrap = styled.div` | ||
29 | + position: absolute; | ||
30 | + right: 0; | ||
31 | + top: 0; | ||
32 | +`; | ||
33 | + | ||
34 | +const MyInput = ({ | ||
35 | + color, | ||
36 | + paddingsize = '10px', | ||
37 | + float, | ||
38 | + width, | ||
39 | + height = '50px', | ||
40 | + placeholder = '내용을 입력해 주세요.', | ||
41 | + display, | ||
42 | + fontsize = '20px', | ||
43 | +}) => { | ||
44 | + const [query, setQuery] = useState(''); | ||
45 | + const history = useHistory(); | ||
46 | + const { search } = useLocation(); | ||
47 | + const name = decodeURIComponent( | ||
48 | + new URLSearchParams(search).get('query') || '' | ||
49 | + ); | ||
50 | + const option = decodeURIComponent( | ||
51 | + new URLSearchParams(search).get('option') || '' | ||
52 | + ); | ||
53 | + const dispatch = useDispatch(); | ||
54 | + const { option: currentSearchOption } = useSelector( | ||
55 | + state => state.searchOption | ||
56 | + ); | ||
57 | + | ||
58 | + useEffect(() => { | ||
59 | + const setSearchDatas = async () => { | ||
60 | + if (option === '') { | ||
61 | + const { results } = await esApi.search(name); | ||
62 | + dispatch(setParsedDocuments(results)); | ||
63 | + } else { | ||
64 | + const { results } = await esApi.searchWithOption(name, option); | ||
65 | + dispatch(setParsedDocuments(results)); | ||
66 | + } | ||
67 | + }; | ||
68 | + setQuery(name); | ||
69 | + setSearchDatas(); | ||
70 | + }, [name, option]); | ||
71 | + | ||
72 | + return ( | ||
73 | + <InputBlock | ||
74 | + color={color} | ||
75 | + size={paddingsize} | ||
76 | + float={float} | ||
77 | + width={width} | ||
78 | + placeholder={placeholder} | ||
79 | + display={display} | ||
80 | + height={height} | ||
81 | + > | ||
82 | + <InputWrap color={color} paddingsize={paddingsize} float={float}> | ||
83 | + <TextInput | ||
84 | + inputStyle={{ | ||
85 | + fontSize: fontsize, | ||
86 | + }} | ||
87 | + variant="unstyled" | ||
88 | + placeholder={placeholder} | ||
89 | + type="text" | ||
90 | + value={decodeURIComponent(query)} | ||
91 | + onChange={e => setQuery(e.target.value)} | ||
92 | + onKeyPress={e => { | ||
93 | + if (e.key === 'Enter') { | ||
94 | + if (query === '') { | ||
95 | + alert('검색어를 입력 해 주세요.'); | ||
96 | + return; | ||
97 | + } | ||
98 | + const searchRequest = {}; | ||
99 | + searchRequest.query = query; | ||
100 | + if (currentSearchOption === 'WRITER') | ||
101 | + searchRequest.option = 'writer'; | ||
102 | + if (currentSearchOption === 'CONTENT') | ||
103 | + searchRequest.option = 'content'; | ||
104 | + const params = new URLSearchParams(searchRequest); | ||
105 | + history.push(`search?${decodeURIComponent(params.toString())}`); | ||
106 | + } | ||
107 | + }} | ||
108 | + /> | ||
109 | + </InputWrap> | ||
110 | + <SearchIconWrap | ||
111 | + onClick={() => { | ||
112 | + if (query === '') { | ||
113 | + alert('검색어를 입력 해 주세요.'); | ||
114 | + return; | ||
115 | + } | ||
116 | + const searchRequest = {}; | ||
117 | + searchRequest.query = query; | ||
118 | + if (currentSearchOption === 'WRITER') searchRequest.option = 'writer'; | ||
119 | + if (currentSearchOption === 'CONTENT') | ||
120 | + searchRequest.option = 'content'; | ||
121 | + | ||
122 | + const params = new URLSearchParams(searchRequest); | ||
123 | + history.push(`search?${decodeURIComponent(params.toString())}`); | ||
124 | + }} | ||
125 | + > | ||
126 | + <SearchBox color="blue" size="50px" display={display} /> | ||
127 | + </SearchIconWrap> | ||
128 | + </InputBlock> | ||
129 | + ); | ||
130 | +}; | ||
131 | + | ||
132 | +export default MyInput; |
frontend/src/components/common/SearchBox.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import { AiOutlineSearch } from 'react-icons/ai'; | ||
4 | +import { searchBoxColorMap } from '../../lib/styles/palette'; | ||
5 | + | ||
6 | +const ButtonBlock = styled.div` | ||
7 | + text-align: center; | ||
8 | + display: table-cell; | ||
9 | + vertical-align: middle; | ||
10 | + width: ${props => props.size}; | ||
11 | + height: ${props => props.size}; | ||
12 | + display: ${props => props.display}; | ||
13 | + background-color: ${props => searchBoxColorMap[props.color].background}; | ||
14 | + &:hover { | ||
15 | + background-color: ${props => | ||
16 | + searchBoxColorMap[props.color].hoverBackground}; | ||
17 | + } | ||
18 | + color: ${props => searchBoxColorMap[props.color].color}; | ||
19 | + float: ${props => props.float || ''}; | ||
20 | + border: 1px ${props => searchBoxColorMap[props.color].background}; | ||
21 | + cursor: pointer; | ||
22 | +`; | ||
23 | + | ||
24 | +const SFaSearch = styled(AiOutlineSearch)` | ||
25 | + width: 35px; | ||
26 | + height: 35px; | ||
27 | + margin-right: 5px; | ||
28 | + margin-top: 2px; | ||
29 | +`; | ||
30 | +const SearchBox = ({ color, float, size = '1px', display = 'none' }) => { | ||
31 | + return ( | ||
32 | + <ButtonBlock color={color} float={float} size={size} display={display}> | ||
33 | + <SFaSearch /> | ||
34 | + </ButtonBlock> | ||
35 | + ); | ||
36 | +}; | ||
37 | + | ||
38 | +export default SearchBox; |
frontend/src/components/document/File.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import { Anchor } from '@mantine/core'; | ||
3 | +import { HiOutlineDocumentDownload } from 'react-icons/hi'; | ||
4 | +import palette from '../../lib/styles/palette'; | ||
5 | + | ||
6 | +const File = ({ filename, filepath }) => { | ||
7 | + return ( | ||
8 | + <Anchor style={{ color: palette.blue7 }} href={filepath} size="lg"> | ||
9 | + {filename} | ||
10 | + <HiOutlineDocumentDownload /> | ||
11 | + </Anchor> | ||
12 | + ); | ||
13 | +}; | ||
14 | +export default File; |
1 | +import { Image, Text } from '@mantine/core'; | ||
2 | +import React, { useState, useEffect } from 'react'; | ||
3 | +import styled from 'styled-components'; | ||
4 | +import palette from '../../lib/styles/palette'; | ||
5 | + | ||
6 | +const ImageContainer = styled.div` | ||
7 | + padding: 10px; | ||
8 | + border: 2px solid ${palette.gray3}; | ||
9 | + display: inline-flex; | ||
10 | +`; | ||
11 | +const ImageWrapper = styled.div` | ||
12 | + box-shadow: 2px 1px 7px 1px rgba(0, 0, 0, 0.4); | ||
13 | + margin: 10px; | ||
14 | + border-radius: 10px; | ||
15 | +`; | ||
16 | +const Thumbnails = ({ srcs }) => { | ||
17 | + const [lists, setLists] = useState([]); | ||
18 | + const placeholder = () => { | ||
19 | + if (srcs.length < 4) { | ||
20 | + const list = []; | ||
21 | + const tempImgList = 4 - srcs.length; | ||
22 | + for (let i = 0; i < tempImgList; i += 1) { | ||
23 | + list.push( | ||
24 | + <ImageWrapper> | ||
25 | + <Image width={200} height={250} radius="md" withPlaceholder /> | ||
26 | + </ImageWrapper> | ||
27 | + ); | ||
28 | + } | ||
29 | + setLists(list); | ||
30 | + } | ||
31 | + }; | ||
32 | + useEffect(() => { | ||
33 | + placeholder(); | ||
34 | + }, [srcs]); | ||
35 | + return ( | ||
36 | + <> | ||
37 | + <Text style={{ marginTop: '1rem' }} size="lg" weight={500}> | ||
38 | + 문서 내 이미지 | ||
39 | + </Text> | ||
40 | + <ImageContainer> | ||
41 | + {srcs.map(src => ( | ||
42 | + <ImageWrapper> | ||
43 | + <Image | ||
44 | + width={200} | ||
45 | + height={250} | ||
46 | + radius="md" | ||
47 | + src={src} | ||
48 | + withPlaceholder | ||
49 | + /> | ||
50 | + </ImageWrapper> | ||
51 | + ))} | ||
52 | + {lists} | ||
53 | + </ImageContainer> | ||
54 | + </> | ||
55 | + ); | ||
56 | +}; | ||
57 | + | ||
58 | +export default Thumbnails; |
frontend/src/components/document/index.js
0 → 100644
1 | +/* eslint-disable react/jsx-boolean-value */ | ||
2 | +/* eslint-disable no-unused-vars */ | ||
3 | +import React, { useState } from 'react'; | ||
4 | +import { Container, Popover, Text } from '@mantine/core'; | ||
5 | +import { FaSearchPlus } from 'react-icons/fa'; | ||
6 | +import File from './File'; | ||
7 | +import Thumbnails from './Thumbnail'; | ||
8 | +import Detail from '../Detail'; | ||
9 | + | ||
10 | +const Document = ({ | ||
11 | + content, | ||
12 | + createdDate, | ||
13 | + filePath, | ||
14 | + filename, | ||
15 | + writer, | ||
16 | + images, | ||
17 | +}) => { | ||
18 | + const srcs = images.map(image => JSON.parse(image).image_path); | ||
19 | + const limit = 250; | ||
20 | + const toggleEllipsis = (str, _limit) => ({ | ||
21 | + string: str.slice(0, _limit), | ||
22 | + }); | ||
23 | + const [opened, setOpened] = useState(false); | ||
24 | + | ||
25 | + return ( | ||
26 | + <Container | ||
27 | + style={{ | ||
28 | + padding: '2rem 2rem', | ||
29 | + margin: '0 4rem', | ||
30 | + }} | ||
31 | + > | ||
32 | + <div style={{ display: 'flex', margin: '0.5rem 0' }}> | ||
33 | + <File filename={filename} filepath={filePath} /> | ||
34 | + <Popover | ||
35 | + opened={opened} | ||
36 | + onClose={() => setOpened(false)} | ||
37 | + target={ | ||
38 | + <FaSearchPlus | ||
39 | + onMouseEnter={() => setOpened(true)} | ||
40 | + onMouseLeave={() => setOpened(false)} | ||
41 | + style={{ | ||
42 | + marginTop: 3, | ||
43 | + marginLeft: 3, | ||
44 | + color: '#868e96', | ||
45 | + border: 'none', | ||
46 | + outline: 'none', | ||
47 | + }} | ||
48 | + /> | ||
49 | + } | ||
50 | + position="bottom-start" | ||
51 | + gutter={50} | ||
52 | + bodyStyle={{ pointerEvents: 'none' }} | ||
53 | + > | ||
54 | + <Detail | ||
55 | + filename={filename} | ||
56 | + filePath={filePath} | ||
57 | + writer={writer} | ||
58 | + createdDate={createdDate} | ||
59 | + /> | ||
60 | + </Popover> | ||
61 | + </div> | ||
62 | + <Text color="green">경로: {filePath}</Text> | ||
63 | + <Text>{toggleEllipsis(content, limit).string}...</Text> | ||
64 | + <Thumbnails srcs={srcs} /> | ||
65 | + </Container> | ||
66 | + ); | ||
67 | +}; | ||
68 | + | ||
69 | +export default Document; |
frontend/src/components/login/LoginForm.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import { TextInput, Button, Container, CONTAINER_SIZES } from '@mantine/core'; | ||
3 | +import { useForm } from '@mantine/hooks'; | ||
4 | +import styled from 'styled-components'; | ||
5 | +import { Link } from 'react-router-dom'; | ||
6 | +import palette from '../../lib/styles/palette'; | ||
7 | +import Logo from '../Logo'; | ||
8 | + | ||
9 | +const LoginFormBlock = styled.div` | ||
10 | + display: flex; | ||
11 | + height: 100%; | ||
12 | + align-items: center; | ||
13 | +`; | ||
14 | +const ButtonBlock = styled.div` | ||
15 | + display: flex; | ||
16 | + justify-content: space-between; | ||
17 | + margin-top: 2rem; | ||
18 | +`; | ||
19 | +const FormBlock = styled.div` | ||
20 | + padding-top: 2rem; | ||
21 | +`; | ||
22 | +const LoginForm = () => { | ||
23 | + const { onSubmit, errors, values, setFieldValue } = useForm({ | ||
24 | + initialValues: { | ||
25 | + email: '', | ||
26 | + password: '', | ||
27 | + termOfService: false, | ||
28 | + }, | ||
29 | + validationRules: { | ||
30 | + email: value => /^[^\s@]+@[^\s@]+$/.test(value), | ||
31 | + }, | ||
32 | + }); | ||
33 | + return ( | ||
34 | + <LoginFormBlock> | ||
35 | + <Container | ||
36 | + size={CONTAINER_SIZES.xs} | ||
37 | + padding={20} | ||
38 | + style={{ | ||
39 | + display: 'block', | ||
40 | + width: CONTAINER_SIZES.xs, | ||
41 | + padding: '5rem', | ||
42 | + border: `1px ${palette.gray5} solid`, | ||
43 | + borderRadius: '5px', | ||
44 | + }} | ||
45 | + > | ||
46 | + <Logo /> | ||
47 | + <FormBlock> | ||
48 | + <TextInput | ||
49 | + required | ||
50 | + label="Email" | ||
51 | + placeholder="example@example.com" | ||
52 | + error={errors.email && 'Please specify valid email'} | ||
53 | + value={values.email} | ||
54 | + onChange={event => | ||
55 | + setFieldValue('email', event.currentTarget.value) | ||
56 | + } | ||
57 | + /> | ||
58 | + <TextInput | ||
59 | + required | ||
60 | + label="Password" | ||
61 | + placeholder="Your password" | ||
62 | + value={values.password} | ||
63 | + type="password" | ||
64 | + style={{ marginTop: '2rem' }} | ||
65 | + onChange={event => | ||
66 | + setFieldValue('password', event.currentTarget.value) | ||
67 | + } | ||
68 | + /> | ||
69 | + <ButtonBlock> | ||
70 | + <Link to="/"> | ||
71 | + <Button variant="outline">홈으로</Button> | ||
72 | + </Link> | ||
73 | + <Button type="button" onClick={onSubmit}> | ||
74 | + 로그인 | ||
75 | + </Button> | ||
76 | + </ButtonBlock> | ||
77 | + </FormBlock> | ||
78 | + </Container> | ||
79 | + </LoginFormBlock> | ||
80 | + ); | ||
81 | +}; | ||
82 | +export default LoginForm; |
1 | +import { createSlice } from '@reduxjs/toolkit'; | ||
2 | + | ||
3 | +const parsedDocumentsSlice = createSlice({ | ||
4 | + name: 'parsedDocuments', | ||
5 | + initialState: { | ||
6 | + documents: [], | ||
7 | + }, | ||
8 | + reducers: { | ||
9 | + setParsedDocuments: (state, action) => { | ||
10 | + state.documents = action.payload; | ||
11 | + }, | ||
12 | + }, | ||
13 | +}); | ||
14 | + | ||
15 | +export const { setParsedDocuments } = parsedDocumentsSlice.actions; | ||
16 | + | ||
17 | +export default parsedDocumentsSlice.reducer; |
frontend/src/features/searchOption.js
0 → 100644
1 | +import { createSlice } from '@reduxjs/toolkit'; | ||
2 | + | ||
3 | +const OPTION = ['ALL', 'WRITER', 'CONTENT']; | ||
4 | + | ||
5 | +const searchOptionSlice = createSlice({ | ||
6 | + name: 'searchOption', | ||
7 | + initialState: { | ||
8 | + option: OPTION[0], | ||
9 | + }, | ||
10 | + reducers: { | ||
11 | + setSearchOption: (state, action) => { | ||
12 | + state.option = OPTION[action.payload]; | ||
13 | + }, | ||
14 | + }, | ||
15 | +}); | ||
16 | + | ||
17 | +export const { setSearchOption } = searchOptionSlice.actions; | ||
18 | + | ||
19 | +export default searchOptionSlice.reducer; |
frontend/src/index.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import ReactDOM from 'react-dom'; | ||
3 | +import { configureStore } from '@reduxjs/toolkit'; | ||
4 | +import { Provider } from 'react-redux'; | ||
5 | +import App from './App'; | ||
6 | +import rootReducer from './reducers'; | ||
7 | + | ||
8 | +const store = configureStore({ | ||
9 | + reducer: rootReducer, | ||
10 | +}); | ||
11 | + | ||
12 | +ReactDOM.render( | ||
13 | + <React.StrictMode> | ||
14 | + <Provider store={store}> | ||
15 | + <App /> | ||
16 | + </Provider> | ||
17 | + </React.StrictMode>, | ||
18 | + document.getElementById('root') | ||
19 | +); |
frontend/src/lib/api/elasticsearch.js
0 → 100644
1 | +import axios from 'axios'; | ||
2 | + | ||
3 | +const esInstance = axios.create({ | ||
4 | + baseURL: process.env.REACT_APP_API_ENDPOINT, | ||
5 | + headers: { | ||
6 | + 'Content-Type': 'application/json', | ||
7 | + Authorization: `Bearer ${process.env.REACT_APP_SEARCH_KEY}`, | ||
8 | + }, | ||
9 | +}); | ||
10 | + | ||
11 | +export const esApi = { | ||
12 | + search: async searchWord => { | ||
13 | + const res = await esInstance.post( | ||
14 | + `/api/as/v1/engines/${process.env.REACT_APP_ENGINE_NAME}/search`, | ||
15 | + { | ||
16 | + query: searchWord, | ||
17 | + } | ||
18 | + ); | ||
19 | + | ||
20 | + return res.data; | ||
21 | + }, | ||
22 | + searchWithOption: async (searchWord, option) => { | ||
23 | + const res = await esInstance.post( | ||
24 | + `/api/as/v1/engines/${process.env.REACT_APP_ENGINE_NAME}/search`, | ||
25 | + { | ||
26 | + query: searchWord, | ||
27 | + search_fields: { | ||
28 | + [option]: {}, | ||
29 | + }, | ||
30 | + } | ||
31 | + ); | ||
32 | + | ||
33 | + return res.data; | ||
34 | + }, | ||
35 | +}; |
frontend/src/lib/styles/palette.js
0 → 100644
1 | +const palette = { | ||
2 | + /* blue */ | ||
3 | + blue0: '#e7f5ff', | ||
4 | + blue1: '#d0ebff', | ||
5 | + blue2: '#a5d8ff', | ||
6 | + blue3: '#74c0fc', | ||
7 | + blue4: '#4dabf7', | ||
8 | + blue5: '#339af0', | ||
9 | + blue6: '#228be6', | ||
10 | + blue7: '#1c7ed6', | ||
11 | + blue8: '#1971c2', | ||
12 | + blue9: '#1864ab', | ||
13 | + /* cyan */ | ||
14 | + cyan0: '#e3fafc', | ||
15 | + cyan1: '#c5f6fa', | ||
16 | + cyan2: '#99e9f2', | ||
17 | + cyan3: '#66d9e8', | ||
18 | + cyan4: '#3bc9db', | ||
19 | + cyan5: '#22b8cf', | ||
20 | + cyan6: '#15aabf', | ||
21 | + cyan7: '#1098ad', | ||
22 | + cyan8: '#0c8599', | ||
23 | + cyan9: '#0b7285', | ||
24 | + /* gray */ | ||
25 | + gray0: '#f8f9fa', | ||
26 | + gray1: '#f1f3f5', | ||
27 | + gray2: '#e9ecef', | ||
28 | + gray3: '#dee2e6', | ||
29 | + gray4: '#ced4da', | ||
30 | + gray5: '#adb5bd', | ||
31 | + gray6: '#868e96', | ||
32 | + gray7: '#495057', | ||
33 | + gray8: '#343a40', | ||
34 | + gray9: '#212529', | ||
35 | +}; | ||
36 | + | ||
37 | +export const buttonColorMap = { | ||
38 | + blue: { | ||
39 | + background: palette.blue6, | ||
40 | + color: 'white', | ||
41 | + hoverBackground: palette.blue7, | ||
42 | + }, | ||
43 | + gray: { | ||
44 | + background: palette.gray7, | ||
45 | + color: 'black', | ||
46 | + hoverBackground: palette.gray8, | ||
47 | + }, | ||
48 | + white: { | ||
49 | + background: 'white', | ||
50 | + color: 'black', | ||
51 | + hoverBackground: palette.gray2, | ||
52 | + }, | ||
53 | +}; | ||
54 | + | ||
55 | +export const inputColorMap = { | ||
56 | + blue: { | ||
57 | + borderColor: palette.blue6, | ||
58 | + color: 'black', | ||
59 | + placeholder: palette.gray5, | ||
60 | + }, | ||
61 | +}; | ||
62 | + | ||
63 | +export const dropdownListColorMap = { | ||
64 | + blue: { | ||
65 | + background: 'white', | ||
66 | + color: palette.blue8, | ||
67 | + borderColor: palette.blue8, | ||
68 | + }, | ||
69 | +}; | ||
70 | + | ||
71 | +export const dropdownHeaderColorMap = { | ||
72 | + blue: { | ||
73 | + background: palette.blue6, | ||
74 | + color: 'white', | ||
75 | + hoverBackground: palette.blue5, | ||
76 | + borderColor: palette.blue8, | ||
77 | + }, | ||
78 | + white: { | ||
79 | + background: 'white', | ||
80 | + color: 'black', | ||
81 | + hoverBackground: palette.gray2, | ||
82 | + }, | ||
83 | +}; | ||
84 | + | ||
85 | +export const searchBoxColorMap = { | ||
86 | + blue: { | ||
87 | + background: palette.blue6, | ||
88 | + color: 'white', | ||
89 | + hoverBackground: palette.blue5, | ||
90 | + }, | ||
91 | +}; | ||
92 | + | ||
93 | +export default palette; |
frontend/src/pages/HomePage.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import { Link } from 'react-router-dom'; | ||
4 | +import Button from '../components/common/Button'; | ||
5 | +import DropDownButton from '../components/common/DropdownButton'; | ||
6 | +import Input from '../components/common/Input'; | ||
7 | + | ||
8 | +const Main = styled.div` | ||
9 | + display: flex; | ||
10 | + align-items: center; | ||
11 | + justify-content: center; | ||
12 | +`; | ||
13 | + | ||
14 | +const Container = styled.div` | ||
15 | + justify-content: center; | ||
16 | + align-items: center; | ||
17 | + flex-direction: column; | ||
18 | +`; | ||
19 | + | ||
20 | +const LogoWrap = styled.div` | ||
21 | + display: flex; | ||
22 | + margin-top: 11%; | ||
23 | + height: 30vh; | ||
24 | + align-items: center; | ||
25 | + justify-content: center; | ||
26 | +`; | ||
27 | + | ||
28 | +const SearchBlock = styled.div` | ||
29 | + display: flex; | ||
30 | + width: 50%; | ||
31 | +`; | ||
32 | +const LoginButtonBlock = styled.div` | ||
33 | + position: fixed; | ||
34 | + top: 20px; | ||
35 | + right: 20px; | ||
36 | +`; | ||
37 | + | ||
38 | +const SLink = styled(Link)` | ||
39 | + text-decoration: none !important; | ||
40 | +`; | ||
41 | + | ||
42 | +const HomePage = () => { | ||
43 | + return ( | ||
44 | + <Container> | ||
45 | + <LogoWrap> | ||
46 | + <img src="eDrive_logo.png" alt="" /> | ||
47 | + </LogoWrap> | ||
48 | + <Main> | ||
49 | + <SearchBlock> | ||
50 | + <DropDownButton | ||
51 | + fontsize="20px" | ||
52 | + height="50px" | ||
53 | + options={[ | ||
54 | + { id: 0, name: '전체' }, | ||
55 | + { id: 1, name: '작성자' }, | ||
56 | + { id: 2, name: '내용' }, | ||
57 | + ]} | ||
58 | + /> | ||
59 | + <Input color="blue" paddingsize="10px" width="100%" display /> | ||
60 | + </SearchBlock> | ||
61 | + | ||
62 | + {/* Todo : 로그인 했을 경우 로그인 버튼 숨기기 */} | ||
63 | + <LoginButtonBlock> | ||
64 | + <SLink to="/login"> | ||
65 | + <Button>로그인</Button> | ||
66 | + </SLink> | ||
67 | + </LoginButtonBlock> | ||
68 | + </Main> | ||
69 | + </Container> | ||
70 | + ); | ||
71 | +}; | ||
72 | + | ||
73 | +export default HomePage; |
frontend/src/pages/LoginPage.js
0 → 100644
frontend/src/pages/SearchPage.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import { useSelector } from 'react-redux'; | ||
3 | +import Documents from '../components/Documents'; | ||
4 | +import Header from '../components/Header'; | ||
5 | + | ||
6 | +const SearchPage = () => { | ||
7 | + const { documents } = useSelector(state => state.parsedDocuments); | ||
8 | + return ( | ||
9 | + <> | ||
10 | + <Header /> | ||
11 | + {documents.length === 0 ? ( | ||
12 | + <div>검색 결과가 없습니다.</div> | ||
13 | + ) : ( | ||
14 | + <Documents datas={documents} /> | ||
15 | + )} | ||
16 | + </> | ||
17 | + ); | ||
18 | +}; | ||
19 | + | ||
20 | +export default SearchPage; |
frontend/src/reducers/index.js
0 → 100644
1 | +import { combineReducers } from 'redux'; | ||
2 | +import parsedDocumentsReducer from '../features/parsedDocumentsSlice'; | ||
3 | +import searchOptionReducer from '../features/searchOption'; | ||
4 | + | ||
5 | +export default combineReducers({ | ||
6 | + parsedDocuments: parsedDocumentsReducer, | ||
7 | + searchOption: searchOptionReducer, | ||
8 | +}); |
frontend/yarn.lock
0 → 100644
This diff could not be displayed because it is too large.
-
Please register or login to post a comment