Commit f9bcb8f6 authored by Kim, Chaerin's avatar Kim, Chaerin
Browse files

1차 크롤링 완성, 상준선배 2번반복 수정필요

parent 7c1243b6
This diff is collapsed.
......@@ -37,5 +37,6 @@
"last 1 firefox version",
"last 1 safari version"
]
}
},
"proxy":"http://localhost:3001"
}
import React, { useEffect, useState } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ohuh from './ohuh.PNG';
import { Container, Row, Form, Image, InputGroup, Button } from 'react-bootstrap';
import { Container, Row, Form, Image, InputGroup, Button, Col } from 'react-bootstrap';
function App() {
const [state, setState] = useState(false);
......@@ -25,28 +25,32 @@ function App() {
return (
<Container className="vh-100">
<Row className="d-flex justify-content-md-center">
<Image src={ohuh} />
</Row>
<Row className="d-flex justify-content-md-center">
<Form className="vw-100" onSubmit={handleSubmit}>
<InputGroup>
<Form.Control
size="lg"
placeholder="검색어를 입력하세요."
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
onChange={handleChange}
/>
<InputGroup.Append>
<Button type='submit' variant="outline-secondary">검색</Button>
</InputGroup.Append>
</InputGroup>
</Form>
</Row>
<Container className="vh-100 d-flex justify-content-md-center align-items-center">
<Col md={6} lassName=" d-flex justify-content-center">
<Row style={{marginBottom:20}}>
<Image src={ohuh} />
</Row>
<Row style={{marginBottom:500}}>
<Form className="vw-100" onSubmit={handleSubmit}>
<InputGroup>
<Form.Control
size="lg"
placeholder="검색어를 입력하세요."
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
onChange={handleChange}
/>
<InputGroup.Append>
<Button type='submit' variant="outline-secondary">검색</Button>
</InputGroup.Append>
</InputGroup>
</Form>
</Row>
</Col>
</Container>
);
}
export default App;
export default App;
\ No newline at end of file
import { Pagination } from "react-bootstrap";
import React from 'react';
function Paginations(props) {
return (
<Pagination>
<Pagination.First onClick={() => props.handlePage(1)} />
{props.index === 1 ? <Pagination.Prev onClick={()=>props.handlePage(props.index)} /> : <Pagination.Prev onClick={()=>props.handlePage(props.index - 1)} />}
{props.index === props.endPage-1 ? <Pagination.Item onClick={()=>props.handlePage(props.index - 3)}>{props.index - 3}</Pagination.Item> : ""}
{props.index === props.endPage ? <Pagination.Item onClick={()=>props.handlePage(props.index - 4)}>{props.index - 4}</Pagination.Item> : ""}
{props.index === props.endPage ? <Pagination.Item onClick={()=>props.handlePage(props.index - 3)}>{props.index - 3}</Pagination.Item> : ""}
{props.index < 3 ? "" : <Pagination.Item onClick={()=>props.handlePage(props.index - 2)}>{props.index - 2}</Pagination.Item>}
{props.index === 1 ? "" : <Pagination.Item onClick={()=>props.handlePage(props.index - 1)}>{props.index - 1}</Pagination.Item>}
<Pagination.Item active>{props.index}</Pagination.Item>
{props.index === props.endPage ? "" : <Pagination.Item onClick={()=>props.handlePage(props.index + 1)}>{props.index + 1}</Pagination.Item>}
{props.index > props.endPage-2 ? "" : <Pagination.Item onClick={()=>props.handlePage(props.index + 2)}>{props.index + 2}</Pagination.Item>}
{props.index === 1 ? <Pagination.Item onClick={()=>props.handlePage(props.index + 3)}>{props.index + 3}</Pagination.Item> : ""}
{props.index === 1 ? <Pagination.Item onClick={()=>props.handlePage(props.index + 4)}>{props.index + 4}</Pagination.Item> : ""}
{props.index === 2 ? <Pagination.Item onClick={()=>props.handlePage(props.index + 3)}>{props.index + 3}</Pagination.Item> : ""}
{props.index === props.endPage ? "" : <Pagination.Next onClick={()=>props.handlePage(props.index + 1)} />}
<Pagination.Last onClick={() =>props.handlePage(props.endPage)} />
</Pagination>
)
}
export default Paginations
import React from 'react';
import { Modal, Container, Row, Col, Button } from 'react-bootstrap';
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { Modal, Container, Row, Col, Button, Badge, Card, Accordion, Carousel } from 'react-bootstrap';
function Place(props) {
const [reviews, setReviews] = useState([])
const getReview = () => {
axios.get(`/api/review/${props.search.name}`)
.then(res => {
setReviews(res.data)
})
.catch(err => {
console.log(err)
})
}
useEffect(() => {
getReview();
}, [])
return (
<>
<style type="text/css">
{`
.modal-backdrop.in {
opacity: 0;
}
`}
</style>
<Modal {...props}
size="lg"
keyboard="true"
variant="backdrop.in"
// show={lgShow}
// onHide={() => setLgShow(false)}
aria-labelledby="example-modal-sizes-title-lg">
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
{props.search.name}
</Modal.Title>
</Modal.Header>
<Modal.Body className="show-grid">
<Container>
<Row className="mt-4">
<div>주도 전역을 지배하는 한라산은 남한에서 가장 높은 산으로 높이는 1,947.3m이다. 한라산이라는 이름은 산이 높아 산정에 서면 은하수를 잡아당길 있다는 뜻이며, 부악·원산·선산·두무악·영주산·부라산·혈망봉·여장군 등으로도 불려왔다.</div>
<a href="https://www.daum.net">다음의 블로그</a>
<Modal {...props}
size="xl"
keyboard="true"
variant="backdrop.in"
// show={lgShow}
// onHide={() => setLgShow(false)}
aria-labelledby="example-modal-sizes-title-lg">
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter" style={{ fontSize: '40px' }}>
{props.index + 1}. {props.search.name}
</Modal.Title>
</Modal.Header>
<Modal.Body className="show-grid">
<Container style={{ fontSize: '40px' }}>
{reviews.map((review, index) => {
return (
<Row className="mt-4">
<a href={review.link}>{review.title}</a>
<div>{review.summary}</div>
</Row>
)
})}
</Container>
{/* <Accordion defaultActiveKey="0">
<Accordion.Toggle as={Button} variant="link" eventKey="0">
<a className="mb-2">다음의 블로그 보기</a>
</Accordion.Toggle>
<Accordion.Collapse eventKey="0">
<Card.Body><div>주도 전역을 지배하는 한라산은 남한에서 가장 높은 산으로 높이는 1,947.3m이다. 한라산이라는 이름은 산이 높아 산정에 서면 은하수를 잡아당길 수 있다는 뜻이며, 부악·원산·선산·두무악·영주산·부라산·혈망봉·여장군 등으로도 불려왔다.</div></Card.Body>
</Accordion.Collapse>
<Row>
<Accordion.Toggle as={Button} variant="link" eventKey="1">
<a className="mb-2">네이버의 블로그 보기</a>
</Accordion.Toggle>
<Accordion.Collapse eventKey="1">
<Card.Body><div>한라산은 1966년 한라산 천연보호구역으로, 1970년 국립공원으로 지정되었다. 그리고 2002년에는 유네스코 생물권보전지역으로 지정되었으며 2007년에는 유네스코 세계자연유산으로 등재되었다. 2008년에는 물장오리오름 산정화구호 습지가 람사르 습지로 등록되어 보호 관리되고 있다</div></Card.Body>
</Accordion.Collapse>
</Row>
<Row className="mt-4">
<div>한라산은 1966 한라산 천연보호구역으로, 1970 국립공원으로 지정되었다. 그리고 2002년에는 유네스코 생물권보전지역으로 지정되었으며 2007년에는 유네스코 세계자연유산으로 등재되었다. 2008년에는 물장오리오름 산정화구호 습지가 람사르 습지로 등록되어 보호 관리되고 있다</div>
<a href="https://www.naver.com/">네이버의 블로그</a>
<Row>
<Accordion.Toggle as={Button} variant="link" eventKey="2">
<a className="mb-2">구글의 블로그 보기</a>
</Accordion.Toggle>
<Accordion.Collapse eventKey="2">
<Card.Body><div>한라산은 제주도에 있는 해발 1,947.06m, 면적 약 1,820km²의 화산으로, 제주도의 면적 대부분을 차지하고 있다. 정상에 백록담이라는 화산호가 있는데, 백록담이라는 이름은 흰 사슴이 물을 먹는 곳이라는 뜻에서 왔다고 전해진다.</div></Card.Body>
</Accordion.Collapse>
</Row>
<Row className="mt-4">
<div>한라산은 제주도에 있는 해발 1,947.06m, 면적 1,820km² 화산으로, 제주도의 면적 대부분을 차지하고 있다. 정상에 백록담이라는 화산호가 있는데, 백록담이라는 이름은 사슴이 물을 먹는 곳이라는 뜻에서 왔다고 전해진다.</div>
<a href="https://www.google.com/">구글의 블로그</a></Row>
</Container>
</Modal.Body>
<Modal.Footer>
<Button onClick={props.onHide}>Close</Button>
</Modal.Footer>
</Modal>
</>
</Accordion>
<Row style={{ color: 'white' }}>
<a href="https://www.daum.net/" className="mb-2">다음</a><a href="https://www.naver.com/" className="mb-2">네이버</a><a href="https://www.google.com/" className="mb-2">구글</a>
</Row>
// </Container>
<Carousel>
<Carousel.Item interval={1000}>
<h3>다음 블로그</h3>
<div>주도 전역을 지배하는 한라산은 남한에서 가장 높은 산으로 높이는 1,947.3m이다. 한라산이라는 이름은 산이 높아 산정에 서면 은하수를 잡아당길 수 있다는 뜻이며, 부악·원산·선산·두무악·영주산·부라산·혈망봉·여장군 등으로도 불려왔다.</div>
</Carousel.Item>
<Carousel.Item interval={1000}>
<h3>네이버 블로그</h3>
<div>한라산은 1966년 한라산 천연보호구역으로, 1970년 국립공원으로 지정되었다. 그리고 2002년에는 유네스코 생물권보전지역으로 지정되었으며 2007년에는 유네스코 세계자연유산으로 등재되었다. 2008년에는 물장오리오름 산정화구호 습지가 람사르 습지로 등록되어 보호 관리되고 있다</div>
</Carousel.Item>
<Carousel.Item interval={1000}>
<h3>구글 블로그</h3>
<div>한라산은 제주도에 있는 해발 1,947.06m, 면적 약 1,820km²의 화산으로, 제주도의 면적 대부분을 차지하고 있다. 정상에 백록담이라는 화산호가 있는데, 백록담이라는 이름은 흰 사슴이 물을 먹는 곳이라는 뜻에서 왔다고 전해진다.</div>
</Carousel.Item>
</Carousel> */}
</Modal.Body>
<Modal.Footer>
<Button block onClick={props.onHide}>Close</Button>
</Modal.Footer>
</Modal>
);
}
export default Place;
\ No newline at end of file
export default Place;
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ohuh from './ohuh-sm.PNG';
import Place from './Components/Place';
import { Container, Form, Row, Col, Card, Image, InputGroup, FormControl, Button, Pagination } from 'react-bootstrap';
import Paginations from './Components/Paginations';
function Search(props) {
const endPage = 10;
const [state, setState] = useState(false);
const [index, setIndex] = useState(1);
const [show, setShow] = useState(false);
const [showSet, setShowSet] = useState([false, false, false, false]);
const [search, setSearch] = useState(props.location.state.id);
const [mobile, setMobile] = useState();
useEffect(() => {
if (window.innerWidth < 960) {
setMobile(true)
} else {
setMobile(false)
}
}, []);
const places = [{
name: "한라산(hallasan)",
address: "제주 서귀포시 토평동 산15-1",
img: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/KOCIS_Halla_Mountain_in_Jeju-do_%286387785543%29.jpg/269px-KOCIS_Halla_Mountain_in_Jeju-do_%286387785543%29.jpg",
img: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/KOCIS_Halla_Mountain_in_Jeju-do_%286387785543%29.jpg/269px-KOCIS_Halla_Mountain_in_Jeju-do_%286387785543%29.jpg?size=200x200",
}, {
name: "성산일출봉(sungsan)",
address: "제주 서귀포시 성산읍 성산리 1",
img: "https://www.jeju.go.kr/pub/site/geopark/images/sub/sub03/02%EC%A7%80%EC%A7%88%EB%A7%88%EC%9D%84%EC%9D%B4%EC%95%BC%EA%B8%B0/%EC%A7%80%EC%A7%88%EB%A7%88%EC%9D%84/%EC%A7%80%EC%A7%88%EB%A7%88%EC%9D%84_%EC%84%B1%EC%82%B0%EC%9D%BC%EC%B6%9C%EB%B4%89/1412402261.jpg",
img: "https://www.jeju.go.kr/pub/site/geopark/images/sub/sub03/02%EC%A7%80%EC%A7%88%EB%A7%88%EC%9D%84%EC%9D%B4%EC%95%BC%EA%B8%B0/%EC%A7%80%EC%A7%88%EB%A7%88%EC%9D%84/%EC%A7%80%EC%A7%88%EB%A7%88%EC%9D%84_%EC%84%B1%EC%82%B0%EC%9D%BC%EC%B6%9C%EB%B4%89/1412402261.jpg?400/400",
}, {
name: "해녀의 집(haenyeo)",
address: "제주 서귀포시 성산읍 한도로 141-13지번오조리 3 오조해녀의집",
......@@ -415,6 +426,11 @@ function Search(props) {
}} />;
}
const handlePage = (num) => {
setIndex(num);
}
const handleChange = (e) => {
setSearch(e.target.value);
}
......@@ -432,14 +448,16 @@ function Search(props) {
return page
}
const pagePlace = paginate(places, index)
return (
<Container>
<Row className="mt-2 mb-2">
<Form className="vw-100" onSubmit={handleSubmit}>
<Container >
<Link to="/" className="d-flex justify-content-center"><Image src={ohuh} /></Link>
<Row className="mb-2" className="d-flex justify-content-center">
<Form style={{ width: "90vw" }} onSubmit={handleSubmit}>
<InputGroup size="lg">
<Link to="/"><Image src={ohuh} /></Link>
<FormControl
placeholder="검색어를 입력하세요."
value={search}
......@@ -456,38 +474,33 @@ function Search(props) {
<Row className="d-flex flex-wrap">
{pagePlace.map((place, index) => {
return (
<Col key={index} md={6}>
<Card>
<Card.Img variant="top" src={place.img} />
<Card.Body>
<Card.Title>{place.name}</Card.Title>
<Card.Text>
<Col key={index} md={6} >
<Card align="center" border="info" style={{ margin: "3%" }}>
<Card.Title style={{ margin: "3%", fontSize: '200%', fontWeight: 'bold' }} >{place.name}</Card.Title>
<Card.Img variant="top" style={{ padding: "5%", width: "100%", height: "340px" }} src={place.img} />
<Card.Body >
<Card.Text style={{ overflow: 'auto', fontSize: '25px', width: '100%', height: "80px" }} >
{place.address} </Card.Text>
<Button variant="primary" onClick={() => setShow(true)}>{place.name} 자세히 살펴보기</Button>
<Place search={place} show={show} onHide={() => setShow(false)} />
<Button variant="primary" onClick={() => {
const showArr = [false, false, false, false]
showArr[index] = true
setShowSet(showArr)
}}>{place.name} 자세히 살펴보기</Button>
<Place search={place} index={index} show={showSet[index]} onHide={() => setShowSet([false, false, false, false])} />
</Card.Body>
</Card>
</Col>
)
})}
</Row>
{console.log(showSet)}
{/* show가 전부 true로 바뀌어서 전부 다 보이게 되는 것이다. */}
<Row className="mt-2 d-flex justify-content-center">
<Pagination>
<Pagination.First onClick={() => setIndex(1)} />
{index === 1 ? "" : <Pagination.Prev onClick={() => setIndex(index - 1)} />}
{index < 3 ? "" : <Pagination.Item onClick={() => setIndex(index - 2)}>{index - 2}</Pagination.Item>}
{index === 1 ? "" : <Pagination.Item onClick={() => setIndex(index - 1)}>{index - 1}</Pagination.Item>}
<Pagination.Item active>{index}</Pagination.Item>
{index === 10 ? "" : <Pagination.Item onClick={() => setIndex(index + 1)}>{index + 1}</Pagination.Item>}
{index > 8 ? "" : <Pagination.Item onClick={() => setIndex(index + 2)}>{index + 2}</Pagination.Item>}
{index === 10 ? "" : <Pagination.Next onClick={() => setIndex(index + 1)} />}
<Pagination.Last onClick={() => setIndex(10)} />
</Pagination>
<Paginations index={index} endPage={endPage} handlePage={handlePage}></Paginations>
</Row>
</Container>
);
}
export default Search;
\ No newline at end of file
export default Search;
......@@ -4,6 +4,7 @@ import './index.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
import Search from './Search';
import axios from 'axios';
import reportWebVitals from './reportWebVitals';
import {
BrowserRouter as Router,
......@@ -12,13 +13,17 @@ import {
Redirect,
} from "react-router-dom";
axios.defaults.validateStatus = function (status) {
return status < 500; // default
}
ReactDOM.render(
<React.StrictMode>
<Router>
<Switch>
<Route exact path="/" component={App} />
<Route path="/search" component={Search} />
<Redirect path="/search/:id" to="/search" />
<Redirect path="/search/:search" to="/search" />
</Switch>
</Router>
</React.StrictMode>,
......
This diff is collapsed.
......@@ -15,7 +15,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"charset": "^1.0.1",
"cheerio": "^1.0.0-rc.5",
"express": "^4.17.1",
"iconv": "^3.0.0",
"iconv-lite": "^0.6.2",
"jschardet": "^2.2.1",
"mongoose": "^5.11.9",
"request": "^2.88.2",
"request-promise": "^4.2.6",
"styled-components": "^5.2.1"
}
}
const config = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3001,
jwtSecret: process.env.JWT_SECRET || 'My_Secret_Key',
mongoDbUri: process.env.MONGODB_URI || 'mongodb://localhost/search_page'
}
export default config
\ No newline at end of file
import Review from '../models/Review.js'
import cheerio from "cheerio";
import express from 'express';
import request from 'request-promise'
import jschardet from 'jschardet'
import iconv from 'iconv'
import fs from 'fs'
const Iconv = iconv.Iconv
const signup = async (req, res) => {
res.send("안녕하세요")
}
const search = async (req, res) => {
console.log(req.params.search)
console.log("req", req)
const url = "https://www.google.com/search?q=%ED%95%9C%EB%9D%BC%EC%82%B0%20site%3Atistory.com&oq=tistory&aqs=chrome..69i57j0l4j69i60l3.1746j0j4&sourceid=chrome&ie=UTF-8&ved=2ahUKEwis_bSFz4buAhWVdXAKHU0tBaoQ2wF6BAgIEAE&ei=T1D1X-yZD5XrwQPN2pTQCg"
request(url)
.then(anyToUtf8)
.then((html) => {
// fs.writeFileSync("googlez.txt", '\ufeff' + html, { encoding: 'utf8' });
let $ = cheerio.load(html, null, false);
let places = []
$('.kCrYT').each(function (i) {
places[i] = {
title: $(this).find('h3').text(),
link: $(this).find('a').attr('href'),
summary: $(this).find('.s3v9rd ').text(),
}
})
// console.log(places)
res.send(places)
})
function anyToUtf8(str) {
const { encoding } = jschardet.detect(str);
console.log("source encoding = " + encoding);
const iconv = new Iconv(encoding, "utf-8//translit//ignore");
return iconv.convert(str).toString();
}
// try {
// const newPlace = await new Place({
// name: req.params.search,
// address,
// img,
// })
// }
}
export default { signup, search }
\ No newline at end of file
import mongoose from 'mongoose'
const { String, ObjectId } = mongoose.Schema.Types
const { String } = mongoose.Schema.Types
const PlaceSchema = new mongoose.Schema({
name: {
......@@ -17,14 +17,14 @@ const PlaceSchema = new mongoose.Schema({
required: true,
default: "https://t1.daumcdn.net/thumb/R600x0/?fname=http%3A%2F%2Ft1.daumcdn.net%2Fqna%2Fimage%2F4b035cdf8372d67108f7e8d339660479dfb41bbd",
},
search: {
type: Array,
required: true,
},
time: {
type: Array,
required: true,
},
review: {
type: ObjectId,
ref: 'Review'
}
}, {
timestamps: true
})
......
......@@ -7,15 +7,14 @@ const ReviewSchema = new mongoose.Schema({
type: String,
required: true,
},
address: {
content: {
type: String,
required: true,
unique: true,
},
imag: {
type: String,
keyword: {
type: Array,
required: true,
default: "https://t1.daumcdn.net/thumb/R600x0/?fname=http%3A%2F%2Ft1.daumcdn.net%2Fqna%2Fimage%2F4b035cdf8372d67108f7e8d339660479dfb41bbd",
},
time: {
type: Array,
......
This diff is collapsed.
import express from 'express'
import review from '../controllers/review.controller.js'
const router = express.Router()
router.route('/api/review/:search')
.post(review.signup)
.get(review.search)
export default router
\ No newline at end of file
import express from 'express'
import connectDb from './utils/connectDb.js'
import placeRouter from './routes/place.routes.js'
import reviewRouter from './routes/review.routes.js'
connectDb()
const app = express()
app.use(express.json())
app.use(placeRouter)
app.use(reviewRouter)
app.get('/', (req, res) => {
console.log("/ req.body", req.body)
res.json({ message: "http://localhost3001/ 에 연결됨" })
})
app.listen(3001, () => {
console.log('Server is listening on port 3001')
......
This diff is collapsed.
......@@ -8,7 +8,7 @@ async function connectDb() {
return
}
const db = await mongoose.connect(config.mongoDburi, {
const db = await mongoose.connect(config.mongoDbUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment