인센티브 기반의 커뮤니티를 구현했습니다. 프로젝트명은 Reward-Psychology 입니다. 해당 게시물은 백엔드를 중심으로 작성되었으며, mongoose ^6.3.0을 기준으로 설명되어 있습니다.
React.js
node.js, express, mongoDB
solidity
인센티브 기반의 Web2.0 커뮤니티를 구현해보겠습니다. 팀에서 백엔드 구현을 맡았으므로, 백엔드를 기준으로 필요한 기능들은 다음에 제시된 3가지입니다.
-> /join
-> /login
-> /search
-> /upload
-> /logout
-> /myposts
-> /:postingId/watch
-> /:postingId/edit
-> /:postingId/delete
express를 사용하여 서버를 구현했습니다.
import "./db.js";
import "./models/Posting.js";
import "./models/User.js";
import express from "express";
import morgan from "morgan";
import cors from "cors";
import rootRouter from "./routers/rootRouter.js";
import userRouter from "./routers/userRouter.js";
import postingRouter from "./routers/postingRouter.js";
import dotenv from "dotenv";
dotenv.config();
const app = express();
const logger = morgan("dev");
const PORT = 4000;
const corsOptions = { origin: "http://localhost:3000" };
app.use(cors(corsOptions));
const handleListening = () => {
console.log(`✅ Server listening on port http://localhost:${PORT}`);
};
app.use(logger);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use("/", rootRouter);
app.use("/users", userRouter);
app.use("/posts", postingRouter);
app.listen(PORT, handleListening);
export default app;
const app = express();
app.use(cors(corsOptions));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const PORT = 4000;
const handleListening = () => {console.log(✅ Server listening on port http://localhost:${PORT}
)};
app.listen(PORT, handleListening);
import mongoose from "mongoose";
import dotenv from "dotenv";
dotenv.config();
mongoose.connect(process.env.DB_URL);
const db = mongoose.connection;
const handleOpen = () => {
console.log("✅ Connected to DB");
};
const handleError = (error) => {
console.log("❌ DB Error", error);
};
db.on("error", handleError);
db.once("open", handleOpen);
mongoDB를 사용하기 위해서, mongoose를 install 해줍니다. mongoose docs의 guide를 따라 설치 가능합니다. 공식 문서에서 다음과 같은 내용을 확인할 수 있습니다.
With Mongoose, everything is derived from a Schema.
Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.
Schema를 통해 shape of the documents를 정의합니다.
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
userName: { type: String, required: true, unique: true },
password: { type: String, required: true },
address: { type: String, required: true, unique: true },
profileImage: {
type: String,
required: true,
default: "../images/rpProfileImage.png",
},
});
required 옵션은 필수 property를 의미합니다.
required : email, userName, password, address, profileImage
unique 옵션은 유일해야 하는 property를 의미합니다.
unique : email, userName, address
userSchema.pre("save", async function () {
this.password = await bcrypt.hash(this.password, 5);
});
const postingSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true, maxlength: 50 },
createdAt: { type: Date, default: Date.now },
contents: { type: String, required: true, trim: true, minlength: 20 },
hashtags: [{ type: String, trim: true }],
meta: {
voting: { type: Number, default: 0 },
views: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
},
owner: { type: String, required: true, ref: "User" },
});
required : title, contents, owner
trim 옵션은 자동으로 앞뒤 공백을 없애줍니다.
trim : title, contents, hashtags
minlength/maxlength 옵션은 최소, 최대 길이를 정해줍니다.
postingSchema.static("formatHashtags", function (hashtags) {
console.log(this.hashtags);
return hashtags
.split(",")
.map((hashtag) => (hashtag.startsWith("#") ? hashtag : `#${hashtag}`));
});
Schema를 이용해서 Model을 정의합니다. 공식 문서를 통해서 다음 내용을 확인할 수 있습니다.
A Model is a class that's your primary tool for interacting with MongoDB. An instance of a Model is called a Document.
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
userName: { type: String, required: true, unique: true },
password: { type: String, required: true },
address: { type: String, required: true, unique: true },
profileImage: {
type: String,
required: true,
default: "../images/rpProfileImage.png",
},
});
userSchema.pre("save", async function () {
this.password = await bcrypt.hash(this.password, 5);
});
const User = mongoose.model("User", userSchema);
const postingSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true, maxlength: 50 },
createdAt: { type: Date, default: Date.now },
contents: { type: String, required: true, trim: true, minlength: 20 },
hashtags: [{ type: String, trim: true }],
meta: {
voting: { type: Number, default: 0 },
views: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
},
owner: { type: String, required: true, ref: "User" },
});
postingSchema.static("formatHashtags", function (hashtags) {
return hashtags
.split(",")
.map((hashtag) => (hashtag.startsWith("#") ? hashtag : `#${hashtag}`));
});
const Posting = mongoose.model("Posting", postingSchema);
import User from "../models/User.js";
import bcrypt from "bcrypt";
import lightwallet from "eth-lightwallet";
export const postJoin = async (req, res) => {
const { email, userName, password, password2 } = req.body;
const mnemonic = lightwallet.keystore.generateRandomSeed();
const userNameOrEmailExists = await User.find({
$or: [{ email }, { userName }],
});
if (password !== password2) {
return res
.status(400)
.send({ errorMessage: "Password Confirmation does not match" });
}
if (userNameOrEmailExists.length !== 0) {
return res
.status(400)
.send({ errorMessage: "This user is already registered" });
}
try {
lightwallet.keystore.createVault(
{
password: password,
seedPhrase: mnemonic,
hdPathString: "m/0'/0'/0'",
},
function (err, ks) {
if (err) {
console.log("❌ Keystore Error:", err);
return res.status(400).send({
errorMessage: `❌ KeyStore Error: ${err}`,
});
}
ks.keyFromPassword(password, function (err, pwDerivedKey) {
if (err) {
console.log("❌ Key from password Error:", err);
return res.status(400).send({
errorMessage: `❌ Key from password Error: ${err}`,
});
}
ks.generateNewAddress(pwDerivedKey, 1);
let address = ks.getAddresses().toString();
let privateKey = ks.exportPrivateKey(address, pwDerivedKey);
// user를 users DB에 저장합니다.
User.create({
email,
userName,
password,
address,
});
return res.send({
successMessage: "Join Success, Go to Login Page",
address,
privateKey,
mnemonic,
});
});
}
);
} catch (error) {
console.log("❌ Join Not Available!");
return res.status(400).send({ errorMessage: "Join Not Available" });
}
};
export const postLogin = async (req, res) => {
const { userName, password } = req.body;
const user = await User.findOne({ userName });
if (!user) {
console.log("❌ An account with this username dose not exist");
return res.status(404).send({
errorMessage: "An account with this username dose not exist",
});
}
const passwordComparision = await bcrypt.compare(password, user.password);
if (!passwordComparision) {
console.log("❌ Password Comparision does not match");
return res
.status(404)
.send({ errorMessage: "Password Comparision does not match" });
}
return res.send({ address: user.address });
};
export const home = async (req, res) => {
try {
const postings = await Posting.find({}).sort({ createdAt: "desc" });
return res.send({ postings });
} catch (error) {
console.log("❌ Get Home Error:", error);
return res.status(400).send({
errorMessage: "This is not the web page you are looking for!",
});
}
};
export const search = async (req, res) => {
const { keyword } = req.query;
let postings = [];
if (keyword) {
postings = await Posting.find({
title: { $regex: new RegExp(keyword, "i") },
}).sort({ createdAt: "desc" });
}
return res.send(postings);
};
export const postUpload = (req, res) => {
const { title, contents, hashtags, userName, address } = req.body;
try {
if (!title || !contents || !userName || !address) {
return res.status(400).send({ errorMessage: "❌ Insufficient data!" });
}
Posting.create(
{
title,
contents,
hashtags: Posting.formatHashtags(hashtags),
owner: userName,
},
() => mint(address, 1)
);
return res.send("Upload Success, Go to Home Page");
} catch (error) {
console.log("❌ Post Upload Error:", error);
return res.status(400).send({ errorMessage: "Fail Post Upload" });
}
};
export const watch = async (req, res) => {
const { postingId } = req.params;
const posting = await Posting.findById(postingId);
if (!posting) {
return res.status(404).send({ errorMessage: "404 Not Found!" });
}
return res.send(posting);
};
export const getEdit = async (req, res) => {
const { postingId } = req.params;
const posting = await Posting.findById(postingId);
if (!posting) {
return res.status(404).send({ errorMessage: "404 Not Found!" });
}
return res.send({ posting });
};
export const postEdit = async (req, res) => {
const { postingId } = req.params;
const posting = await Posting.exists({ _id: postingId });
const { title, contents, hashtags } = req.body;
if (!posting) {
return res.status(404).send({ errorMessage: "404 Not Found!" });
}
await Posting.findByIdAndUpdate(postingId, {
title,
contents,
hashtags: Posting.formatHashtags(hashtags),
});
return res.send("Upload Success");
};
export const deletePosting = async (req, res) => {
const { postingId } = req.params;
await Posting.findByIdAndDelete(postingId);
return res.send("Success Delete the Post");
};