Jeongmin Seo

Release version0.2.0

MIT License
Copyright (c) 2022 Jeongmin Seo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
# 맛집지도 : 실시간 맛집 정보 공유 플랫폼
카카오맵 API와 실시간 채팅 기능을 통해 사용자들과 소통함으로써 맛집에 대한 정보를 얻을 수 있는 플랫폼입니다.
<div align="center">
<img src="/uploads/f2c62af097272859d8f37acda908d6aa/그림1.png" width="300" height="300">
</div>
### 📚 STACKS
![node-16.14.2](https://img.shields.io/badge/Node-16.14.2-green)
![express-4.18.1](https://img.shields.io/badge/Express-4.18.1-green)
![html-latest](https://img.shields.io/badge/html-5.2-green)
![css-latest](https://img.shields.io/badge/css-3-green)
![nginx-latest](https://img.shields.io/badge/nginx-latest-blue)
![socket.io-latest](https://img.shields.io/badge/socket.io-latest-blue)
![AWS_RDS](https://img.shields.io/badge/AWS_RDS-blue)
![mysql-2.18.1](https://img.shields.io/badge/Mysql-2.18.1-yellowgreen)
![AWS_RDS](https://img.shields.io/badge/AWS_RDS-yellowgreen)
## About the Project
- 로그인을 통해 사이트에 들어갈 수 있으며, 지역별 맛집정보를 파악할 수 있습니다.
- 타 유저들과 실시간 소통을 통해 맛집에 대한 정보를 빠르게 얻을 수 있습니다.
### Overview
- 추가 예정(이미지)
### Project Architecture
- 추가 예정(이미지)
### Built With
- [node.js](https://nodejs.org/ko/)
- [express](https://expressjs.com/ko/)
- [AWS_EC2](https://aws.amazon.com/ko/)
- [AWS_RDS](https://aws.amazon.com/ko/)
- [socket.io](https://socket.io/)
- [nginx](https://www.nginx.com/)
- [mysql](https://www.mysql.com/)
## Getting Started ( Installation )
### Prerequisites
- 추가 예정
### Installation
- 추가 예정
## Roadmap
- [x] 1. 로그인/회원가입 창 구현
- [x] 2. 맛집지도 UI 및 기능 구현
- [x] 3. socket.io를 이용한 실시간 채팅 구현
- [x] 4. AWS, Mysql을 이용한 데이터베이스 구축
- [ ] 5. 서버 구축 및 배포
## Contributing
프로젝트에 기여하고 싶으신 분들은 아래 절차를 따라주시기 바랍니다.
1. 프로젝트 fork
2. feature branch 생성 (git checkout -b feature/name)
3. commit (git commit -m "Add feature")
4. push (git push origin feature/name)
5. pull request 생성
## License
MIT 라이센스 아래 사용 가능합니다. LICENSE.txt를 통해 자세한 정보를 확인하세요.
## Contact
- 서정민 : balljm@khu.ac.kr
- 양주미 : luckyyjm@khu.ac.kr
\ No newline at end of file
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
app/log/*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
**node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.package-lock.json
\ No newline at end of file
"use strict";
//모듈
const express = require("express");
const bodyParser = require("body-parser");
//환경변수 (운영체제간 gap 없애고자)
const dotenv = require("dotenv");
dotenv.config();
const compression = require("compression");
const methodOverride = require("method-override");
const path = require("path")
const socketIO = require("socket.io")
const moment = require("moment")
const http = require("http");
const app = express();
const server = http.createServer(app);
var cors = require("cors");
const { logger } = require("./src/config/winston");
//app이라는 express 객체 생성
//라우팅
const home = require("./src/routes/home");
const port = 3000;
const jwtMiddleware = require("./src/config/jwtMiddleware");
const io = socketIO(server);
// 앱 세팅
app.set("views", "./src/views");
app.set("view engine", "ejs");
app.use(express.static(`${__dirname}/src/public`));
app.use(express.static(path.join(__dirname, "src")))
app.use(bodyParser.json());
//url통해 전달되는 데이터에 한글, 공백 등의 문자 오류 해결
app.use(bodyParser.urlencoded({extended: true}));
app.use(compression()); // HTTP 요청을 압축 및 해제
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(methodOverride());
app.use(cors());
// app.use("/restaurants", require("../app/src/routes/home/restaurant.route"));
// require("../app/src/routes/home/restaurant.route")(app);
app.use("/", home); //미들웨어 등록해주는 method
io.on('connection', (socket) => {
socket.on("chatting", (data)=>{
const { name, msg } = data;
io.emit("chatting", {
name,
msg,
time: moment.format("h:ss A")
})
})
});
logger.info(`${process.env.NODE_ENV} - API Server Start At Port ${port}`);
module.exports = app;
\ No newline at end of file
"use strict";
const app = require("../app");
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log("서버 가동");
});
This diff is collapsed. Click to expand it.
{
"name": "login",
"version": "1.0.0",
"main": "app.js",
"bin": {
"login": "www.js"
},
"dependencies": {
"body-parser": "^1.20.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dateformat": "^4.3.1",
"dotenv": "^16.0.1",
"ejs": "^3.1.8",
"express": "^4.18.1",
"jsonwebtoken": "^8.5.1",
"method-override": "^3.0.0",
"moment": "^2.29.3",
"mysql": "^2.18.1",
"mysql2": "^2.2.0",
"regex-email": "^1.0.2",
"request": "^2.88.2",
"socket.io": "^4.5.1",
"winston": "^3.2.1",
"winston-daily-rotate-file": "^4.2.1"
},
"devDependencies": {},
"scripts": {
"start": "nodemon ./bin/www.js",
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "NODE_ENV=development node index.js",
"prod": "NODE_ENV=production node index.js"
},
"author": "Jeongmin Seo, Jumi Yang",
"license": "MIT",
"keywords": [],
"description": "Node.js API Server"
}
// const mysql = require("mysql");
const { logger } = require("./winston");
const mysql2 = require("mysql2/promise");
// const db = mysql.createConnection({
// host: process.env.DB_HOST,
// user: process.env.DB_USER,
// password: process.env.DB_PASSWORD,
// database: process.env.DB_DATABASE, //schema
// });
const pool = mysql2.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE, //schema
connectionLimit: 10000,
multipleStatements: true,
});
// db.connect();
module.exports = {
pool: pool,
};
\ No newline at end of file
const jwt = require("jsonwebtoken");
const secret_config = require("./db");
const jwtMiddleware = function (req, res, next) {
// read the token from header or url
const token = req.headers["x-access-token"] || req.query.token;
// token does not exist
if (!token) {
return res.status(403).json({
isSuccess: false,
code: 403,
message: "로그인이 되어 있지 않습니다.",
});
}
try {
const verifiedToken = jwt.verify(token, secret_config.jwtsecret);
req.verifiedToken = verifiedToken;
next();
} catch {
res.status(403).json({
isSuccess: false,
code: 403,
message: "검증 실패",
});
}
};
module.exports = jwtMiddleware;
const { createLogger, format, transports } = require('winston');
require('winston-daily-rotate-file');
const fs = require('fs');
const env = process.env.NODE_ENV || 'development';
const logDir = 'log';
// https://lovemewithoutall.github.io/it/winston-example/
// Create the log directory if it does not exist
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir)
}
const dailyRotateFileTransport = new transports.DailyRotateFile({
level: 'debug',
filename: `${logDir}/%DATE%-smart-push.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
});
const logger = createLogger({
level: env === 'development' ? 'debug' : 'info',
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
format.json()
),
transports: [
new transports.Console({
level: 'info',
format: format.combine(
format.colorize(),
format.printf(
info => `${info.timestamp} ${info.level}: ${info.message}`
)
)
}),
dailyRotateFileTransport
]
});
module.exports = {
logger: logger
};
\ No newline at end of file
'use strict';
//for DB manipulate
const RestaurantStorage = require("./RestaurantStorage");
const {pool} = require("../config/db");
const { logger } = require("../config/winston");
const jwt = require("jsonwebtoken");
exports.readRestaurants = async function (req,res) {
const {category} = req.query;
if (category) {
const validCategory = ["한식", "중식", "일식", "양식", "분식", "구이", "회/초밥", "기타",];
if (!validCategory.includes(category)) {
return res.send({
isSuccess: false,
code: 400,
message: "유효한 카테고리가 아닙니다.",
});
}
}
try {
const connection = await pool.getConnection(async (conn) => conn);
try {
//mysql접속 관련 부분 정의하는 함수
//es6 비구조할당
const [rows] = await RestaurantStorage.selectRestaurants(connection, category);
return res.send({
result: rows,
isSuccess: true,
code: 200, // 요청 성공시 200번대 코드를 뿌려주고, 실패시 400번대 코드
message: "식당 목록 요청 성공",
});
} catch (err) {
logger.error(`readRestaurants Query error\n: ${JSON.stringify(err)}`);
return false;
} finally {
connection.release();
}
} catch (err) {
logger.error(`readRestaurants DB Connection error\n: ${JSON.stringify(err)}`);
return false;
}
}
\ No newline at end of file
'use strict';
//for DB CRUD
// const db = require("../config/db");
const { pool } = require("../config/db");
exports.selectRestaurants = async function (connection, category) {
const selectAllRestaurantsQuery = `select title, address, category from restaurants where status='A';`;
const selectCategorizedRestaurantsQuery = `select title, address, category from restaurants where status='A' and category=?;`;
const Params = [category];
const Query = category ? selectCategorizedRestaurantsQuery : selectAllRestaurantsQuery;
const rows = await connection.query(Query, Params);
return rows;
}
\ No newline at end of file
'use strict';
//for DB manipulate
const UserStorage = require("./UserStorage");
const {pool} = require("../config/db");
const { logger } = require("../config/winston");
const jwt = require("jsonwebtoken");
class User {
constructor(body) {
this.body = body;
}
async login() {
const client = this.body;
try {
try {
const {id, password} = await UserStorage.getUserInfo(connection, client.id);
// console.log(id, password);
if (id) {
if (id === client.id && password === client.password) {
return { success: true};
}
return { success : false, msg: "비밀번호가 틀렸습니다."};
}
return {success: false, msg: "존재하지 않는 아이디입니다."};
} catch (err) {
return {success: false, msg: err};
} finally {
connection.release();
}
} catch (err) {
logger.error(`login DB Connection error\n: ${JSON.stringify(err)}`);
return false;
}
}
async register() {
const client = this.body;
try {
const connection = await pool.getConnection(async (conn) => conn);
// console.log(client);
try {
const response = await UserStorage.save(connection, client);
// console.log("테스트2 : ", response);
return response;
} catch (err) {
console.log(err);
return {success: false, msg : err};
} finally {
connection.release();
}
} catch (err) {
logger.error(`usersaving DB Connection error\n: ${JSON.stringify(err)}`);
return false;
}
}
}
module.exports = User;
'use strict';
const { pool } = require("../config/db");
//for DB CRUD
class UserStorage {
constructor(body) {
this.body = body;
// this.connection = await pool.getConnection(async (conn) => conn);
}
// static getUsers(isAll, ...fields) {}
static async getUserInfo(connection, id) {
return new Promise((resolve, reject) => {
const query = "SELECT * FROM users WHERE id = ?;";
connection.query(query, [id], (err, data) => {
if (err) reject(`${err}`);
// console.log(data[0]);
resolve(data[0]);
pool.releaseConnection(conn);
});
});
}
static async save (connection, userInfo) {
const query = "INSERT INTO users(id, name, password) VALUES(?, ?, ?);";
try {
const [rows] = await connection.query({
sql: query,
timeout: 30000,
values: [userInfo.id, userInfo.name, userInfo.password]
});
// console.log(fields);
if (rows.affectedRows) {
return {success: true};
} else {
return {success: false};
}
} catch (error) {
console.log(error);
}
}
}
// static getUserInfo(id) {
// return new Promise((resolve, reject) => {
// const query = "SELECT * FROM users WHERE id = ?;";
// pool.query(query, [id], (err, data) => {
// if (err) reject(`${err}`);
// // console.log(data[0]);
// resolve(data[0]);
// });
// });
// }
module.exports = UserStorage;
\ No newline at end of file
* {
margin: 0;
padding: 0;
}
html, body {
height : 100%;
}
.wrapper {
height : 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.user-container {
background: rebeccapurple;
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.5rem;
}
.user-container .nickname {
font-size : 14px;
margin-right : 1.5rem;
margin-left : 1rem;
color:#fff;
}
.user-container input {
border-radius: 3px;
border: none;
height: 80%;
}
.display-container {
background: #D2D2FF;
flex : 12;
overflow-y:scroll;
}
.input-container {
flex:1;
display:flex;
justify-content: stretch;
align-items: stretch;
}
.input-container span {
display: flex;
justify-content: flex-start;
align-items:center;
padding: 0.3rem;
width: 100%;
}
.chatting-input {
font-size:12px;
height:100%;
flex:8;
border:none;
}
.send-button {
flex:1;
background: rebeccapurple;
color:#fff;
border:none;
height:100%;
border-radius:3px;
}
.chatting-list li {
width:50%;
padding:0.3rem;
display:flex;
justify-content: flex-start;
align-items:flex-end;
margin-top:0.5rem;
}
.profile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
}
.profile .user {
font-size: 10px;
margin-bottom: 0.3rem;
}
.profile .image {
border-radius: 50%;
object-fit: cover;
width: 50px;
height: 50px;
}
.message {
border-radius: 5px;
padding: 0.5rem;
font-size: 12px;
margin: 0 5px;
flex: 10;
}
.time {
font-size: 10px;
margin: 0 5px;
}
.sent {
flex-direction: row-reverse;
float: right;
}
.sent .message {
background: #9986EE;
color: #fff;
}
.received .message {
background: #fff;
}
\ No newline at end of file
@font-face {
font-family: 'Noto Sans KR', sans-serif;
src: url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@700&display=swap');
font-weight: normal;
font-style: normal;
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
html {
font-size: 10px;
font-family: 'Noto Sans KR', sans-serif;
}
nav {
/* background-color: #e69a06; */
}
.nav-container {
padding: 1rem 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.nav-title {
font-size: 2.5rem;
color :rebeccapurple;
}
.nav-contact {
font-size: 1.5rem;
border: 0;
background: none;
cursor: pointer;
font-family: inherit;
color :lightslategray;
margin-right: 30px;
}
.category-title {
font-size: 2rem;
padding : 0 30%;
}
.category-list {
padding: 15px 1rem;
}
.category-item {
width: 24%;
height: 5rem;
background: none;
border: none;
font-family: inherit;
font-size: 1.6rem;
}
.category-item:hover {
color: #e69a06;
cursor: pointer;
}
.inner {
padding: 0 1.5rem;
}
@media all and (min-width: 1024px) {
.inner {
max-width: 1024px;
margin: 0 auto;
}
}
/* 카카오맵 CSS */
body {
height: 100vh;
}
nav {
height: 59px;
}
main {
padding-top: 1.5rem;
height: calc(100% - 59px);
display: flex;
flex-direction: column;
}
#map {
flex-grow: 1;
width: 100%;
height: 100%;
}
\ No newline at end of file
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 12% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form #button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: rebeccapurple;
width: 89%;
border: 0;
margin: 0 auto;
padding: 15px;
color: #FFFFFF;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form #button:hover,.form #button:active,.form #button:focus {
background: rebeccapurple;
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: rebeccapurple;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before, .container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #EF3B3A;
}
/* #id::placeholder #password::placeholder {
color: black;
font-style: italic;
font-weight: bold;
} */
body {
background: rebeccapurple; /* fallback for old browsers */
/* background: rebeccapurple; */
background: linear-gradient(90deg, rebeccapurple 0%, rebeccapurple 0%);
font-family: "Roboto", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Copyright (c) 2022 by Aigars Silkalns (https://codepen.io/colorlib/pen/rxddKy) */
\ No newline at end of file
/*인포윈도우 설정*/
.infowindow {
width : 25rem;
border : 1px solid black;
border-radius: 5px;
background-color : white;
}
.infowindow-title {
font-size: 3rem;
}
.infowindow-address {
font-size: 1.6rem;
}
.infowindow-btn {
font-size: 1.6rem;
}
\ No newline at end of file
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form #button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: rebeccapurple;
width: 89%;
border: 0;
margin: 0 auto;
padding: 15px;
color: #FFFFFF;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form #button:hover,.form #button:active,.form #button:focus {
background: rebeccapurple;
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: rebeccapurple;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before, .container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #EF3B3A;
}
/* #id::placeholder #password::placeholder {
color: black;
font-style: italic;
font-weight: bold;
} */
body {
background: rebeccapurple; /* fallback for old browsers */
/* background: rebeccapurple; */
background: linear-gradient(90deg, rebeccapurple 0%, rebeccapurple 0%);
font-family: "Roboto", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Copyright (c) 2022 by Aigars Silkalns (https://codepen.io/colorlib/pen/rxddKy) */
\ No newline at end of file
/*********************************************************************************
* 1. 지도 생성 및 확대 축소 컨트롤러
*/
var container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
var options = { //지도를 생성할 때 필요한 기본 옵션
center: new kakao.maps.LatLng(37.54, 126.96), //지도의 중심좌표.
level: 7 //지도의 레벨(확대, 축소 정도)
};
var map = new kakao.maps.Map(container, options); //지도 생성 및 객체 리턴
// 확대 축소 컨트롤러
var zoomControl = new kakao.maps.ZoomControl();
map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);
/******************************************************************************
* 2. 데이터 준비하기(제목, 주소, 카테고리)
*/
const dataSet = [
{
title: "희락돈까스",
address: "서울 영등포구 양산로 210",
category: "양식",
},
{
title: "즉석우동짜장",
address: "서울 영등포구 대방천로 260",
category: "한식",
},
{
title: "아카사카",
address: "서울 서초구 서초대로74길 23",
category: "일식",
}
];
// async function getDataSet(category) {
// let qs = category;
// if(!qs) {
// qs = "";
// }
// const dataSet = await axios({
// method: "get", // http method
// url: `http://localhost:3000/restaurants?category=${qs}`,
// headers: {},
// data: {},
// });
// return dataSet.data.result;
// }
// getDataSet();
/******************************************************************************
* 3. 여러개 마커찍기
*/
// 주소-좌표 변환 객체를 생성합니다
var geocoder = new kakao.maps.services.Geocoder();
function getCoordsByAddress(address) {
return new Promise((resolve, reject) => {
// 주소로 좌표를 검색합니다
geocoder.addressSearch(address, function (result, status) {
// 정상적으로 검색이 완료됐으면
if (status === kakao.maps.services.Status.OK) {
var coords = new kakao.maps.LatLng(result[0].y, result[0].x);
return resolve(coords);
}
reject(new Error("getCoordsByAddress Error: not valid Address"));
});
});
}
setMap(dataSet);
/*
*************************************************************
4. 마커에 인포윈도우 붙이기
*/
function getContent(data) {
// 인포윈도우 가공하기
return `
<div class="infowindow">
<div class="infowindow-body">
<h5 class="infowindow-title">${data.title}</h5>
<p class="infowindow-address">${data.address}</p>
<a href='/chat' class="infowindow-btn" target="_blank">채팅방이동</a>
</div>
</div>
`;
}
async function setMap(dataSet) {
for (var i = 0; i < dataSet.length; i++) {
// 마커를 생성합니다
let coords = await getCoordsByAddress(dataSet[i].address);
var marker = new kakao.maps.Marker({
map: map, // 마커를 표시할 지도
position: coords,
});
markerArray.push(marker);
// 마커에 표시할 인포윈도우를 생성합니다
var infowindow = new kakao.maps.InfoWindow({
content: getContent(dataSet[i]),// 인포윈도우에 표시할 내용
});
infowindowArray.push(infowindow);
// 마커에 mouseover 이벤트와 mouseout 이벤트를 등록합니다
// 이벤트 리스너로는 클로저를 만들어 등록합니다
// for문에서 클로저를 만들어 주지 않으면 마지막 마커에만 이벤트가 등록됩니다
kakao.maps.event.addListener(marker, 'click', makeOverListener(map, marker, infowindow, coords));
kakao.maps.event.addListener(map, 'click', makeOutListener(infowindow));
}
}
// 인포윈도우를 표시하는 클로저를 만드는 함수입니다
function makeOverListener(map, marker, infowindow, coords) {
return function() {
// 1. 클릭시 다른 인포윈도우 닫기
closeInfoWindow();
infowindow.open(map, marker);
// 2. 클릭한 곳으로 지도 중심 옮기기
map.panTo(coords);
};
}
let infowindowArray = [];
function closeInfoWindow() {
for (let infowindow of infowindowArray) {
infowindow.close();
}
}
// 인포윈도우를 닫는 클로저를 만드는 함수입니다
function makeOutListener(infowindow) {
return function() {
infowindow.close();
};
}
/*
**********************************************
5. 카테고리 분류
*/
// 카테고리
const categoryMap = {
korea: "한식",
china: "중식",
japan: "일식",
america: "양식",
wheat: "분식",
meat: "구이",
sushi: "회/초밥",
etc: "기타",
};
const categoryList = document.querySelector(".category-list");
categoryList.addEventListener("click", categoryHandler);
async function categoryHandler(event) {
const categoryId = event.target.id;
const category = categoryMap[categoryId];
try {
// 데이터 분류
let categorizedDataSet = await getDataSet(category);
// 기존 마커 삭제
closeMarker();
// 기존 인포윈도우 닫기
closeInfoWindow();
setMap(categorizedDataSet);
} catch (error) {
console.error(error);
}
}
let markerArray = [];
function closeMarker() {
for (marker of markerArray) {
marker.setMap(null);
}
}
setMap(dataSet);
// async function setting() {
// try {
// const dataSet = await getDataSet();
// setMap(dataSet);
// } catch (error) {
// console.error(error);
// }
// }
// setting();
\ No newline at end of file
"use strict"
const socket = io.connect("http://localhost:3000/", {transports:['websocket']});
const nickname = document.querySelector("#nickname")
const chatlist = document.querySelector(".chatting-list")
const chatInput = document.querySelector(".chatting-input")
const sendButton = document.querySelector(".send-button")
const displayContainer = document.querySelector(".display-container")
chatInput.addEventListener("keypress", (event)=> {
if(event.keyCode === 13) {
send()
}
})
function send() {
const param = {
name: nickname.value,
msg: chatInput.value
}
socket.emit("chatting", param)
}
sendButton.addEventListener("click", send)
socket.on("chatting", (data)=>{
console.log(data)
const {name, msg, time} = data;
const item = new LiModel(name, msg, time);
item.makeLi()
displayContainer.scrollTo(0, displayContainer.scrollHeight)
})
//console.log(socket);
function LiModel(name, msg, time) {
this.name = name;
this.msg = msg;
this.time = time;
this.makeLi = ()=>{
const li = document.createElement("li");
li.classList.add(nickname.value === this.name ? "sent":"received")
const dom = `<span class="profile">
<span class="user">${this.name}</span>
<img class="image" src="https://placeimg.com/50/50/any" alt="any">
</span>
<span class="message">${this.msg}</span>
<span class="time">${this.time}</span>`;
li.innerHTML = dom;
chatlist.appendChild(li)
}
}
\ No newline at end of file
'use strict';
const id = document.querySelector("#id"),
password = document.querySelector("#password"),
loginBtn = document.querySelector("#button");
loginBtn.addEventListener("click", login);
function login() {
const req = {
id : id.value,
password : password.value,
};
// console.log("login value : ", id.value);
fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(req),
})
.then((res) => res.json())
.then((res) => {
if (res.success) {
//성공하면 이동
location.href = "/";
} else {
alert(res.msg);
}
})
.catch((err) => {
console.error("로그인 중 에러 발생");
});
}
\ No newline at end of file
'use strict';
const id = document.querySelector("#id"),
name = document.querySelector("#name"),
password = document.querySelector("#password"),
confirmPassword = document.querySelector("#confirm-password"),
registerBtn = document.querySelector("#button");
registerBtn.addEventListener("click", register);
async function register() {
if(!id.value) {
return alert("아이디를 입력해주세요.")
}
if(!name.value) {
return alert("이름을 입력해주세요.")
}
if(!password.value) {
return alert("비밀번호를 입력해주세요.")
}
if(!confirmPassword.value) {
return alert("비밀번호를 확인해주세요.")
}
if (password.value !== confirmPassword.value) {
return alert("비밀번호가 일치하지 않습니다.")
}
console.log(1);
const req = {
id: id.value,
name: name.value,
password: password.value,
};
console.log("여기 안찍히나????");
fetch("/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(req),
})
.then((res) => res.json())
.then((res) => {
if (res.success) {
location.href = "/login";
} else {
if (res.err) return alert(res.err);
alert(res.msg);
}
})
.catch((err) => {
console.error("회원가입 중 에러 발생");
});
// try {
// const response = await fetch("/register", {
// method: "POST",
// headers: {
// "Content-Type": "application/json"
// },
// body: JSON.stringify(req),
// });
// console.log("테스트 : ", response);
// } catch(e) {
// console.log(e);
// }
}
\ No newline at end of file
"use strict";
const User = require("../../models/User");
// const Restaurant = require("../../models/Restaurant");
const output = {
hello: (req, res) => {
res.render("home/index");
},
login: (req, res) => {
res.render("home/login");
},
register: (req, res) => {
res.render("home/register");
},
// restaurants: (req, res) => {
// res.render("home/restaurants");
// }
};
const process = {
login: async (req, res) => {
const user = new User(req.body);
const response = await user.login();
return res.json(response);
},
register: async (req, res) => {
const user = new User(req.body);
const response = await user.register();
// console.log("req.body", req.body);
// console.log(res.json(response));
// console.log(res.json(response).statusCode); => 이거도 잘 찍혔음.
return res.json(response);
},
// restaurants: async (req, res) => {
// const restaurant = new Restaurant(req.body);
// const response = await restaurant.restaurants();
// return res.json(response);
// },
};
module.exports = {
output,
process,
};
\ No newline at end of file
"use strict";
const express = require("express");
const router = express.Router();
const jwtMiddleware = require("../../config/jwtMiddleware");
// const Restaurant = require("../../models/Restaurant");
const ctrl = require("./home.ctrl");
const index = require("../../models/Restaurant");
router.get("/", ctrl.output.hello);
router.get("/login", ctrl.output.login);
router.get("/register", ctrl.output.register);
router.get("/restaurants", index.readRestaurants);
// router.get("/restaurants", Restaurant.restaurants);
// router.get("/restaurants", ctrl.output.restaurants);
router.post("/login", ctrl.process.login);
router.post("/register", ctrl.process.register);
module.exports = router;
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="/css/home/chat.css" />
</head>
<body>
<div class="wrapper">
<div class="user-container">
<lable class="nickname" for="nickname">닉네임설정</lable>
<input type="text" id="nickname" />
</div>
<div class="display-container">
<ul class="chatting-list"></ul>
</div>
<div class="input-container">
<span>
<input type="text" class="chatting-input" />
<button class="send-button">전송</button>
</span>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/home/chat.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>맛집지도</title>
<meta name="author" content="양주미" />
<meta name="description" content="맛집지도 서비스" />
<meta name="keywords" content="맛집지도, 맛집추천, 실시간채팅" />
<link rel="stylesheet" href="/css/home/index.css" />
</head>
<body>
<nav>
<div class="inner">
<div class="nav-container">
<h1 class="nav-title">맛집지도</h1>
<button class="nav-contact">Chatting Rooms</button>
</div>
</div>
</nav>
<main>
<section id="category">
<div class="inner">
<div class="category-container">
<h2 class="category-title">💜맛집지도 카테고리를 선택해보세요💜</h2>
<div class="category-list">
<button class="category-item" id="korea">한식🍚</button>
<button class="category-item" id="china">중식🍜</button>
<button class="category-item" id="japan">일식🍙</button>
<button class="category-item" id="america">양식🍝</button>
<button class="category-item" id="wheat">분식🍭</button>
<button class="category-item" id="meat">구이🍖</button>
<button class="category-item" id="sushi">회/초밥🍣</button>
<button class="category-item" id="etc">기타🍴</button>
</div>
</div>
</div>
</section>
<!-- 카테고리 -->
<div id="map" class="inner"></div>
<!-- 카카오지도 -->
</main>
<script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=e55f753363b95e27b799aa6286a6c398&libraries=services"
></script>
<script src="/js/home/axios-index.js"></script>
<!-- <script
src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.0.0-alpha.1/axios.min.js"
integrity="sha512-xIPqqrfvUAc/Cspuj7Bq0UtHNo/5qkdyngx6Vwt+tmbvTLDszzXM0G6c91LXmGrRx8KEPulT+AfOOez+TeVylg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script> -->
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/css/home/login.css" />
<script src="/js/home/login.js" defer></script>
<title>로그인</title>
</head>
<body>
<div class="login-page">
<div class="form">
<!-- <form class="register-form">
<input type="text" placeholder="name" />
<input type="password" placeholder="password" />
<input type="text" placeholder="email address" />
<button>create</button>
<p class="message">Already registered? <a href="#">Sign In</a></p>
</form> -->
<form class="login-form">
<input id="id" type="text" placeholder="아이디" />
<input id="password" type="password" placeholder="비밀번호" />
<p id="button">LOGIN</p>
<p class="message">
계정이 없으신가요? <a href="/register">회원가입</a>
</p>
</form>
</div>
</div>
</body>
</html>
<!-- Copyright (c) 2022 by Aigars Silkalns (https://codepen.io/colorlib/pen/rxddKy) -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/css/home/register.css" />
<script src="/js/home/register.js" defer></script>
<title>회원가입</title>
</head>
<body>
<div class="login-page">
<div class="form">
<!-- <form class="register-form">
<input type="text" placeholder="name" />
<input type="password" placeholder="password" />
<input type="text" placeholder="email address" />
<button>create</button>
<p class="message">Already registered? <a href="#">Sign In</a></p>
</form> -->
<form class="login-form">
<input id="id" type="text" placeholder="아이디" />
<input id="name" type="text" placeholder="이름" />
<input id="password" type="password" placeholder="비밀번호" />
<input
id="confirm-password"
type="password"
placeholder="비밀번호 확인"
/>
<p id="button">SIGN UP</p>
<p class="message">계정이 있으신가요? <a href="/login">로그인</a></p>
</form>
</div>
</div>
</body>
</html>
<!-- Copyright (c) 2022 by Aigars Silkalns (https://codepen.io/colorlib/pen/rxddKy) -->
{
"lockfileVersion": 1
}