안형욱

Merge branch 'develop' into 'master'

eDrive v1.0.0 Merge



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