Skip to content

Latest commit

Β 

History

History
703 lines (549 loc) Β· 19.8 KB

File metadata and controls

703 lines (549 loc) Β· 19.8 KB

λ°±μ—”λ“œ 개발 κ°€μ΄λ“œ

Altsis λ°±μ—”λ“œλŠ” Express.js 기반의 REST API μ„œλ²„μž…λ‹ˆλ‹€. 이 λ¬Έμ„œμ—μ„œλŠ” λ°±μ—”λ“œ κ°œλ°œμ— ν•„μš”ν•œ 핡심 κ°œλ…κ³Ό νŒ¨ν„΄μ„ μ„€λͺ…ν•©λ‹ˆλ‹€.


λͺ©μ°¨

  1. 기술 μŠ€νƒ
  2. μ•„ν‚€ν…μ²˜ 계측 ꡬ쑰
  3. 라우트 μž‘μ„±
  4. 컨트둀러 μž‘μ„±
  5. λͺ¨λΈ μž‘μ„±
  6. μ„œλΉ„μŠ€ 계측
  7. 인증 미듀웨어
  8. μ—λŸ¬ 처리
  9. 파일 μ—…λ‘œλ“œ (S3)
  10. WebSocket
  11. λ‘œκΉ…
  12. API 버그 ν•΄κ²° ν”„λ‘œμ„ΈμŠ€

1. 기술 μŠ€νƒ

기술 버전 μš©λ„
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 문법을 μ‚¬μš©ν•˜μ‹­μ‹œμ˜€.


2. μ•„ν‚€ν…μ²˜ 계측 ꡬ쑰

λ°±μ—”λ“œλŠ” λ‹€μŒκ³Ό 같은 계측 ꡬ쑰λ₯Ό λ”°λ¦…λ‹ˆλ‹€:

ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­
    ↓
[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 })

3. 라우트 μž‘μ„±

기본 ꡬ쑰

라우트 νŒŒμΌμ€ 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 };

URL νŒ¨ν„΄

λͺ¨λ“  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} κ²½λ‘œμ— λ“±λ‘ν•©λ‹ˆλ‹€.


4. 컨트둀러 μž‘μ„±

CRUD λͺ…λͺ… κ·œμΉ™

컨트둀러 ν•¨μˆ˜λŠ” λ‹€μŒ λͺ…λͺ… κ·œμΉ™μ„ λ”°λ¦…λ‹ˆλ‹€:

ν•¨μˆ˜λͺ… νŒ¨ν„΄ 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 λ¬Έμ„œν™”

λͺ¨λ“  컨트둀러 ν•¨μˆ˜λŠ” 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 });
  }
};

5. λͺ¨λΈ μž‘μ„±

기본 ꡬ쑰

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 νƒ€μž… μ •μ˜

λͺ¨λΈ μŠ€ν‚€λ§ˆμ—λŠ” 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 - ν™œμ„±ν™” μƒνƒœ
 */

6. μ„œλΉ„μŠ€ 계측

λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 λ³΅μž‘ν•˜κ±°λ‚˜ μ—¬λŸ¬ μ»¨νŠΈλ‘€λŸ¬μ—μ„œ κ³΅μœ λ˜λŠ” 경우 μ„œλΉ„μŠ€ κ³„μΈ΅μœΌλ‘œ λΆ„λ¦¬ν•©λ‹ˆλ‹€.

ν˜„μž¬ μ„œλΉ„μŠ€ λͺ©λ‘

μ„œλΉ„μŠ€ μ—­ν• 
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 () => {
    // μ •κΈ° μž‘μ—… (예: 만료된 μ„Έμ…˜ 정리, μ•Œλ¦Ό λ°œμ†‘ λ“±)
  });
};

7. 인증 미듀웨어

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 κ°±μ‹ ).


8. μ—λŸ¬ 처리

λ©”μ‹œμ§€ μƒμˆ˜

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";

HTTP μƒνƒœ μ½”λ“œ κ·œμΉ™

μƒνƒœ μ½”λ“œ μš©λ„ λ©”μ‹œμ§€ μ˜ˆμ‹œ
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 });
  }
};

9. 파일 μ—…λ‘œλ“œ (S3)

Multer + S3 νŒ¨ν„΄

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 μ €μž₯ 경둜λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.


10. WebSocket

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));
  });
};

μ£Όμš” μ‹€μ‹œκ°„ κΈ°λŠ₯:

  • μ±„νŒ… λ©”μ‹œμ§€ μ†‘μˆ˜μ‹ 
  • μ•Œλ¦Ό μ‹€μ‹œκ°„ 전달

11. λ‘œκΉ…

Winston 기반 λ‘œκΉ… μ‹œμŠ€ν…œμ„ μ‚¬μš©ν•©λ‹ˆλ‹€:

파일 μš©λ„
log/logger.js ν™˜κ²½λ³„ 둜거 νŒ©ν† λ¦¬
log/devLogger.js 개발 ν™˜κ²½: μ½˜μ†” 좜λ ₯
log/prodLogger.js ν”„λ‘œλ•μ…˜ ν™˜κ²½: 파일 + S3 λ‘œκΉ…

HTTP μš”μ²­μ€ morgan으둜 λ‘œκΉ…λ˜λ©°, μ»€μŠ€ν…€ 포맷으둜 아카데미 ID, μ‚¬μš©μž ID 등을 ν•¨κ»˜ κΈ°λ‘ν•©λ‹ˆλ‹€.


12. API 버그 ν•΄κ²° ν”„λ‘œμ„ΈμŠ€

API κ΄€λ ¨ 버그λ₯Ό 효율적으둜 ν•΄κ²°ν•˜κΈ° μœ„ν•œ 단계별 ν”„λ‘œμ„ΈμŠ€μž…λ‹ˆλ‹€.

1단계: ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ μ‚¬μš©ν•˜λŠ” API 이름 확인

ν”„λ‘ νŠΈμ—”λ“œ μ½”λ“œμ—μ„œ ν˜ΈμΆœν•˜λŠ” API ν•¨μˆ˜λͺ…을 ν™•μΈν•©λ‹ˆλ‹€:

// ν”„λ‘ νŠΈμ—”λ“œ μ½”λ“œμ—μ„œ
const { SeasonAPI } = useAPIv2();
const result = await SeasonAPI.CSeason({ data: {...} });
//                             ^^^^^^^ 이 ν•¨μˆ˜λͺ… 확인

2단계: useAPIv2μ—μ„œ ν•΄λ‹Ή API μ°ΎκΈ°

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 };
}

3단계: λ°±μ—”λ“œμ—μ„œ ν•΄λ‹Ή μ—”λ“œν¬μΈνŠΈ 검색

λ°±μ—”λ“œ routes/ λ””λ ‰ν† λ¦¬μ—μ„œ ν•΄λ‹Ή μ—”λ“œν¬μΈνŠΈλ₯Ό μ°ΎμŠ΅λ‹ˆλ‹€:

# 라우트 νŒŒμΌμ—μ„œ 검색
grep -r "seasons" backend/src/routes/
// routes/seasons.js
router.post("/", isAdManager, seasons.create);  // β†’ controllers/seasons.js의 create

4단계: μ—λŸ¬ λ©”μ‹œμ§€λ‘œ 원인 νŒŒμ•…

μ»¨νŠΈλ‘€λŸ¬μ—μ„œ λ°˜ν™˜ν•˜λŠ” μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό μΆ”μ ν•©λ‹ˆλ‹€:

// controllers/seasons.js
return res.status(404).send({ message: __NOT_FOUND("school") });
// β†’ ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ "SCHOOL_NOT_FOUND" λ©”μ‹œμ§€ μˆ˜μ‹ 

5단계: μˆ˜μ • ν›„ 검증

  1. μ—λŸ¬ 원인이 λ˜λŠ” μ½”λ“œλ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
  2. yarn dev둜 μ„œλ²„λ₯Ό μž¬μ‹œμž‘ν•©λ‹ˆλ‹€.
  3. ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ λ™μΌν•œ λ™μž‘μ„ μˆ˜ν–‰ν•˜μ—¬ μˆ˜μ •μ„ κ²€μ¦ν•©λ‹ˆλ‹€.
  4. κ΄€λ ¨ ν…ŒμŠ€νŠΈκ°€ μžˆλ‹€λ©΄ yarn test둜 ν™•μΈν•©λ‹ˆλ‹€.

디버깅 팁

상황 확인 방법
API 응닡 확인 λΈŒλΌμš°μ € 개발자 도ꡬ Network νƒ­
μš”μ²­ λ³Έλ¬Έ 확인 morgan 둜그 (req.body 포함)
DB 쿼리 확인 MongoDB Compassμ—μ„œ 데이터 확인
μ„Έμ…˜ 확인 Redis CLI둜 μ„Έμ…˜ 데이터 확인
κΆŒν•œ 문제 req.user.auth κ°’κ³Ό 미듀웨어 쑰건 확인

λͺ©μ°¨λ‘œ λŒμ•„κ°€κΈ°