Skip to content

Commit

Permalink
Merge pull request #144 from boostcampwm-2022/dev
Browse files Browse the repository at this point in the history
[배포] Production v1.0.1
  • Loading branch information
yeynii authored Dec 15, 2022
2 parents 1be54f5 + f977fd2 commit f7b9cc1
Show file tree
Hide file tree
Showing 27 changed files with 319 additions and 159 deletions.
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@

![Javascript](https://img.shields.io/badge/javascript-ES6+-yellow?logo=javascript)
![NodeJS](https://img.shields.io/badge/node.js-v18-green?logo=node.js)
![데모](https://user-images.githubusercontent.com/25934842/207359316-f7056911-d26a-4671-bc3c-2a80e46f24b8.gif)

</div>

### 서비스 배포

- Dev 서버 : http://49.50.172.204:3000/

- Production 서버 : http://101.101.217.49:3000/
### 서비스 링크 : https://paperef.com

### 팀원

Expand All @@ -39,7 +36,6 @@
</td>
<td align="center"><a href="https://github.com/yeynii">최예윤</a>
</tr>
<tr>
</table>

### 개발 환경 세팅
Expand Down Expand Up @@ -84,6 +80,7 @@ ELASTIC_USER=
ELASTIC_PASSWORD=
ALLOW_UPDATE=
MAIL_TO=
SHOULD_RUN_BATCH=
```

## 기술스택
Expand All @@ -100,7 +97,7 @@ MAIL_TO=
- 키워드 자동완성 검색 서비스 제공
- 키워드 검색 서비스 제공
- 논문 DOI를 통한 인용관계 시각화 서비스 제공
- 사용자는 키워드 검색시 PRV 데이터베이스에 있는 정보만 조회할 수 있으며, 데이터베이스에 없는 논문에 대한 데이터 수집은 Request batch에 의해 처리되므로 검색 결과를 즉시 받아보지 못할 수 있습니다.
- 사용자는 키워드 검색시 PRV 데이터베이스에 있는 정보 혹은 Crossref API를 통해 요청한 정보를 조회할 수 있으며, 데이터베이스에 없는 논문에 대한 데이터 수집은 Request batch에 의해 처리되므로 검색 결과를 즉시 받아보지 못할 수 있습니다.
- Request batch에 의해 수집된 결과는 데이터베이스에 저장됩니다.
- 추가 문의사항은 [email protected] 로 연락바랍니다.

Expand Down
4 changes: 0 additions & 4 deletions backend/src/ranking/ranking.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,4 @@ export class RankingController {
async getTen() {
return await this.rankingService.getTen();
}
@Get('/insert')
async insertCache(@Query('keyword') searchStr: string) {
return this.rankingService.insertRedis(searchStr);
}
}
16 changes: 8 additions & 8 deletions backend/src/ranking/tests/ranking.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ describe('RankingServiceTest', () => {
describe('/keyword-ranking', () => {
it('redis date가 10개 이하인 경우', async () => {
//Case 1. redis date가 10개 이하인 경우
const topTen = await controller.getTen();
const topTen = await service.getTen();
expect(topTen.length).toBeLessThanOrEqual(10);
});
it('데이터 삽입 후 topTen 체크', async () => {
//Case 2. 데이터 삽입 후 topTen 체크
const flag = await controller.insertCache('9번째 데이터');
const flag = await service.insertRedis('9번째 데이터');
expect(flag).toBe('new');
const topTen = await controller.getTen();
expect(topTen.length).toBe(9);
const flag2 = await controller.insertCache('10번째 데이터');
const flag2 = await service.insertRedis('10번째 데이터');
expect(flag2).toBe('new');
const topTen2 = await controller.getTen();
expect(topTen2.length).toBe(10);
});
it('2위인 "사랑해요" 데이터가 한번 더 검색시 1위로 업데이트', async () => {
//Case 3. 2위인 "사랑해요" 데이터가 한번 더 검색시 1위로 업데이트
const flag = await controller.insertCache('사랑해요');
const flag = await service.insertRedis('사랑해요');
expect(flag).toBe('update');
const topTen = await controller.getTen();
expect(topTen[0].keyword).toBe('부스트캠프');
Expand All @@ -46,23 +46,23 @@ describe('RankingServiceTest', () => {
describe('/keyword-ranking/insert', () => {
// Case1. 기존 redis에 없던 데이터 삽입
it('기존 redis에 없던 데이터 삽입', async () => {
const result = await controller.insertCache('newData');
const result = await service.insertRedis('newData');
expect(result).toBe('new');
});
// Case2. 기존 redis에 있던 데이터 삽입
it('기존 redis에 있던 데이터 삽입', async () => {
const result = await controller.insertCache('부스트캠프');
const result = await service.insertRedis('부스트캠프');
expect(result).toBe('update');
});
//Case3. redis에 빈 검색어 입력
it('빈 검색어 redis에 삽입', async () => {
await expect(controller.insertCache('')).rejects.toEqual(
await expect(service.insertRedis('')).rejects.toEqual(
new BadRequestException({ status: 400, error: 'bad request' }),
);
});
//Case4. insert 실패시 타임 아웃 TimeOut
it('insert 실패시 타임 아웃 TimeOut', async () => {
await expect(controller.insertCache('')).rejects.toEqual(
await expect(service.insertRedis('')).rejects.toEqual(
new BadRequestException({ status: 400, error: 'bad request' }),
);
});
Expand Down
12 changes: 12 additions & 0 deletions backend/src/search/search.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ export class SearchController {
const keywordHasSet = await this.batchService.setKeyword(keyword);
if (keywordHasSet) this.batchService.searchBatcher.pushToQueue(0, 0, -1, true, keyword);

// Elasticsearch 검색 결과가 없을 경우, Crossref 검색
if (totalItems === 0) {
const { items: papers, totalItems } = await this.searchService.getPapersFromCrossref(keyword, rows, page);
return {
papers,
pageInfo: {
totalItems,
totalPages: Math.ceil(totalItems / rows),
},
};
}

const papers = data.hits.hits.map((paper) => new PaperInfoExtended(paper._source));
return {
papers,
Expand Down
16 changes: 14 additions & 2 deletions backend/src/search/search.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, RequestTimeoutException } from '@nestjs/common';
import {
CrossRefItem,
PaperInfoExtended,
PaperInfo,
PaperInfoDetail,
CrossRefPaperResponse,
CrossRefResponse,
} from './entities/crossRef.entity';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { MgetOperation, SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { HttpService } from '@nestjs/axios';
import { CROSSREF_API_PAPER_URL } from '../util';
import { CROSSREF_API_PAPER_URL, CROSSREF_API_URL } from '../util';
import { ELASTIC_INDEX } from 'src/envLayer';

@Injectable()
Expand Down Expand Up @@ -75,6 +76,17 @@ export class SearchService {

return new PaperInfoDetail(data);
};
async getPapersFromCrossref(keyword: string, rows: number, page: number, selects?: string[]) {
const crossRefdata = await this.httpService.axiosRef
.get<CrossRefResponse>(CROSSREF_API_URL(keyword, rows, page, selects))
.catch((err) => {
throw new RequestTimeoutException(err.message);
});
const items = crossRefdata.data.message.items.map((item) => this.parsePaperInfoExtended(item));
const totalItems = crossRefdata.data.message['total-results'];
return { items, totalItems };
}

async getPaperFromCrossref(doi: string) {
try {
const item = await this.httpService.axiosRef.get<CrossRefPaperResponse>(CROSSREF_API_PAPER_URL(doi));
Expand Down
7 changes: 6 additions & 1 deletion backend/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { MAIL_TO } from './envLayer';

const BASE_URL = 'https://api.crossref.org/works';
export const CROSSREF_API_URL = (keyword: string, rows = 5, page = 1, selects: string[] = ['author', 'title', 'DOI']) =>
export const CROSSREF_API_URL = (
keyword: string,
rows = 5,
page = 1,
selects: string[] = ['title', 'author', 'created', 'is-referenced-by-count', 'references-count', 'DOI'],
) =>
`${BASE_URL}?query=${keyword}&rows=${rows}&select=${selects.join(',')}&offset=${rows * (page - 1)}&mailto=${MAIL_TO}`;

export const MAX_ROWS = 1000;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ it('Footer 렌더링 테스트', () => {
);
});
const span = container?.querySelector('span');
expect(span?.textContent).toBe('문의사항, 버그제보: vp[email protected]');
expect(span?.textContent).toBe('문의사항, 버그제보: viewpoint[email protected]');
});
2 changes: 1 addition & 1 deletion frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface FooterProps {
const Footer = ({ bgColor, contentColor }: FooterProps) => {
return (
<Container bgColor={bgColor} contentColor={contentColor}>
<span>문의사항, 버그제보: vp[email protected]</span>
<span>문의사항, 버그제보: viewpoint[email protected]</span>
<FooterRight>
<DataLink
href="https://insidious-abacus-0a9.notion.site/PRV-eb42bf64ddc5435a8f0f939329e0429c"
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const IconButton = ({ icon, ...rest }: IProps) => {
};

const Button = styled.button`
display: flex;
align-items: center;
background-color: transparent;
cursor: pointer;
`;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/search/AutoCompletedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const AutoCompleted = styled.li<{ hovered: boolean }>`

const Title = styled.div`
${({ theme }) => theme.TYPO.body1}
line-height: 1.1em;
`;

const Author = styled.div`
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/search/RecentKeywordsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IconButton } from '@/components';
import { ClockIcon, XIcon } from '@/icons';
import { Ellipsis } from '@/style/styleUtils';
import { setLocalStorage } from '@/utils/storage';
import { isEmpty } from 'lodash-es';
import { Dispatch, SetStateAction, useEffect } from 'react';
Expand Down Expand Up @@ -42,7 +43,7 @@ const RecentKeywordsList = ({
onMouseDown={() => handleMouseDown(keyword)}
>
<ClockIcon />
{keyword}
<KeywordText>{keyword}</KeywordText>
<DeleteButton
icon={<XIcon />}
onMouseDown={(e) => handleRecentKeywordRemove(e, keyword)}
Expand Down Expand Up @@ -71,6 +72,11 @@ const Keyword = styled.li<{ hovered: boolean }>`
background-color: ${({ theme, hovered }) => (hovered ? theme.COLOR.gray1 : 'auto')};
`;

const KeywordText = styled(Ellipsis)`
width: 100%;
display: block;
`;

const NoResult = styled.div`
padding-top: 25px;
text-align: center;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { Link, Node } from '@/pages/PaperDetail/components/ReferenceGraph';
import theme from '@/style/theme';
import * as d3 from 'd3';
import { useCallback } from 'react';

const NORMAL_SYMBOL_SIZE = 20;
const STAR_SYMBOL_SIZE = 100;

export default function useNodeUpdate(
nodes: any[],
changeHoveredNode: (key: string) => void,
const useGraph = (
nodeSelector: SVGGElement | null,
linkSelector: SVGGElement | null,
addChildrensNodes: (doi: string) => void,
) {
return useCallback(
(nodesSelector: SVGGElement) => {
changeHoveredNode: (doi: string) => void,
) => {
const drawLink = useCallback(
(links: Link[]) => {
d3.select(linkSelector)
.selectAll('line')
.data(links)
.join('line')
.attr('x1', (d) => (d.source as Node).x || null)
.attr('y1', (d) => (d.source as Node).y || null)
.attr('x2', (d) => (d.target as Node).x || null)
.attr('y2', (d) => (d.target as Node).y || null);
},
[linkSelector],
);

const drawNode = useCallback(
(nodes: Node[]) => {
const NORMAL_SYMBOL_SIZE = 20;
const STAR_SYMBOL_SIZE = 100;

const normalSymbol = d3.symbol().type(d3.symbolSquare).size(NORMAL_SYMBOL_SIZE)();
const starSymbol = d3.symbol().type(d3.symbolStar).size(STAR_SYMBOL_SIZE)();

Expand All @@ -20,7 +36,7 @@ export default function useNodeUpdate(
return d3.scaleLinear([0, 4], ['white', theme.COLOR.secondary2]).interpolate(d3.interpolateRgb)(loged);
};

d3.select(nodesSelector)
d3.select(nodeSelector)
.selectAll('path')
.data(nodes)
.join('path')
Expand All @@ -32,18 +48,23 @@ export default function useNodeUpdate(
.on('mouseout', () => changeHoveredNode(''))
.on('click', (_, d) => d.doi && addChildrensNodes(d.doi));

d3.select(nodesSelector)
d3.select(nodeSelector)
.selectAll('text')
.data(nodes)
.join('text')
.text((d) => `${d.author} ${d.publishedYear ? `(${d.publishedYear})` : ''}`)
.attr('x', (d) => d.x)
.attr('y', (d) => d.y + 10)
.attr('x', (d) => d.x || null)
.attr('y', (d) => (d.y ? d.y + 10 : null))
.attr('dy', 5)
.style('font-weight', 700)
.on('mouseover', (_, d) => d.doi && changeHoveredNode(d.key))
.on('mouseout', () => changeHoveredNode(''))
.on('click', (_, d) => d.doi && addChildrensNodes(d.doi));
},
[nodes, addChildrensNodes, changeHoveredNode],
[nodeSelector, addChildrensNodes, changeHoveredNode],
);
}

return { drawLink, drawNode };
};

export default useGraph;
13 changes: 7 additions & 6 deletions frontend/src/hooks/graph/useGraphData.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { IPaperDetail } from '@/api/api';
import { Link, Node } from '@/pages/PaperDetail/components/ReferenceGraph';
import { useEffect, useRef, useState } from 'react';

export default function useGraphData<T>(data: IPaperDetail) {
const [links, setLinks] = useState<any[]>([]);
const nodes = useRef<any[]>([]);
export default function useGraphData(data: IPaperDetail) {
const [links, setLinks] = useState<Link[]>([]);
const nodes = useRef<Node[]>([]);
const doiMap = useRef<Map<string, number>>(new Map());

useEffect(() => {
Expand All @@ -27,7 +28,7 @@ export default function useGraphData<T>(data: IPaperDetail) {
citations: v.citations,
publishedYear: v.publishedAt && new Date(v.publishedAt).getFullYear(),
})),
];
] as Node[];

newNodes.forEach((node) => {
const foundIndex = doiMap.current.get(node.key);
Expand All @@ -49,7 +50,7 @@ export default function useGraphData<T>(data: IPaperDetail) {
target: reference.key.toLowerCase(),
}));
setLinks((prev) => [...prev, ...newLinks]);
}, [data]);
}, [data, links]);

return { nodes: nodes.current, links } as T;
return { nodes: nodes.current, links };
}
Loading

0 comments on commit f7b9cc1

Please sign in to comment.