Altsis λ°±μλλ Express.js κΈ°λ°μ REST API μλ²μ λλ€. μ΄ λ¬Έμμμλ λ°±μλ κ°λ°μ νμν ν΅μ¬ κ°λ κ³Ό ν¨ν΄μ μ€λͺ ν©λλ€.
- κΈ°μ μ€ν
- μν€ν μ² κ³μΈ΅ ꡬ쑰
- λΌμ°νΈ μμ±
- 컨νΈλ‘€λ¬ μμ±
- λͺ¨λΈ μμ±
- μλΉμ€ κ³μΈ΅
- μΈμ¦ λ―Έλ€μ¨μ΄
- μλ¬ μ²λ¦¬
- νμΌ μ λ‘λ (S3)
- WebSocket
- λ‘κΉ
- API λ²κ·Έ ν΄κ²° νλ‘μΈμ€
| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| Express.js | 4.21.x | REST API μλ² |
| Mongoose | 6.x | MongoDB ODM |
| Passport | 0.6.x | μΈμ¦ (Local + Google OAuth) |
| Redis | 4.x | μΈμ μ μ₯μ |
| Socket.IO | 4.x | WebSocket μ€μκ° ν΅μ |
| AWS SDK | 2.x | S3 νμΌ μ€ν λ¦¬μ§ |
| Winston | 3.x | λ‘κΉ |
| Multer | 1.4.x | νμΌ μ λ‘λ λ―Έλ€μ¨μ΄ |
| bcrypt | 5.x | λΉλ°λ²νΈ ν΄μ± |
| node-cron | 4.x | μ€μΌμ€λ¬ (cron) |
| mongoose-encryption | 2.x | νλ μνΈν |
AI κΈ°λ₯(OpenAI / Anthropic / Google Gemini)μ λ³λ SDK μμ΄ Node.js λ΄μ₯ fetchλ‘ κ° μ 곡μμ REST APIλ₯Ό μ§μ νΈμΆν©λλ€ (src/services/aiProvider.js).
κ΄λ ¨ λͺ¨λ:
src/services/aiPromptPolicy.jsβ ν둬ννΈ νλ, μμ λ³ νλ‘ν(syllabusReview/chat), JSON νμ±src/services/aiSkills.jsβ Alter Skill λΌμ°ν° (chat,syllabus-review)src/services/aiSafety.jsβ κ°μΈμ 보 ν¨ν΄ λ§μ€νΉsrc/services/aiUsage.jsβAIUsageLogκΈ°λ‘ ν¬νΌ (provider,feature,success,errorCode)- κ°μκ³νμ μμ±μ JSON κ²μ¦ μ€ν¨ μ 1ν μ¬μλνλ©°, λΉ μλ΅/
rawμ±κ³΅ μ²λ¦¬λ νμ§ μμ΅λλ€.
μ°Έκ³ : λ°±μλλ ES Module (
"type": "module")μ μ¬μ©ν©λλ€.import/exportλ¬Έλ²μ μ¬μ©νμμμ€.
λ°±μλλ λ€μκ³Ό κ°μ κ³μΈ΅ ꡬ쑰λ₯Ό λ°λ¦ λλ€:
ν΄λΌμ΄μΈνΈ μμ²
β
[Route] λΌμ°νΈ μ μ + λ―Έλ€μ¨μ΄ 체μ΄λ
β
[Middleware] μΈμ¦/κΆν κ²μ¬ (isLoggedIn, isAdmin λ±)
β
[Controller] μμ² μ²λ¦¬, μλ΅ λ°ν
β
[Service] λΉμ¦λμ€ λ‘μ§ (μ νμ )
β
[Model] MongoDB λ°μ΄ν° μ κ·Ό
νκΈ° μμ± API (POST /api/seasons)μ μ 체 νλ¦:
1. ν΄λΌμ΄μΈνΈ: POST /api/seasons { school, year, term, period }
2. Route: router.post("/", isAdManager, seasons.create)
3. Middleware: isAdManager β admin λλ manager κΆν νμΈ
4. Controller: seasons.create β μμ² κ²μ¦, School μ‘°ν, Season μμ±
5. Service: SeasonService β μΆκ° λΉμ¦λμ€ λ‘μ§
6. Model: Season(dbName).create({...}) β MongoDB μ μ₯
7. μλ΅: res.status(200).send({ season })
λΌμ°νΈ νμΌμ backend/src/routes/μ μμΉνλ©°, 리μμ€λ³λ‘ λΆλ¦¬λ©λλ€.
// routes/seasons.js
import express from "express";
const router = express.Router();
import * as seasons from "../controllers/seasons.js";
import { isAdManager, isLoggedIn } from "../middleware/auth.js";
// CREATE
router.post("/", isAdManager, seasons.create);
// READ (λ¨κ±΄/λͺ©λ‘)
router.get("/:_id?", isLoggedIn, seasons.find);
// UPDATE
router.put("/:_id/activate", isAdManager, seasons.activate);
router.put("/:_id/inactivate", isAdManager, seasons.inactivate);
router.put("/:_id/period", isAdManager, seasons.updatePeriod);
router.put("/:_id/classrooms", isAdManager, seasons.updateClassrooms);
// DELETE
router.delete("/:_id", isAdManager, seasons.remove);
export { router };λͺ¨λ APIλ /api/{resource} ν¨ν΄μ λ°λ¦
λλ€:
| ν¨ν΄ | HTTP λ©μλ | μ€λͺ |
|---|---|---|
/api/seasons |
POST | νκΈ° μμ± |
/api/seasons |
GET | νκΈ° λͺ©λ‘ μ‘°ν |
/api/seasons/:_id |
GET | νκΈ° λ¨κ±΄ μ‘°ν |
/api/seasons/:_id/period |
PUT | νκΈ° κΈ°κ° μμ |
/api/seasons/:_id |
DELETE | νκΈ° μμ |
νμ 리μμ€λ νΉμ λμμ URL κ²½λ‘λ‘ ννν©λλ€:
PUT /api/seasons/:_id/activate # νκΈ° νμ±ν
PUT /api/seasons/:_id/permission/:type # κΆν μμ
POST /api/seasons/:_id/permission/:type/exceptions # κΆν μμΈ μΆκ°
λͺ¨λ λΌμ°ν°λ routes/index.jsμμ ν΅ν©λ©λλ€:
// routes/index.js
export const routers = [
{ label: "academies", routes: academies },
{ label: "seasons", routes: seasons },
{ label: "syllabuses", routes: syllabuses },
{ label: "enrollments", routes: enrollments },
// ...
];app.jsμμ λΌμ°ν°λ₯Ό /api/{label} κ²½λ‘μ λ±λ‘ν©λλ€.
컨νΈλ‘€λ¬ ν¨μλ λ€μ λͺ λͺ κ·μΉμ λ°λ¦ λλ€:
| ν¨μλͺ ν¨ν΄ | HTTP λ©μλ | μ€λͺ | μμ |
|---|---|---|---|
CResource |
POST | μμ± | CSeason, CSyllabus |
RResources |
GET | λͺ©λ‘ μ‘°ν | RSeasons, RSyllabuses |
RResource |
GET | λ¨κ±΄ μ‘°ν | RSeason, RSyllabus |
UResource |
PUT | μμ | USeason, USyllabus |
DResource |
DELETE | μμ | DSeason, DSyllabus |
μ°Έκ³ : λΌμ°νΈ νμΌμμλ
create,find,update,removeκ°μ μ΄λ¦μ μ¬μ©νλ κ²½μ°λ μμ΅λλ€. μ΄λ λ κ±°μ ν¨ν΄μ΄λ―λ‘, μλ‘μ΄ μ»¨νΈλ‘€λ¬ μμ± μμλ μμ λͺ λͺ κ·μΉμ λ°λ₯΄μμμ€.
λͺ¨λ 컨νΈλ‘€λ¬ ν¨μλ JSDocμΌλ‘ λ¬Έμνν΄μΌ ν©λλ€:
/**
* @memberof APIs.SeasonAPI
* @function CSeason API
* @description νκΈ° μμ± API
* @version 2.0.0
*
* @param {Object} req
*
* @param {"POST"} req.method
* @param {"/seasons"} req.url
*
* @param {Object} req.user - "admin"|"manager"
*
* @param {Object} req.body
* @param {string} req.body.school - ObjectId of school(sid)
* @param {string} req.body.year
* @param {string} req.body.term
* @param {Object} req.body.period
* @param {string} req.body.period.start - "YYYY-MM-DD"
* @param {string} req.body.period.end - "YYYY-MM-DD"
*
* @param {Object} res
* @param {Object} res.season - created season
*
* @throws {}
* | status | message | description |
* | :----- | :--------------- | :-------------------------------- |
* | 404 | SCHOOL_NOT_FOUND | if school is not found |
* | 409 | FIELD_IN_USE | if year+term already exists |
*/
export const create = async (req, res) => {
try {
// ꡬν...
return res.status(200).send({ season });
} catch (err) {
return res.status(500).send({ message: err.message });
}
};// controllers/seasons.js
import { Season, School } from "../models/index.js";
import { FIELD_REQUIRED, FIELD_INVALID, __NOT_FOUND } from "../messages/index.js";
import { validate } from "../utils/validate.js";
export const create = async (req, res) => {
try {
// 1. μμ² κ²μ¦
if (!req.body.school) {
return res.status(400).send({ message: FIELD_REQUIRED("school") });
}
// 2. κ΄λ ¨ λ°μ΄ν° μ‘°ν (λ©ν° DB ν¨ν΄)
const school = await School(req.user.academyId).findById(req.body.school);
if (!school) {
return res.status(404).send({ message: __NOT_FOUND("school") });
}
// 3. λ°μ΄ν° μμ±
const season = await Season(req.user.academyId).create({
school: school._id,
schoolId: school.schoolId,
schoolName: school.schoolName,
year: req.body.year,
term: req.body.term,
period: req.body.period,
});
// 4. μλ΅ λ°ν
return res.status(200).send({ season });
} catch (err) {
if (err.code === 11000) {
// MongoDB μ€λ³΅ ν€ μλ¬
return res.status(409).send({ message: "SEASON_IN_USE" });
}
return res.status(500).send({ message: err.message });
}
};Mongoose μ€ν€λ§λ₯Ό μ μνκ³ , λ©ν° λ°μ΄ν°λ² μ΄μ€ ν¨ν΄μΌλ‘ λ΄λ³΄λ λλ€:
// models/Season.js
import mongoose from "mongoose";
import { conn } from "../_database/mongodb/index.js";
// μλΈ μ€ν€λ§ μ μ
const periodSchema = mongoose.Schema(
{
start: String, // "YYYY-MM-DD"
end: String,
},
{ _id: false } // μλΈ μ€ν€λ§μμ _id λΉνμ±ν
);
// λ©μΈ μ€ν€λ§ μ μ
const seasonSchema = mongoose.Schema(
{
school: {
type: mongoose.Types.ObjectId,
required: true,
},
schoolId: {
type: String,
required: true,
},
year: {
type: String,
required: true,
},
term: {
type: String,
required: true,
},
period: {
type: periodSchema,
default: { start: "", end: "" },
},
isActivated: {
type: Boolean,
default: false,
},
},
{ timestamps: true } // createdAt, updatedAt μλ μΆκ°
);
// μΈλ±μ€ μ€μ
seasonSchema.index(
{ school: 1, year: -1, term: 1 },
{ unique: true } // λ³΅ν© μ λν¬ μΈλ±μ€
);
// μΈμ€ν΄μ€ λ©μλ
seasonSchema.methods.getSubdocument = function () {
return {
season: this._id,
school: this.school,
year: this.year,
term: this.term,
};
};
// λ©ν° λ°μ΄ν°λ² μ΄μ€ ν¨ν΄: μμΉ΄λ°λ―Έλ³ DBμμ λͺ¨λΈ μμ±
export const Season = (dbName) => {
return conn[dbName].model("Season", seasonSchema);
};Altsisλ μμΉ΄λ°λ―Έλ³λ‘ λ
립λ MongoDB λ°μ΄ν°λ² μ΄μ€λ₯Ό μ¬μ©ν©λλ€. λͺ¨λ λͺ¨λΈμ ν©ν 리 ν¨μλ‘ λ΄λ³΄λ΄λ©°, dbName (= academyId)μ λ§€κ°λ³μλ‘ λ°μ΅λλ€:
// λͺ¨λΈ λ΄λ³΄λ΄κΈ° ν¨ν΄
export const Season = (dbName) => {
return conn[dbName].model("Season", seasonSchema);
};
// 컨νΈλ‘€λ¬μμ μ¬μ©
const seasons = await Season(req.user.academyId).find({ school: schoolId });connμ _database/mongodb/index.jsμμ κ΄λ¦¬λλ μ°κ²° κ°μ²΄λ‘, μμΉ΄λ°λ―Έλ³ λ°μ΄ν°λ² μ΄μ€ μ°κ²°μ μΊμ±ν©λλ€.
μ±λ₯μ μν΄ μ μ ν μΈλ±μ€λ₯Ό μ€μ ν©λλ€:
// λ¨μΌ μΈλ±μ€
seasonSchema.index({ school: 1 });
// λ³΅ν© μ λν¬ μΈλ±μ€
seasonSchema.index(
{ school: 1, year: -1, term: 1 },
{ unique: true }
);λ―Όκ°ν λ°μ΄ν°λ mongoose-encryptionμ μ¬μ©νμ¬ μνΈνν©λλ€:
import encrypt from "mongoose-encryption";
userSchema.plugin(encrypt, {
encryptionKey: process.env.ENCRYPTION_KEY,
signingKey: process.env.SIGNING_KEY,
encryptedFields: ["sensitiveField"],
});λͺ¨λΈ μ€ν€λ§μλ JSDocμΌλ‘ νμ μ λ¬Έμνν©λλ€:
/**
* @memberof Models.Season
* @typedef TSeason
*
* @prop {ObjectId} _id
* @prop {ObjectId} school - school._id
* @prop {string} schoolId - school.schoolId
* @prop {string} year - νλ
λ
* @prop {string} term - νκΈ°
* @prop {TPeriod} period - κΈ°κ°
* @prop {boolean} isActivated - νμ±ν μν
*/λΉμ¦λμ€ λ‘μ§μ΄ 볡μ‘νκ±°λ μ¬λ¬ 컨νΈλ‘€λ¬μμ 곡μ λλ κ²½μ° μλΉμ€ κ³μΈ΅μΌλ‘ λΆλ¦¬ν©λλ€.
| μλΉμ€ | μν |
|---|---|
scheduler.js |
cron κΈ°λ° μ€μΌμ€λ¬ (μ κΈ° μμ ) |
notifications.js |
μλ¦Ό λ°μ‘ λ‘μ§ |
registrations.js |
νκΈ° λ±λ‘ κΆν/μ²λ¦¬ |
seasons.js |
νκΈ° λΉμ¦λμ€ λ‘μ§ (κΆν μμΈ μ²λ¦¬ λ±) |
boards.js |
κ²μν μλΉμ€ |
users.js |
μ¬μ©μ κ΄λ ¨ μλΉμ€ |
themeSettings.js |
ν λ§ μ€μ CRUD |
// services/seasons.js
import { Season, Registration } from "../models/index.js";
export const SeasonService = {
/**
* νκΈ° νμ±ν μ μΆκ° μ²λ¦¬
*/
async onActivate(dbName, seasonId) {
const season = await Season(dbName).findById(seasonId);
// λΉμ¦λμ€ λ‘μ§...
return season;
},
};
// κ°λ³ ν¨μ λ΄λ³΄λ΄κΈ°λ κ°λ₯
export const addSeasonPermissionException = async (dbName, seasonId, data) => {
// ...
};node-cronμ μ¬μ©νμ¬ μ κΈ° μμ
μ μ€νν©λλ€:
// services/scheduler.js
import cron from "node-cron";
export const initializeScheduler = () => {
// λ§€μΌ μμ μ μ€ν
cron.schedule("0 0 * * *", async () => {
// μ κΈ° μμ
(μ: λ§λ£λ μΈμ
μ 리, μλ¦Ό λ°μ‘ λ±)
});
};middleware/auth.jsμ μ μλ μΈμ¦ λ―Έλ€μ¨μ΄μ
λλ€:
| λ―Έλ€μ¨μ΄ | μ€λͺ | μ¬μ© μμ |
|---|---|---|
isLoggedIn |
λ‘κ·ΈμΈ μ¬λΆ νμΈ | μΌλ° API |
isNotLoggedIn |
λΉλ‘κ·ΈμΈ μν νμΈ | λ‘κ·ΈμΈ/νμκ°μ API |
forceNotLoggedIn |
λ‘κ·ΈμΈ μνλ©΄ κ°μ λ‘κ·Έμμ | νΉμ μν© |
isOwner |
owner κΆν νμΈ | μμ€ν κ΄λ¦¬ API |
isAdmin |
admin κΆν νμΈ | μμΉ΄λ°λ―Έ κ΄λ¦¬ API |
isAdManager |
admin λλ manager κΆν νμΈ | νκ΅/νκΈ° κ΄λ¦¬ API |
isOwAdManager |
owner, admin, manager μ€ νλ | κ³ κΈ κ΄λ¦¬ API |
isOwAdmin |
owner λλ admin | μμΉ΄λ°λ―Έ + μμ€ν κ΄λ¦¬ |
ownerToAdmin |
ownerκ° νΉμ μμΉ΄λ°λ―Έμ μ κ·Ό | μμΉ΄λ°λ―Έ μμ κ΄λ¦¬ |
// admin λλ managerλ§ μ κ·Ό κ°λ₯
router.post("/", isAdManager, seasons.create);
// λ‘κ·ΈμΈν λͺ¨λ μ¬μ©μ μ κ·Ό κ°λ₯
router.get("/:_id?", isLoggedIn, seasons.find);
// adminλ§ μ κ·Ό κ°λ₯
router.delete("/:_id", isAdmin, seasons.remove);μμ² β Express μΈμ
β Passport deserializeUser β req.user μ€μ
β λ―Έλ€μ¨μ΄ (isLoggedIn λ±) β 컨νΈλ‘€λ¬
μΈμ μ Redisμ μ μ₯λ©λλ€ (TTL: 24μκ°, rolling κ°±μ ).
messages/index.jsμ μ μλ μμλ₯Ό μ¬μ©νμ¬ μΌκ΄λ μλ¬ λ©μμ§λ₯Ό λ°νν©λλ€:
import {
FIELD_REQUIRED, // (field) => `${FIELD}_REQUIRED`
FIELD_INVALID, // (field) => `${FIELD}_INVALID`
FIELD_IN_USE, // (field) => `${FIELD}_IN_USE`
__NOT_FOUND, // (field) => `${FIELD}_NOT_FOUND`
PERMISSION_DENIED, // "PERMISSION_DENIED"
CONNECTED_ALREADY, // (field) => `${FIELD}_CONNECTED_ALREADY`
DISCONNECTED_ALREADY, // (field) => `${FIELD}_DISCONNECTED_ALREADY`
} from "../messages/index.js";| μν μ½λ | μ©λ | λ©μμ§ μμ |
|---|---|---|
| 200 | μ±κ³΅ | { season: {...} } |
| 400 | μλͺ»λ μμ² (νλ λλ½/무ν¨) | FIELD_REQUIRED("school") |
| 401 | μΈμ¦ μ€ν¨ | PASSWORD_INCORRECT |
| 403 | κΆν μμ | PERMISSION_DENIED |
| 404 | 리μμ€ μμ | __NOT_FOUND("season") |
| 409 | μΆ©λ (μ€λ³΅, μ ν μ΄κ³Ό) | FIELD_IN_USE("season"), STUDENTS_FULL |
| 500 | μλ² μλ¬ | err.message |
export const create = async (req, res) => {
try {
// 1. νμ νλ κ²μ¦ β 400
if (!req.body.school) {
return res.status(400).send({ message: FIELD_REQUIRED("school") });
}
// 2. 리μμ€ μ‘΄μ¬ νμΈ β 404
const school = await School(req.user.academyId).findById(req.body.school);
if (!school) {
return res.status(404).send({ message: __NOT_FOUND("school") });
}
// 3. λΉμ¦λμ€ κ·μΉ κ²μ¦ β 409
if (someCondition) {
return res.status(409).send({ message: SYLLABUS_CONFIRMED_ALREADY });
}
// 4. λ°μ΄ν° μ²λ¦¬
const result = await Model(req.user.academyId).create({...});
// 5. μ±κ³΅ μλ΅
return res.status(200).send({ result });
} catch (err) {
// 6. MongoDB μ€λ³΅ ν€ μλ¬ β 409
if (err.code === 11000) {
return res.status(409).send({ message: FIELD_IN_USE("resource") });
}
// 7. κΈ°ν μλ¬ β 500
return res.status(500).send({ message: err.message });
}
};AWS S3μ νμΌμ μ
λ‘λνκΈ° μν΄ multer + multer-s3λ₯Ό μ¬μ©ν©λλ€:
backend/src/_s3/
βββ fileBucket.js # νμΌ λ²ν· μ€μ (S3 ν΄λΌμ΄μΈνΈ, μλͺ
URL)
βββ profileBucket.js # νλ‘ν μ΄λ―Έμ§ λ²ν·
βββ archiveMulter.js # κΈ°λ‘λ¬Ό μ
λ‘λ μ€μ
βββ chatMulter.js # μ±ν
νμΌ μ
λ‘λ μ€μ
βββ courseMulter.js # μμ
νμΌ μ
λ‘λ μ€μ
βββ profileMulter.js # νλ‘ν μ΄λ―Έμ§ μ
λ‘λ μ€μ
βββ aiRefMulter.js # AI μ°Έκ³ μλ£ μ
λ‘λ μ€μ
κ° Multer μ€μ μ νμ© νμΌ ν¬κΈ°, MIME νμ , S3 μ μ₯ κ²½λ‘λ₯Ό μ μν©λλ€.
Socket.IOλ₯Ό μ¬μ©νμ¬ μ€μκ° ν΅μ μ μ§μν©λλ€:
// utils/webSocket.js
import { Server } from "socket.io";
export const initializeWebSocket = (server) => {
const io = new Server(server, {
cors: { origin: process.env.URL },
});
io.on("connection", (socket) => {
// μ°κ²° μ²λ¦¬
socket.on("join", (room) => socket.join(room));
socket.on("message", (data) => io.to(data.room).emit("message", data));
});
};μ£Όμ μ€μκ° κΈ°λ₯:
- μ±ν λ©μμ§ μ‘μμ
- μλ¦Ό μ€μκ° μ λ¬
Winston κΈ°λ° λ‘κΉ μμ€ν μ μ¬μ©ν©λλ€:
| νμΌ | μ©λ |
|---|---|
log/logger.js |
νκ²½λ³ λ‘κ±° ν©ν 리 |
log/devLogger.js |
κ°λ° νκ²½: μ½μ μΆλ ₯ |
log/prodLogger.js |
νλ‘λμ νκ²½: νμΌ + S3 λ‘κΉ |
HTTP μμ²μ morganμΌλ‘ λ‘κΉ
λλ©°, 컀μ€ν
ν¬λ§·μΌλ‘ μμΉ΄λ°λ―Έ ID, μ¬μ©μ ID λ±μ ν¨κ» κΈ°λ‘ν©λλ€.
API κ΄λ ¨ λ²κ·Έλ₯Ό ν¨μ¨μ μΌλ‘ ν΄κ²°νκΈ° μν λ¨κ³λ³ νλ‘μΈμ€μ λλ€.
νλ‘ νΈμλ μ½λμμ νΈμΆνλ API ν¨μλͺ μ νμΈν©λλ€:
// νλ‘ νΈμλ μ½λμμ
const { SeasonAPI } = useAPIv2();
const result = await SeasonAPI.CSeason({ data: {...} });
// ^^^^^^^ μ΄ ν¨μλͺ
νμΈfrontend/src/hooks/useAPIv2.tsμμ ν΄λΉ ν¨μλ₯Ό μ°Ύμ νΈμΆνλ API μλν¬μΈνΈλ₯Ό νμΈν©λλ€:
// useAPIv2.ts λ΄λΆ
async function CSeason(props) {
const { season } = await database.C({
location: `seasons`, // β POST /api/seasons
data: props.data,
});
return { season };
}λ°±μλ routes/ λλ ν 리μμ ν΄λΉ μλν¬μΈνΈλ₯Ό μ°Ύμ΅λλ€:
# λΌμ°νΈ νμΌμμ κ²μ
grep -r "seasons" backend/src/routes/// routes/seasons.js
router.post("/", isAdManager, seasons.create); // β controllers/seasons.jsμ create컨νΈλ‘€λ¬μμ λ°ννλ μλ¬ λ©μμ§λ₯Ό μΆμ ν©λλ€:
// controllers/seasons.js
return res.status(404).send({ message: __NOT_FOUND("school") });
// β νλ‘ νΈμλμμ "SCHOOL_NOT_FOUND" λ©μμ§ μμ - μλ¬ μμΈμ΄ λλ μ½λλ₯Ό μμ ν©λλ€.
yarn devλ‘ μλ²λ₯Ό μ¬μμν©λλ€.- νλ‘ νΈμλμμ λμΌν λμμ μννμ¬ μμ μ κ²μ¦ν©λλ€.
- κ΄λ ¨ ν
μ€νΈκ° μλ€λ©΄
yarn testλ‘ νμΈν©λλ€.
| μν© | νμΈ λ°©λ² |
|---|---|
| API μλ΅ νμΈ | λΈλΌμ°μ κ°λ°μ λꡬ Network ν |
| μμ² λ³Έλ¬Έ νμΈ | morgan λ‘κ·Έ (req.body ν¬ν¨) |
| DB 쿼리 νμΈ | MongoDB Compassμμ λ°μ΄ν° νμΈ |
| μΈμ νμΈ | Redis CLIλ‘ μΈμ λ°μ΄ν° νμΈ |
| κΆν λ¬Έμ | req.user.auth κ°κ³Ό λ―Έλ€μ¨μ΄ 쑰건 νμΈ |