Showing
9 changed files
with
174 additions
and
19 deletions
1 | +# 📒 Mapmory | ||
2 | + | ||
3 | +#### Mapmory provides location-based recording sevices utilizing Google Maps API. | ||
4 | + | ||
5 | + | ||
6 | +### Mapmory functions | ||
7 | +----------------- | ||
8 | +- [x] Register | ||
9 | +- [x] Log in/out | ||
10 | +- [x] Member information edit | ||
11 | +- [x] Create your memories(With Title, Date, Author, Address) | ||
12 | +- [x] Delete your memories | ||
13 | +- [x] Save your memory with map | ||
14 | +- [x] Show your memory with map | ||
15 | +- [x] Edit your memory with map | ||
16 | +- [x] Search your memories | ||
17 | + | ||
18 | + | ||
19 | +### Stack | ||
20 | +----------------- | ||
21 | ++ Front end : EJS template engine | ||
22 | ++ Back end : Express/NodeJS | ||
23 | ++ Database : Mongo DB | ||
24 | ++ Server : AWS EC2 | ||
25 | + | ||
26 | + | ||
27 | +## ✏️ Quick Start (build, install, setup manual) | ||
28 | + | ||
29 | + $ git clone http://khuhub.khu.ac.kr/2017101294/Mapmory.git | ||
30 | + $ npm install | ||
31 | + $ node index.js | ||
32 | + | ||
33 | +If it does not work well | ||
34 | + | ||
35 | + $ npm install nodemon-g | ||
36 | + | ||
37 | +At package.json, add ``` "start" : "nodemon index.js" ``` | ||
38 | + | ||
39 | + "scripts": { | ||
40 | + "test": "echo \"Error: no test specified\" && exit 1", | ||
41 | + "start" : "nodemon index.js" | ||
42 | + } | ||
43 | + | ||
44 | + $ npm start | ||
45 | + | ||
46 | + | ||
47 | +### Dependency | ||
48 | +----------------- | ||
49 | ++ bcryptjs : 2.4.3, | ||
50 | ++ body-parser : 1.19.0, | ||
51 | ++ connect-flash : 0.1.1, | ||
52 | ++ ejs : 3.1.6, | ||
53 | ++ express : 4.17.1, | ||
54 | ++ express-session : 1.17.1, | ||
55 | ++ method-override : 3.0.0, | ||
56 | ++ mongoose : 5.12.8, | ||
57 | ++ passport : 0.4.1, | ||
58 | ++ passport-local : 1.0.0 | ||
59 | + | ||
60 | + | ||
61 | +### 👬 Team members | ||
62 | +----------------- | ||
63 | ++ Im Taemin (@devTaemin) | ||
64 | ++ Hong Jiyoon (@fheldgktpdy) | ||
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -16,7 +16,7 @@ mongoose.set('useCreateIndex', true); | ... | @@ -16,7 +16,7 @@ mongoose.set('useCreateIndex', true); |
16 | mongoose.set('useUnifiedTopology', true); | 16 | mongoose.set('useUnifiedTopology', true); |
17 | 17 | ||
18 | // Connect DB environment variable | 18 | // Connect DB environment variable |
19 | -mongoose.connect('mongodb+srv://Mapmory_admin:admin@cluster0.ncnjj.mongodb.net/Project-Mapmory?retryWrites=true&w=majority'); | 19 | +mongoose.connect('mongodbkey입력하는곳'); |
20 | 20 | ||
21 | // Store DB in the variable 'db' | 21 | // Store DB in the variable 'db' |
22 | var db = mongoose.connection; | 22 | var db = mongoose.connection; | ... | ... |
... | @@ -7,7 +7,7 @@ var postSchema = mongoose.Schema({ | ... | @@ -7,7 +7,7 @@ var postSchema = mongoose.Schema({ |
7 | address:{type:String, required:[true, 'address is required!']}, | 7 | address:{type:String, required:[true, 'address is required!']}, |
8 | body:{type:String, required:[true, 'Content is required!']}, | 8 | body:{type:String, required:[true, 'Content is required!']}, |
9 | author:{type:mongoose.Schema.Types.ObjectId, ref:'user', required:true}, | 9 | author:{type:mongoose.Schema.Types.ObjectId, ref:'user', required:true}, |
10 | - createdAt:{type:Date, default:Date.now()}, | 10 | + createdAt:{type:Date, default:Date.now}, |
11 | updatedAt:{type:Date} | 11 | updatedAt:{type:Date} |
12 | }); | 12 | }); |
13 | 13 | ... | ... |
... | @@ -6,13 +6,31 @@ var util = require('../util'); | ... | @@ -6,13 +6,31 @@ var util = require('../util'); |
6 | 6 | ||
7 | 7 | ||
8 | // Post home | 8 | // Post home |
9 | -router.get('/', function(req, res){ | 9 | +router.get('/', async function(req, res){ |
10 | - Post.find({}) | 10 | + // Paging 기능 추가 |
11 | - .populate('author') | 11 | + var page = Math.max(1, parseInt(req.query.page)); |
12 | - .sort('-createdAt') | 12 | + var limit = Math.max(1, parseInt(req.query.limit)); |
13 | - .exec(function(err, posts){ | 13 | + page = !isNaN(page)?page:1; |
14 | - if(err){return res.json(err)}; | 14 | + limit = !isNaN(limit)?limit:10; |
15 | - res.render('posts/index', {posts:posts}); | 15 | + |
16 | + var searchQuery = createSearchQuery(req.query); | ||
17 | + var skip = (page-1)*limit; | ||
18 | + var count = await Post.countDocuments(searchQuery); | ||
19 | + var maxPage = Math.ceil(count/limit); | ||
20 | + var posts = await Post.find(searchQuery) | ||
21 | + .populate('author') | ||
22 | + .sort('-createdAt') | ||
23 | + .skip(skip) | ||
24 | + .limit(limit) | ||
25 | + .exec(); | ||
26 | + | ||
27 | + res.render('posts/index', { | ||
28 | + posts:posts, | ||
29 | + currentPage:page, | ||
30 | + maxPage:maxPage, | ||
31 | + limit:limit, | ||
32 | + searchType:req.query.searchType, | ||
33 | + searchText:req.query.searchText | ||
16 | }); | 34 | }); |
17 | }); | 35 | }); |
18 | 36 | ||
... | @@ -97,5 +115,23 @@ function checkPermission(req, res, next){ | ... | @@ -97,5 +115,23 @@ function checkPermission(req, res, next){ |
97 | }); | 115 | }); |
98 | } | 116 | } |
99 | 117 | ||
118 | + | ||
119 | +// Search function | ||
120 | +function createSearchQuery(queries){ | ||
121 | + var searchQuery = {}; | ||
122 | + if(queries.searchType && queries.searchText && queries.searchText.length >= 3){ | ||
123 | + var searchTypes = queries.searchType.toLowerCase().split(','); | ||
124 | + var postQueries = []; | ||
125 | + if(searchTypes.indexOf('title')>=0){ | ||
126 | + postQueries.push({ title: { $regex: new RegExp(queries.searchText, 'i') } }); | ||
127 | + } | ||
128 | + if(searchTypes.indexOf('body')>=0){ | ||
129 | + postQueries.push({ body: { $regex: new RegExp(queries.searchText, 'i') } }); | ||
130 | + } | ||
131 | + if(postQueries.length > 0) searchQuery = {$or:postQueries}; | ||
132 | + } | ||
133 | + return searchQuery; | ||
134 | +} | ||
135 | + | ||
100 | // Export module | 136 | // Export module |
101 | module.exports = router; | 137 | module.exports = router; |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -33,6 +33,26 @@ util.noPermission = function(req, res){ | ... | @@ -33,6 +33,26 @@ util.noPermission = function(req, res){ |
33 | res.redirect('/login'); | 33 | res.redirect('/login'); |
34 | } | 34 | } |
35 | 35 | ||
36 | +// 검색 기능 추가 | ||
37 | +util.getPostQueryString = function(req, res, next){ | ||
38 | + res.locals.getPostQueryString = function(isAppended=false, overwrites={}){ | ||
39 | + var queryString = ''; | ||
40 | + var queryArray = []; | ||
41 | + var page = overwrites.page?overwrites.page:(req.query.page?req.query.page:''); | ||
42 | + var limit = overwrites.limit?overwrites.limit:(req.query.limit?req.query.limit:''); | ||
43 | + var searchType = overwrites.searchType?overwrites.searchType:(req.query.searchType?req.query.searchType:''); | ||
44 | + var searchText = overwrites.searchText?overwrites.searchText:(req.query.searchText?req.query.searchText:''); | ||
36 | 45 | ||
46 | + if(page) queryArray.push('page='+page); | ||
47 | + if(limit) queryArray.push('limit='+limit); | ||
48 | + if(searchType) queryArray.push('searchType='+searchType); | ||
49 | + if(searchText) queryArray.push('searchText='+searchText); | ||
50 | + | ||
51 | + if(queryArray.length>0) queryString = (isAppended?'&':'?') + queryArray.join('&'); | ||
52 | + | ||
53 | + return queryString; | ||
54 | + } | ||
55 | + next(); | ||
56 | +} | ||
37 | 57 | ||
38 | module.exports = util; | 58 | module.exports = util; |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -135,7 +135,7 @@ | ... | @@ -135,7 +135,7 @@ |
135 | 135 | ||
136 | <!-- Async script executes immediately and must be after any DOM elements used in callback. --> | 136 | <!-- Async script executes immediately and must be after any DOM elements used in callback. --> |
137 | <script | 137 | <script |
138 | - src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDK6K4iDdo9cKQdrNoOJaaYg29nEG0BIjw&callback=initMap&libraries=&v=weekly" | 138 | + src="https://maps.googleapis.com/maps/api/js?key=googlekey값을 입력하세요&callback=initMap&libraries=&v=weekly" |
139 | async | 139 | async |
140 | ></script> | 140 | ></script> |
141 | </body> | 141 | </body> | ... | ... |
... | @@ -142,7 +142,7 @@ | ... | @@ -142,7 +142,7 @@ |
142 | 142 | ||
143 | <!-- Async script executes immediately and must be after any DOM elements used in callback. --> | 143 | <!-- Async script executes immediately and must be after any DOM elements used in callback. --> |
144 | <script | 144 | <script |
145 | - src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDK6K4iDdo9cKQdrNoOJaaYg29nEG0BIjw&callback=initMap&libraries=&v=weekly" | 145 | + src="https://maps.googleapis.com/maps/api/js?key=googlekey값을 입력하세요&callback=initMap&libraries=&v=weekly" |
146 | async | 146 | async |
147 | ></script> | 147 | ></script> |
148 | 148 | ... | ... |
... | @@ -47,15 +47,49 @@ | ... | @@ -47,15 +47,49 @@ |
47 | </tr> | 47 | </tr> |
48 | <% }) %> | 48 | <% }) %> |
49 | </tbody> | 49 | </tbody> |
50 | - | ||
51 | </table> | 50 | </table> |
52 | 51 | ||
53 | - </div> | 52 | + <nav class="nav justify-content-center bg-light"> |
54 | - | 53 | + <% |
55 | - | 54 | + var offset = 2; |
56 | - | 55 | + var previousBtnEnabled = currentPage>1; |
56 | + var nextBtnEnabled = currentPage<maxPage; | ||
57 | + %> | ||
58 | + <ul class="pagination pagination-sm justify-content-center align-items-center h-100 mb-0"> | ||
59 | + <li class="page-item <%= previousBtnEnabled?'':'disabled' %>"> | ||
60 | + <a class="page-link" href="/posts?page=<%= currentPage-1 %>&limit=<%= limit %>"<%= previousBtnEnabled?'':'tabindex=-1' %>>«</a> | ||
61 | + </li> | ||
62 | + <% for(i=1;i<=maxPage;i++){ %> | ||
63 | + <% if(i==1 || i==maxPage || (i>=currentPage-offset && i<=currentPage+offset)){ %> | ||
64 | + <li class="page-item <%= currentPage==i?'active':'' %>"><a class="page-link" href="/posts?page=<%= i %>&limit=<%= limit %>"><%= i %></a></li> | ||
65 | + <% } else if(i==2 || i==maxPage-1){ %> | ||
66 | + <li><a class="page-link">...</a></li> | ||
67 | + <% } %> | ||
68 | + <% } %> | ||
69 | + <li class="page-item <%= nextBtnEnabled?'':'disabled' %>"> | ||
70 | + <a class="page-link" href="/posts?page=<%= currentPage+1 %>&limit=<%= limit %>"<%= nextBtnEnabled?'':'tabindex=-1' %>>»</a> | ||
71 | + </li> | ||
72 | + </ul> | ||
73 | + </nav> | ||
57 | 74 | ||
75 | + | ||
76 | + <form action="/posts" method="get" style="float:right"> | ||
77 | + <div class="form-row"> | ||
78 | + <div class="form-group"> | ||
79 | + <div class="input-group"> | ||
80 | + <select name="searchType" class="custom-select"> | ||
81 | + <option value="title" <%= searchType=='title'?'selected':'' %>>Title</option> | ||
82 | + </select> | ||
83 | + <input minLength="3" type="text" name="searchText" value="<%= searchText %>"> | ||
84 | + <div class="input-group-append"> | ||
85 | + <button class="btn btn-outline-primary" type="submit">Search</button> | ||
86 | + </div> | ||
87 | + </div> | ||
88 | + </div> | ||
89 | + </div> | ||
90 | + </form> | ||
58 | 91 | ||
92 | + </div> | ||
59 | </div> | 93 | </div> |
60 | </body> | 94 | </body> |
61 | </html> | 95 | </html> |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -13,12 +13,13 @@ | ... | @@ -13,12 +13,13 @@ |
13 | <ol class="breadcrumb p-1 pl-2 pr-2"> | 13 | <ol class="breadcrumb p-1 pl-2 pr-2"> |
14 | <li class="breadcrumb-item"><a href="/">Home</a></li> | 14 | <li class="breadcrumb-item"><a href="/">Home</a></li> |
15 | <li class="breadcrumb-item"><a href="/posts">Board</a></li> | 15 | <li class="breadcrumb-item"><a href="/posts">Board</a></li> |
16 | - <li class="breadcrumb-item active" aria-current="page"><%= post.title %></li> | 16 | + <li class="breadcrumb-item active" aria-current="page"><%= post.address %></li> |
17 | </ol> | 17 | </ol> |
18 | </nav> | 18 | </nav> |
19 | 19 | ||
20 | <div class="card"> | 20 | <div class="card"> |
21 | - <h5 class="card-header p-2" style="font-weight: bold; font-family: 'Archivo', sans-serif;"><%= post.title %></h5> | 21 | + <h5 class="card-header p-2" style="font-weight: bold; font-family: 'Archivo', sans-serif; background-color:goldenrod;"><%= post.title %></h5> |
22 | + <!-- <h5 class="card-header p-1" style="font-family: 'Archivo', sans-serif;"><%= post.address %></h5> --> | ||
22 | <div class="row"> <!-- 1 --> | 23 | <div class="row"> <!-- 1 --> |
23 | 24 | ||
24 | <div class="col-md-7 col-lg-8 col-xl-9 order-sm-2 order-md-1"> <!-- 1 --> | 25 | <div class="col-md-7 col-lg-8 col-xl-9 order-sm-2 order-md-1"> <!-- 1 --> |
... | @@ -45,7 +46,7 @@ | ... | @@ -45,7 +46,7 @@ |
45 | <% if(isAuthenticated && post.author && currentUser.id == post.author.id){ %> <!-- 1 --> | 46 | <% if(isAuthenticated && post.author && currentUser.id == post.author.id){ %> <!-- 1 --> |
46 | <a class="btn btn-outline-primary" href="/posts/<%= post._id %>/edit">Edit</a> | 47 | <a class="btn btn-outline-primary" href="/posts/<%= post._id %>/edit">Edit</a> |
47 | <form action="/posts/<%= post._id %>?_method=delete" method="post" class="d-inline"> | 48 | <form action="/posts/<%= post._id %>?_method=delete" method="post" class="d-inline"> |
48 | - <a class="btn btn-outline-primary" href="javascript:void(0)" onclick="confirm('Do you want to delete this?')?this.parentElement.submit():null;">Delete</a> | 49 | + <a class="btn btn-outline-primary" href="javascript:void(0)" onclick="confirm('기록을 삭제하시겠습니까?')?this.parentElement.submit():null;">Delete</a> |
49 | </form> | 50 | </form> |
50 | <% } %> | 51 | <% } %> |
51 | </div> | 52 | </div> | ... | ... |
-
Please register or login to post a comment