ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,17 +1,38 @@
 import { AccessTokenGuard } from '#guards/access-token.guard.js';
 import { IReviewController } from '#reviews/interfaces/review.controller.interface.js';
-import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
+import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
 import { ApiOperation } from '@nestjs/swagger';
 import { ReviewService } from './review.service.js';
 import { ReviewInputDTO } from './review.types.js';
+import { GetQueries } from '#types/queries.type.js';
+import { SortOrder } from '#types/options.type.js';
 
 @Controller('reviews')
 export class ReviewController implements IReviewController {
   constructor(private readonly reviewService: ReviewService) {}
 
+  @Get('my')
+  @UseGuards(AccessTokenGuard)
+  @ApiOperation({ summary: '내가 작성한 리뷰 조회' })
+  async getMyReviews(@Query() query: GetQueries) {
+    const { page = 1, pageSize = 10, orderBy = SortOrder.Recent, keyword = '' } = query;
+    const options = { page, pageSize, orderBy, keyword };
+
+    const { totalCount, list } = await this.reviewService.getMyReviews(options);
+
+    return { totalCount, list };
+  }
+
   @Get(':driverId')
   @ApiOperation({ summary: '기사 리뷰 조회' })
-  async getReviews() {}
+  async getDriverReviews(@Param('driverId') driverId: string, @Query() query: GetQueries) {
+    const { page = 1, pageSize = 10, orderBy = SortOrder.Recent, keyword = '' } = query;
+    const options = { page, pageSize, orderBy, keyword };
+
+    const { totalCount, list } = await this.reviewService.getDriverReviews(driverId, options);
+
+    return { totalCount, list };
+  }
 
   @Post(':driverId')
   @UseGuards(AccessTokenGuard)
@@ -22,11 +43,21 @@ export class ReviewController implements IReviewController {
     return review;
   }
 
-  @Post(':reviewId')
+  @Post('patch/:reviewId')
+  @UseGuards(AccessTokenGuard)
   @ApiOperation({ summary: '리뷰 수정' })
-  async patchReview() {}
+  async patchReview(@Param('reviewId') reviewId: string, @Body() body: Partial<ReviewInputDTO>) {
+    const review = await this.reviewService.patchReview(reviewId, body);
+
+    return review;
+  }
 
   @Delete(':reviewId')
+  @UseGuards(AccessTokenGuard)
   @ApiOperation({ summary: '리뷰 삭제' })
-  async deleteReview() {}
+  async deleteReview(@Param('reviewId') reviewId: string) {
+    const review = await this.reviewService.deleteReview(reviewId);
+
+    return review;
+  }
 }

리뷰: 기사 리뷰 조회는 기사 상세 조회에서 하게 될 것으로 보이는데
기사 상세 조회는 비회원도 열람 가능한 페이지입니다!
@@ -1,7 +1,7 @@
 import { PrismaService } from '#global/prisma.service.js';
 import { IReviewRepository } from '#reviews/interfaces/review.repository.interface.js';
 import { ReviewInputDTO } from '#reviews/review.types.js';
-import { FindOptions } from '#types/options.type.js';
+import { FindOptions, SortOrder } from '#types/options.type.js';
 import { Injectable } from '@nestjs/common';
 
 @Injectable()
@@ -11,7 +11,67 @@ export class ReviewRepository implements IReviewRepository {
     this.review = prisma.review;
   }
 
-  async findMany(options: FindOptions) {}
+  async totalCount(id: string, type: 'user' | 'driver') {
+    const whereCondition = type === 'user' ? { ownerId: id } : type === 'driver' ? { driverId: id } : {};
+    const totalCount = await this.review.count({ where: whereCondition });
+
+    return totalCount;
+  }
+
+  async findManyMyReviews(userId: string, options: FindOptions) {
+    console.log('findManyMyReviews called with:', userId, options);
+    const { page, pageSize, orderBy } = options;
+
+    const sort = orderBy === SortOrder.Recent ? { createdAt: 'desc' } : { createdAt: 'asc' };
+
+    const list = await this.review.findMany({
+      where: { ownerId: userId },
+      orderBy: sort,
+      skip: (page - 1) * pageSize,
+      take: pageSize,
+      include: {
+        driver: { select: { name: true, image: true } },
+        owner: {
+          select: {
+            moveInfos: {
+              where: { progress: 'COMPLETE' },
+              select: {
+                type: true,
+                date: true,
+                confirmedEstimation: {
+                  select: {
+                    price: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    });
+
+    return list;
+  }
+
+  async findManyDriverReviews(driverId: string, options: FindOptions) {
+    const { page, pageSize, orderBy } = options;
+
+    const sort = orderBy === SortOrder.Recent ? { createdAt: 'desc' } : { createdAt: 'asc' };
+
+    const list = await this.review.findMany({
+      where: { driverId },
+      orderBy: sort,
+      skip: (page - 1) * pageSize,
+      take: pageSize,
+      include: {
+        owner: {
+          select: { name: true },
+        },
+      },
+    });
+
+    return list;
+  }
 
   async findById(id: string) {}
 
@@ -21,7 +81,15 @@ export class ReviewRepository implements IReviewRepository {
     return review;
   }
 
-  async update(id: string, data: Partial<ReviewInputDTO>) {}
+  async update(id: string, data: Partial<ReviewInputDTO>) {
+    const review = await this.review.update({ where: { id }, data });
 
-  async delete(id: string) {}
+    return review;
+  }
+
+  async delete(id: string) {
+    const review = await this.review.delete({ where: { id } });
+
+    return review;
+  }
 }

리뷰: repository의 한 메소드는 하나의 결과만을 내보내는 것이 좋습니다.
count 메소드는 findMany와 따로 작성하고, service에서 결과를 합쳐서 내보내는 것이 더 바람직할 것 같아요!
@@ -1,7 +1,7 @@
 import { PrismaService } from '#global/prisma.service.js';
 import { IReviewRepository } from '#reviews/interfaces/review.repository.interface.js';
 import { ReviewInputDTO } from '#reviews/review.types.js';
-import { FindOptions } from '#types/options.type.js';
+import { FindOptions, SortOrder } from '#types/options.type.js';
 import { Injectable } from '@nestjs/common';
 
 @Injectable()
@@ -11,7 +11,67 @@ export class ReviewRepository implements IReviewRepository {
     this.review = prisma.review;
   }
 
-  async findMany(options: FindOptions) {}
+  async totalCount(id: string, type: 'user' | 'driver') {
+    const whereCondition = type === 'user' ? { ownerId: id } : type === 'driver' ? { driverId: id } : {};
+    const totalCount = await this.review.count({ where: whereCondition });
+
+    return totalCount;
+  }
+
+  async findManyMyReviews(userId: string, options: FindOptions) {
+    console.log('findManyMyReviews called with:', userId, options);
+    const { page, pageSize, orderBy } = options;
+
+    const sort = orderBy === SortOrder.Recent ? { createdAt: 'desc' } : { createdAt: 'asc' };
+
+    const list = await this.review.findMany({
+      where: { ownerId: userId },
+      orderBy: sort,
+      skip: (page - 1) * pageSize,
+      take: pageSize,
+      include: {
+        driver: { select: { name: true, image: true } },
+        owner: {
+          select: {
+            moveInfos: {
+              where: { progress: 'COMPLETE' },
+              select: {
+                type: true,
+                date: true,
+                confirmedEstimation: {
+                  select: {
+                    price: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    });
+
+    return list;
+  }
+
+  async findManyDriverReviews(driverId: string, options: FindOptions) {
+    const { page, pageSize, orderBy } = options;
+
+    const sort = orderBy === SortOrder.Recent ? { createdAt: 'desc' } : { createdAt: 'asc' };
+
+    const list = await this.review.findMany({
+      where: { driverId },
+      orderBy: sort,
+      skip: (page - 1) * pageSize,
+      take: pageSize,
+      include: {
+        owner: {
+          select: { name: true },
+        },
+      },
+    });
+
+    return list;
+  }
 
   async findById(id: string) {}
 
@@ -21,7 +81,15 @@ export class ReviewRepository implements IReviewRepository {
     return review;
   }
 
-  async update(id: string, data: Partial<ReviewInputDTO>) {}
+  async update(id: string, data: Partial<ReviewInputDTO>) {
+    const review = await this.review.update({ where: { id }, data });
 
-  async delete(id: string) {}
+    return review;
+  }
+
+  async delete(id: string) {
+    const review = await this.review.delete({ where: { id } });
+
+    return review;
+  }
 }

리뷰: 위와 마찬가지
@@ -0,0 +1,23 @@
+public class Calculator {
+
+    public int add(int fir, int sec) {
+        return fir + sec;
+    }
+
+    public int subtract(int fir, int sec) {
+        return fir - sec;
+    }
+
+    public int multiply(int fir, int sec) {
+        return fir * sec;
+    }
+
+    public int divide(int fir, int sec) {
+        if (sec == 0) {
+            throw new ArithmeticException("Divide by zero");
+        }
+        return fir / sec;
+    }
+
+
+}

리뷰: 예외처리 👍
@@ -0,0 +1,40 @@
+public class StringCalculator {
+
+    public int add(String string) {
+        if (string.isEmpty()) {
+            return 0;
+        }
+
+
+        String[] numbers = string.split(",|:");
+        return sum(numbers);
+    }
+
+    private int sum(String[] numbers) {
+        int total = 0;
+
+        for (String number : numbers) {
+            int num = checkNumber(number);
+            total += num;
+        }
+
+        return total;
+    }
+
+    private int checkNumber(String number) {
+        if (number.isEmpty()) {
+            return 0;
+        }
+
+        // 숫자가 아닌 경우 RuntimeException 발생
+        try {
+            int num = Integer.parseInt(number);
+            if (num < 0) {
+                throw new RuntimeException("음수가 포함되어 있습니다.");
+            }
+            return num;
+        } catch (NumberFormatException e) {
+            throw new RuntimeException("잘못된 입력입니다.");
+        }
+    }
+}
\ No newline at end of file

리뷰: 변수명을 string으로 하기보다는 좀 더 나은 변수명으로 짓는 것이 가독성 측면에서 좋아보여요!
@@ -0,0 +1,40 @@
+public class StringCalculator {
+
+    public int add(String string) {
+        if (string.isEmpty()) {
+            return 0;
+        }
+
+
+        String[] numbers = string.split(",|:");
+        return sum(numbers);
+    }
+
+    private int sum(String[] numbers) {
+        int total = 0;
+
+        for (String number : numbers) {
+            int num = checkNumber(number);
+            total += num;
+        }
+
+        return total;
+    }
+
+    private int checkNumber(String number) {
+        if (number.isEmpty()) {
+            return 0;
+        }
+
+        // 숫자가 아닌 경우 RuntimeException 발생
+        try {
+            int num = Integer.parseInt(number);
+            if (num < 0) {
+                throw new RuntimeException("음수가 포함되어 있습니다.");
+            }
+            return num;
+        } catch (NumberFormatException e) {
+            throw new RuntimeException("잘못된 입력입니다.");
+        }
+    }
+}
\ No newline at end of file

리뷰: 4번째 줄의 코드와 중복되는 부분으로 보이네요.
따로 모듈화해서 함수로 만들어주면 가독성도 좋아지고 재사용성도 높아져요.
@@ -0,0 +1,15 @@
+public class CalculatorTest {
+
+    public static void main(String[] args) {
+
+        Calculator calc = new Calculator();
+
+        System.out.println(calc.add(10, 20));
+        System.out.println(calc.add(10, -5));
+        System.out.println(calc.subtract(10, 20));
+        System.out.println(calc.multiply(10, 20));
+        System.out.println(calc.divide(10, 5));
+
+
+    }
+}

리뷰: 테스트코드로 작성된 부분이 아닌 것 같아요.
@@ -0,0 +1,46 @@
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class StringCalculatorTest {
+
+    private final StringCalculator calculator = new StringCalculator();
+
+    @Test
+    public void testEmptyString() {
+        assertEquals(0, calculator.add(""));
+    }
+
+
+    @Test
+    public void testNumbers() {
+        assertEquals(3, calculator.add("1,2"));
+        assertEquals(7, calculator.add("3:4"));
+        assertEquals(6, calculator.add("1,2,3"));
+        assertEquals(10, calculator.add("4:2:4"));
+        assertEquals(15, calculator.add("1,2:3,4,5"));
+    }
+
+    @Test
+    public void testNegativeNumber() {
+        RuntimeException exception = assertThrows(RuntimeException.class, () -> {
+            calculator.add("1,-2");
+        });
+        assertEquals("음수가 포함되어 있습니다.", exception.getMessage());
+    }
+
+    @Test
+    public void testInvalidString() {
+        RuntimeException exception = assertThrows(RuntimeException.class, () -> {
+            calculator.add("1,a");
+        });
+        assertEquals("잘못된 입력입니다.", exception.getMessage());
+    }
+
+    @Test
+    public void testMixedDelimiters() {
+        assertEquals(10, calculator.add("1,2:3,4"));
+        assertEquals(15, calculator.add("1,2:3,4,5"));
+    }
+}
+
+

리뷰: 하나의 테스트는 하나에 대해서만 테스트하면 더 좋을 것 같아요.
@@ -0,0 +1,65 @@
+import History from "@models/History";
+import {
+  getHistoryItemTemplate,
+  NO_HISTORY_TEMPLATE,
+} from "@templates/history";
+
+export default class HistoryController {
+  constructor(historyContainer) {
+    this.historyContainer = historyContainer;
+    this.numberOfHistory = 0;
+    this._init();
+  }
+  _init() {
+    this._initEvent();
+    this._initHistory();
+  }
+  _initEvent() {
+    this.historyContainer.addEventListener("click", (event) => {
+      if (event.target.classList.contains("button__delete")) {
+        const toDeleteEl = event.target.parentElement;
+        this.deleteHistory(toDeleteEl);
+      }
+    });
+  }
+  _initHistory() {
+    const history = History.getAll();
+    this.numberOfHistory = history.length;
+
+    if (this.numberOfHistory === 0) {
+      this.historyContainer.innerHTML = NO_HISTORY_TEMPLATE;
+      return;
+    }
+
+    const historyTemplates = history
+      .map(({ value, id }) => getHistoryItemTemplate(value, id))
+      .join("");
+    this.historyContainer.innerHTML = historyTemplates;
+  }
+  addHistory(searchValue) {
+    const historyId = History.add(searchValue);
+    if (!historyId) {
+      return;
+    }
+
+    const listItemEl = getHistoryItemTemplate(searchValue, historyId);
+
+    this.numberOfHistory++;
+    if (this.numberOfHistory === 1) {
+      this.historyContainer.innerHTML = listItemEl;
+      return;
+    }
+
+    this.historyContainer.insertAdjacentHTML("beforeend", listItemEl);
+  }
+  deleteHistory(historyEl) {
+    const id = historyEl.id;
+    this.historyContainer.removeChild(historyEl);
+    History.delete(id);
+    this.numberOfHistory--;
+
+    if (this.numberOfHistory === 0) {
+      this.historyContainer.innerHTML = NO_HISTORY_TEMPLATE;
+    }
+  }
+}

리뷰: 👍 👍  리뷰드린 부분 반영해주셨네요~! 확실히 템플릿을 상수로 분리하니까 가독성이 좋아졌네요 :)
@@ -19,41 +19,56 @@ export default class Repo {
   }) {
     this.id = id;
     this.name = name;
-    this.full_name = full_name;
+    this.fullName = full_name;
     this.owner = owner;
-    this.html_url = html_url;
+    this.htmlUrl = html_url;
     this.description = description;
-    this.stargazers_count = stargazers_count;
-    this.watchers_count = watchers_count;
-    this.forks_count = forks_count;
+    this.stargazersCount = stargazers_count;
+    this.watchersCount = watchers_count;
+    this.forksCount = forks_count;
     this.forks = forks;
-    this.created_at = created_at;
-    this.updated_at = updated_at;
-    this.clone_url = clone_url;
+    this.createdAt = created_at;
+    this.updatedAt = updated_at;
+    this.cloneUrl = clone_url;
     this.language = language;
     this.watchers = watchers;
     this.visibility = visibility;
   }
   render() {
     return `
-      <div class="col-12 col-sm-6  mb-4" id="repo-${this.id}">
+      <div class="col-12 col-sm-6 mb-4 px-2" id="repo-${this.id}">
         <div class="card border-secondary h-100" >
-          <div class="card-body">
-            <div class="d-flex align-items-center flex-wrap">
-              <h5 class="mb-2 mr-2">
-                ${this.name}
-              </h5>
-              <span class="mb-2 badge bg-light">
+          <div class="card-body d-flex flex-column justify-content-between">
+            <div class="d-flex align-items-center flex-wrap" >
+              <a class="text-info d-flex align-items-center flex-wrap mb-2 mr-2" href="${
+                this.htmlUrl
+              }" target="_blank">
+                <h5>
+                  ${this.name}
+                </h5>
+              </a>
+              <span class="mb-2 badge bg-light mr-2">
                 ${this.visibility}
               </span>
+              <span class="mb-2 d-flex align-items-center">
+                <img alt="레포지토리 스타 아이콘" src="${
+                  this.stargazersCount > 0 ? "star_filled.svg" : "star.svg"
+                }" height="16"/>
+                ${this.stargazersCount}
+              </span>
             </div>
             <p class="card-text">${this.description ?? ""}</p>
             <div>
-              <span class="text-secondary">
+              <span class="text-secondary mr-2">
                 ${this.language ?? ""}
               </span>
+              <span class="text-secondary mr-2">
+                <img src="fork.svg" alt="포크 아이콘" height="18"/>
+                ${this.forks}
+              </span>
               <span class="text-secondary">
-                ${this.forks > 0 ? this.forks : ""}
+                <img src="watch.svg" alt="워치 아이콘" height="18"/>
+                ${this.watchersCount}
               </span>
             </div>
           </div>

리뷰: `models/User.js` 에서 사용되는 이미지에는 alt가 한글로 되어있는데 이부분은 영어로 되어있네요! 통일시켜주는 것도 좋을 것 같아요~
@@ -0,0 +1,65 @@
+import History from "@models/History";
+import {
+  getHistoryItemTemplate,
+  NO_HISTORY_TEMPLATE,
+} from "@templates/history";
+
+export default class HistoryController {
+  constructor(historyContainer) {
+    this.historyContainer = historyContainer;
+    this.numberOfHistory = 0;
+    this._init();
+  }
+  _init() {
+    this._initEvent();
+    this._initHistory();
+  }
+  _initEvent() {
+    this.historyContainer.addEventListener("click", (event) => {
+      if (event.target.classList.contains("button__delete")) {
+        const toDeleteEl = event.target.parentElement;
+        this.deleteHistory(toDeleteEl);
+      }
+    });
+  }
+  _initHistory() {
+    const history = History.getAll();
+    this.numberOfHistory = history.length;
+
+    if (this.numberOfHistory === 0) {
+      this.historyContainer.innerHTML = NO_HISTORY_TEMPLATE;
+      return;
+    }
+
+    const historyTemplates = history
+      .map(({ value, id }) => getHistoryItemTemplate(value, id))
+      .join("");
+    this.historyContainer.innerHTML = historyTemplates;
+  }
+  addHistory(searchValue) {
+    const historyId = History.add(searchValue);
+    if (!historyId) {
+      return;
+    }
+
+    const listItemEl = getHistoryItemTemplate(searchValue, historyId);
+
+    this.numberOfHistory++;
+    if (this.numberOfHistory === 1) {
+      this.historyContainer.innerHTML = listItemEl;
+      return;
+    }
+
+    this.historyContainer.insertAdjacentHTML("beforeend", listItemEl);
+  }
+  deleteHistory(historyEl) {
+    const id = historyEl.id;
+    this.historyContainer.removeChild(historyEl);
+    History.delete(id);
+    this.numberOfHistory--;
+
+    if (this.numberOfHistory === 0) {
+      this.historyContainer.innerHTML = NO_HISTORY_TEMPLATE;
+    }
+  }
+}

리뷰: 지어주신 변수명도 충분히 의미파악이 잘되는데요! 혹시 좀더 간결한 변수명을 원하신다면 `historyCount` 로 네이밍하는 방법도 있을 것 같습니다 ㅎㅎ
@@ -19,41 +19,56 @@ export default class Repo {
   }) {
     this.id = id;
     this.name = name;
-    this.full_name = full_name;
+    this.fullName = full_name;
     this.owner = owner;
-    this.html_url = html_url;
+    this.htmlUrl = html_url;
     this.description = description;
-    this.stargazers_count = stargazers_count;
-    this.watchers_count = watchers_count;
-    this.forks_count = forks_count;
+    this.stargazersCount = stargazers_count;
+    this.watchersCount = watchers_count;
+    this.forksCount = forks_count;
     this.forks = forks;
-    this.created_at = created_at;
-    this.updated_at = updated_at;
-    this.clone_url = clone_url;
+    this.createdAt = created_at;
+    this.updatedAt = updated_at;
+    this.cloneUrl = clone_url;
     this.language = language;
     this.watchers = watchers;
     this.visibility = visibility;
   }
   render() {
     return `
-      <div class="col-12 col-sm-6  mb-4" id="repo-${this.id}">
+      <div class="col-12 col-sm-6 mb-4 px-2" id="repo-${this.id}">
         <div class="card border-secondary h-100" >
-          <div class="card-body">
-            <div class="d-flex align-items-center flex-wrap">
-              <h5 class="mb-2 mr-2">
-                ${this.name}
-              </h5>
-              <span class="mb-2 badge bg-light">
+          <div class="card-body d-flex flex-column justify-content-between">
+            <div class="d-flex align-items-center flex-wrap" >
+              <a class="text-info d-flex align-items-center flex-wrap mb-2 mr-2" href="${
+                this.htmlUrl
+              }" target="_blank">
+                <h5>
+                  ${this.name}
+                </h5>
+              </a>
+              <span class="mb-2 badge bg-light mr-2">
                 ${this.visibility}
               </span>
+              <span class="mb-2 d-flex align-items-center">
+                <img alt="레포지토리 스타 아이콘" src="${
+                  this.stargazersCount > 0 ? "star_filled.svg" : "star.svg"
+                }" height="16"/>
+                ${this.stargazersCount}
+              </span>
             </div>
             <p class="card-text">${this.description ?? ""}</p>
             <div>
-              <span class="text-secondary">
+              <span class="text-secondary mr-2">
                 ${this.language ?? ""}
               </span>
+              <span class="text-secondary mr-2">
+                <img src="fork.svg" alt="포크 아이콘" height="18"/>
+                ${this.forks}
+              </span>
               <span class="text-secondary">
-                ${this.forks > 0 ? this.forks : ""}
+                <img src="watch.svg" alt="워치 아이콘" height="18"/>
+                ${this.watchersCount}
               </span>
             </div>
           </div>

리뷰: 그 부분은 신경쓰지 못했네요!
한국 서비스에서는 alt도 한글로 적어주는게 더 나을까요?
@@ -0,0 +1,65 @@
+import History from "@models/History";
+import {
+  getHistoryItemTemplate,
+  NO_HISTORY_TEMPLATE,
+} from "@templates/history";
+
+export default class HistoryController {
+  constructor(historyContainer) {
+    this.historyContainer = historyContainer;
+    this.numberOfHistory = 0;
+    this._init();
+  }
+  _init() {
+    this._initEvent();
+    this._initHistory();
+  }
+  _initEvent() {
+    this.historyContainer.addEventListener("click", (event) => {
+      if (event.target.classList.contains("button__delete")) {
+        const toDeleteEl = event.target.parentElement;
+        this.deleteHistory(toDeleteEl);
+      }
+    });
+  }
+  _initHistory() {
+    const history = History.getAll();
+    this.numberOfHistory = history.length;
+
+    if (this.numberOfHistory === 0) {
+      this.historyContainer.innerHTML = NO_HISTORY_TEMPLATE;
+      return;
+    }
+
+    const historyTemplates = history
+      .map(({ value, id }) => getHistoryItemTemplate(value, id))
+      .join("");
+    this.historyContainer.innerHTML = historyTemplates;
+  }
+  addHistory(searchValue) {
+    const historyId = History.add(searchValue);
+    if (!historyId) {
+      return;
+    }
+
+    const listItemEl = getHistoryItemTemplate(searchValue, historyId);
+
+    this.numberOfHistory++;
+    if (this.numberOfHistory === 1) {
+      this.historyContainer.innerHTML = listItemEl;
+      return;
+    }
+
+    this.historyContainer.insertAdjacentHTML("beforeend", listItemEl);
+  }
+  deleteHistory(historyEl) {
+    const id = historyEl.id;
+    this.historyContainer.removeChild(historyEl);
+    History.delete(id);
+    this.numberOfHistory--;
+
+    if (this.numberOfHistory === 0) {
+      this.historyContainer.innerHTML = NO_HISTORY_TEMPLATE;
+    }
+  }
+}

리뷰: 간결한 버전도 괜찮군요 ㅎㅎ