Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "[배포] Production v1.0.1" #143

Merged
merged 1 commit into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

![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>

### 서비스 링크 : https://paperef.com
### 서비스 배포

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

- Production 서버 : http://101.101.217.49:3000/

### 팀원

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

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

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

Expand Down
4 changes: 4 additions & 0 deletions backend/src/ranking/ranking.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ 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 service.getTen();
const topTen = await controller.getTen();
expect(topTen.length).toBeLessThanOrEqual(10);
});
it('데이터 삽입 후 topTen 체크', async () => {
//Case 2. 데이터 삽입 후 topTen 체크
const flag = await service.insertRedis('9번째 데이터');
const flag = await controller.insertCache('9번째 데이터');
expect(flag).toBe('new');
const topTen = await controller.getTen();
expect(topTen.length).toBe(9);
const flag2 = await service.insertRedis('10번째 데이터');
const flag2 = await controller.insertCache('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 service.insertRedis('사랑해요');
const flag = await controller.insertCache('사랑해요');
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 service.insertRedis('newData');
const result = await controller.insertCache('newData');
expect(result).toBe('new');
});
// Case2. 기존 redis에 있던 데이터 삽입
it('기존 redis에 있던 데이터 삽입', async () => {
const result = await service.insertRedis('부스트캠프');
const result = await controller.insertCache('부스트캠프');
expect(result).toBe('update');
});
//Case3. redis에 빈 검색어 입력
it('빈 검색어 redis에 삽입', async () => {
await expect(service.insertRedis('')).rejects.toEqual(
await expect(controller.insertCache('')).rejects.toEqual(
new BadRequestException({ status: 400, error: 'bad request' }),
);
});
//Case4. insert 실패시 타임 아웃 TimeOut
it('insert 실패시 타임 아웃 TimeOut', async () => {
await expect(service.insertRedis('')).rejects.toEqual(
await expect(controller.insertCache('')).rejects.toEqual(
new BadRequestException({ status: 400, error: 'bad request' }),
);
});
Expand Down
12 changes: 0 additions & 12 deletions backend/src/search/search.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,6 @@ 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: 2 additions & 14 deletions backend/src/search/search.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Injectable, NotFoundException, RequestTimeoutException } from '@nestjs/common';
import { Injectable, NotFoundException } 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, CROSSREF_API_URL } from '../util';
import { CROSSREF_API_PAPER_URL } from '../util';
import { ELASTIC_INDEX } from 'src/envLayer';

@Injectable()
Expand Down Expand Up @@ -76,17 +75,6 @@ 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: 1 addition & 6 deletions backend/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
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[] = ['title', 'author', 'created', 'is-referenced-by-count', 'references-count', 'DOI'],
) =>
export const CROSSREF_API_URL = (keyword: string, rows = 5, page = 1, selects: string[] = ['author', 'title', '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('문의사항, 버그제보: viewpoint.prv@gmail.com');
expect(span?.textContent).toBe('문의사항, 버그제보: vp.prv@gmail.com');
});
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>문의사항, 버그제보: viewpoint.prv@gmail.com</span>
<span>문의사항, 버그제보: vp.prv@gmail.com</span>
<FooterRight>
<DataLink
href="https://insidious-abacus-0a9.notion.site/PRV-eb42bf64ddc5435a8f0f939329e0429c"
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ const IconButton = ({ icon, ...rest }: IProps) => {
};

const Button = styled.button`
display: flex;
align-items: center;
background-color: transparent;
cursor: pointer;
`;
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/search/AutoCompletedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ 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: 1 addition & 7 deletions frontend/src/components/search/RecentKeywordsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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 @@ -43,7 +42,7 @@ const RecentKeywordsList = ({
onMouseDown={() => handleMouseDown(keyword)}
>
<ClockIcon />
<KeywordText>{keyword}</KeywordText>
{keyword}
<DeleteButton
icon={<XIcon />}
onMouseDown={(e) => handleRecentKeywordRemove(e, keyword)}
Expand Down Expand Up @@ -72,11 +71,6 @@ 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
13 changes: 6 additions & 7 deletions frontend/src/hooks/graph/useGraphData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { IPaperDetail } from '@/api/api';
import { Link, Node } from '@/pages/PaperDetail/components/ReferenceGraph';
import { useEffect, useRef, useState } from 'react';

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

useEffect(() => {
Expand All @@ -28,7 +27,7 @@ export default function useGraphData(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 @@ -50,7 +49,7 @@ export default function useGraphData(data: IPaperDetail) {
target: reference.key.toLowerCase(),
}));
setLinks((prev) => [...prev, ...newLinks]);
}, [data, links]);
}, [data]);

return { nodes: nodes.current, links };
return { nodes: nodes.current, links } as T;
}
54 changes: 23 additions & 31 deletions frontend/src/hooks/graph/useGraphEmphasize.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Link, Node } from '@/pages/PaperDetail/components/ReferenceGraph';
import * as d3 from 'd3';
import { useCallback, useEffect } from 'react';
import { useTheme } from 'styled-components';
import * as d3 from 'd3';
import { useEffect, useCallback } from 'react';

const styles = {
EMPHASIZE_OPACITY: '1',
BASIC_OPACITY: '0.5',
EMPHASIZE_STROKE_WIDTH: '1.5px',
EMPHASIZE_STROKE_WIDTH: '0.8px',
BASIC_STROKE_WIDTH: '0.5px',
EMPHASIZE_STROKE_DASH: 'none',
BASIC_STROKE_DASH: '1',
Expand All @@ -15,8 +14,8 @@ const styles = {
export default function useGraphEmphasize(
nodeSelector: SVGGElement | null,
linkSelector: SVGGElement | null,
nodes: Node[],
links: Link[],
nodes: any[],
links: any[],
hoveredNode: string,
selectedKey: string,
) {
Expand All @@ -41,53 +40,46 @@ export default function useGraphEmphasize(
d3.select(nodeSelector)
.selectAll('text')
.data(nodes)
.filter((d) => {
return links
.filter((l) => l.source === hoveredNode)
.map((l) => l.target)
.includes(d.key);
})
.filter((d) =>
links
.filter((l) => l.source.key === hoveredNode)
.map((l) => l.target.key)
.includes(d.key),
)
.style('fill-opacity', styles.EMPHASIZE_OPACITY);
}, [hoveredNode, links, nodeSelector, nodes, theme]);

useEffect(() => {
if (nodeSelector === null) return;

// click된 노드 강조
d3.select(nodeSelector)
.selectAll('text')
.data(nodes)
.filter((d) => d.key === selectedKey)
.style('fill', theme.COLOR.secondary2);
.style('fill', theme.COLOR.secondary1);

// click된 노드의 자식 노드들 강조
d3.select(nodeSelector)
.selectAll('text')
.data(nodes)
.filter((d) => {
const result = links
.filter((l) => l.source === selectedKey)
.map((l) => l.target)
.includes(d.key);
return result;
})
.style('fill', theme.COLOR.secondary2);
.filter((d) =>
links
.filter((l) => l.source.key === selectedKey)
.map((l) => l.target.key)
.includes(d.key),
)
.style('fill', theme.COLOR.secondary1);

// click/hover된 노드의 링크 강조
d3.select(linkSelector)
.selectAll('line')
.data(links)
.style('stroke', (d) => getStyles(d.source as string, theme.COLOR.secondary1, theme.COLOR.gray1))
.style('stroke-width', (d) =>
getStyles(d.source as string, styles.EMPHASIZE_STROKE_WIDTH, styles.BASIC_STROKE_WIDTH),
)
.style('stroke', (d) => getStyles(d.source.key, theme.COLOR.secondary1, theme.COLOR.gray1))
.style('stroke-width', (d) => getStyles(d.source.key, styles.EMPHASIZE_STROKE_WIDTH, styles.BASIC_STROKE_WIDTH))
.style('stroke-dasharray', (d) =>
getStyles(d.source as string, styles.EMPHASIZE_STROKE_DASH, styles.BASIC_STROKE_DASH),
getStyles(d.source.key, styles.EMPHASIZE_STROKE_DASH, styles.BASIC_STROKE_DASH),
);

return () => {
d3.select(nodeSelector).selectAll('text').style('fill-opacity', styles.BASIC_OPACITY);
d3.select(nodeSelector).selectAll('text').style('fill', theme.COLOR.offWhite);
};
}, [nodeSelector, nodes, links, selectedKey, linkSelector, getStyles, theme]);
}, [nodeSelector, hoveredNode, nodes, links, selectedKey, linkSelector, getStyles, theme]);
}
18 changes: 18 additions & 0 deletions frontend/src/hooks/graph/useLinkUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as d3 from 'd3';
import { useCallback } from 'react';

export default function useLinkUpdate(links: any[]) {
return useCallback(
(linksSelector: SVGGElement) => {
d3.select(linksSelector)
.selectAll('line')
.data(links)
.join('line')
.attr('x1', (d) => d.source?.x)
.attr('y1', (d) => d.source?.y)
.attr('x2', (d) => d.target?.x)
.attr('y2', (d) => d.target?.y);
},
[links],
);
}
Loading