안형욱

Merge branch 'develop' into 'master'

eDrive v1.0.0 Merge



See merge request !17
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
1 +REACT_APP_API_ENDPOINT={API_ENDPOINT}
2 +REACT_APP_SEARCH_KEY={SEARCH_KEY}
3 +REACT_APP_ENGINE_NAME={ENGINE_NAME}
...\ No newline at end of file ...\ No newline at end of file
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
1 +{
2 + "singleQuote": true,
3 + "semi": true,
4 + "tabWidth": 2,
5 + "trailingComma": "es5",
6 + "printWidth": 80,
7 + "arrowParens": "avoid"
8 +}
...\ No newline at end of file ...\ No newline at end of file
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 +}
No preview for this file type
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
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 +}
1 +# https://www.robotstxt.org/robotstxt.html
2 +User-agent: *
3 +Disallow:
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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 +);
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 +};
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;
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;
1 +import React from 'react';
2 +import Login from '../components/login/LoginForm';
3 +
4 +const LoginPage = () => {
5 + return (
6 + <>
7 + <Login />
8 + </>
9 + );
10 +};
11 +
12 +export default LoginPage;
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;
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 +});
This diff could not be displayed because it is too large.