명예의 전당 리팩토링 시키기(4)

JinYoungMo·2024년 3월 31일

명예의 전당을 투표 수 기준으로 만들었으니 이제는 좋아요 순 조회순으로 만들어볼 차례이다.

일단

trail_hall_of_fame.entity.ts

@Entity()
export class TrialHallOfFames {
  @PrimaryColumn({ type: 'int', unique: true })
  id: number;

  @Column({ type: 'int' })
  userId: number;

  @Column({ type: 'varchar', nullable: false })
  title: string;

  @Column({ type: 'varchar', nullable: false })
  content: string;

  @Column({ type: 'varchar'})
  totalVotes: number;
  
  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;
}

이 entity는 투표수를 기준으로 하기 위해 만들었던 엔티티 이다.

trial_hall_of_fame.like.entity.ts

@Entity()
  export class TrialLikeHallOfFames {
    @PrimaryColumn({ type: 'int', unique: true })
    id: number;
  
    @Column({ type: 'int' })
    userId: number;
  
    @Column({ type: 'varchar', nullable: false })
    title: string;
  
    @Column({ type: 'varchar', nullable: false })
    content: string;
  
    @Column({ type: 'varchar'})
    totallike: number;
    
    @CreateDateColumn({ type: 'timestamp' })
    createdAt: Date;
  
    @UpdateDateColumn({ type: 'timestamp' })
    updatedAt: Date;
  }

trial_hall_of_fame.like.entity.ts 이다.
기존의 trail_hall_of_fame.entity.ts 에서 totalVotes를 totallike로 바꾸어 주었다.

trial_hall_of_fame.Views.entity.ts

@Entity()
  export class TrialViewHallOfFames {
    @PrimaryColumn({ type: 'int', unique: true })
    id: number;
  
    @Column({ type: 'int' })
    userId: number;
  
    @Column({ type: 'varchar', nullable: false })
    title: string;
  
    @Column({ type: 'varchar', nullable: false })
    content: string;
  
    @Column({ type: 'varchar'})
    totalview: number;
    
    @CreateDateColumn({ type: 'timestamp' })
    createdAt: Date;
  
    @UpdateDateColumn({ type: 'timestamp' })
    updatedAt: Date;
  }

trial_hall_of_fame.Views.entity.ts 이다.
기존의 trial_hall_of_frame.entity.ts 에서 totalVotes를 totalViews로 바꾸어 주었다.

다음은 서비스 로직이다.

기존 코드

// 명예의 전당 매서드 일주일마다 업데이트
@Cron(CronExpression.EVERY_WEEK)
async updateHallOfFame() {
  const lastWeekStart = new Date();
  lastWeekStart.setDate(lastWeekStart.getDate() - 7 - lastWeekStart.getDay())
  lastWeekStart.setHours(0, 0, 0, 0);

  const lastWeekEnd = new Date(lastWeekStart);
  lastWeekEnd.setDate(lastWeekEnd.getDate() + 6)
  lastWeekEnd.setHours(23, 59, 59, 999);

  // 지난주에 진행된 투표 데이터를 조회
  const lastWeekVotes = await this.votesRepository.find({
    where: {
      createdAt: Between(lastWeekStart, lastWeekEnd),
    },
  });
  
  / 투표 기반으로 명예의 전당 집계
  // 투표 수 데이터 가공
  const hallOfFameData = this.aggVotesForHallOfFame(lastWeekVotes)
  
  // 투표 명전 업데이트
  await this.updateHallOfFameDatabase(hallOfFameData);
// 날짜 추상화 매서드
private getThisMonthRange(){
  const start = new Date();
  start.setDate(1); // 이번달 첫쨰날
  start.setHours(0, 0, 0, 0) // 자정

  const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
  end.setHours(23, 59, 59, 999) // 하루의 마지막 시간

  return { start, end }
}
// 투표 데이터 집계 매서드(투표 수 기준)
private async aggVotesForHallOfFame(votes: Votes[]){
  const { start, end } = this.getThisMonthRange();

  const candidates = await this.votesRepository
  .createQueryBuilder("vote")
  .select(['vote.id', 'vote.title1', 'vote.title2'])
  .addSelect("vote.voteCount1 + vote.voteCount2", "totalVotes")
  .where('vote.createdAt BETWEEN :start AND :end', { start: start.toISOString(), end: end.toISOString() })
  .having("totalVotes >= :minTotalVotes", { minTotalVotes: 100 }) // 투표 수 100 이상인 것만 조회
  .orderBy('totalVotes', "DESC")
  .limit(1000)// 1000개 이상의 데이터가 없어도 남은 데이터 만큼 올라간다. 즉 데이터 집계 상한선이 1000개 라는 뜻
  .groupBy("vote.id")
  .getRawMany();

  return candidates
}
// DB에 명예의 전당 데이터를 업데이트(배열형태로 받아서 한번에 저장) ver 1.(투표수)
private async updateHallOfFameDatabase(hallOfFameData: any){
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  try{
  // 한번에 저장
    const newHallOfFameEntries = hallOfFameData.map(data => {
    const newHallOfFameEntry = new TrialHallOfFames();
    newHallOfFameEntry.id = data.id // vote table의 id임다
    newHallOfFameEntry.userId = data.trial.userId // vote에는 userId가 없으므로 일대일관계인 trial에 가서 userId 가져옴
    newHallOfFameEntry.title = data.title1 + 'Vs' + data.title2
    newHallOfFameEntry.content = data.trial.content; // vote에는 content가 없으므로 일대일관계인 trial에 가서 content 가져옴
    newHallOfFameEntry.totalVotes = data.voteCount1 + data.voteCount2;
    newHallOfFameEntry.createdAt = new Date();
    newHallOfFameEntry.updatedAt = new Date();
    return newHallOfFameEntry;
  });
    // DB에 새로운 명전 저장
    await queryRunner.manager.save(TrialHallOfFames, newHallOfFameEntries)

    await queryRunner.commitTransaction();
  } catch (err) {
    console.log(err);
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
}

이 서비스 로직에서 그저 조회수와 좋아요만 추가해주면 되는 것이다.
다음은 추가한 서비스 코드이다.

// 명예의 전당 매서드 일주일마다 업데이트
@Cron(CronExpression.EVERY_WEEK)
async updateHallOfFame() {
  const lastWeekStart = new Date();
  lastWeekStart.setDate(lastWeekStart.getDate() - 7 - lastWeekStart.getDay())
  lastWeekStart.setHours(0, 0, 0, 0);

  const lastWeekEnd = new Date(lastWeekStart);
  lastWeekEnd.setDate(lastWeekEnd.getDate() + 6)
  lastWeekEnd.setHours(23, 59, 59, 999);

  // 지난주에 진행된 투표 데이터를 조회
  const lastWeekVotes = await this.votesRepository.find({
    where: {
      createdAt: Between(lastWeekStart, lastWeekEnd),
    },
  });

  // 지난주에 진행된 재판 데이터를 조회
  const lastWeekTrials= await this.trialsRepository.find({
    where: {
      createdAt: Between(lastWeekStart, lastWeekEnd)
    },
  });

  // 투표 기반으로 명예의 전당 집계
  // 투표 수 데이터 가공
  const hallOfFameData = this.aggVotesForHallOfFame(lastWeekVotes)
  // 좋아요 데이터 가공
  const likeHallOfFameData = this.aggVotesLikeForHallOfFame(lastWeekTrials)
  // 조회수 데이터 가공
  const viewHallOfFameData = this.aggVotesViewForHallOfFame(lastWeekTrials)

  // 업데이트
  // 투표 명전 업데이트
  await this.updateHallOfFameDatabase(hallOfFameData);

  // 좋아요 명전 업데이트
  await this.updateLikeHallOfFameDatabase(likeHallOfFameData);

  // 조회수 명전 업데이트
  await this.updateViewHallOfFameDatabase(viewHallOfFameData);
}




// 날짜 추상화 매서드
private getThisMonthRange(){
  const start = new Date();
  start.setDate(1); // 이번달 첫쨰날
  start.setHours(0, 0, 0, 0) // 자정

  const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
  end.setHours(23, 59, 59, 999) // 하루의 마지막 시간

  return { start, end }
}

// 투표 데이터 집계 매서드(좋아요 수 기준)
private async aggVotesLikeForHallOfFame(trials: Trials[]){
  const { start, end } = this.getThisMonthRange();

  const candidates = await this.trialsRepository
  .createQueryBuilder("trial")
  .select(['trial.id', 'trial.title', 'trial.content'])
  .addSelect("trial.like", "likes")
  .where('trial.createdAt BETWEEN :start AND :end', { start: start.toISOString(), end: end.toISOString() })
  .having("likes >= :minlikes", { minlikes: 100 }) // 투표 수 100 이상인 것만 조회
  .orderBy('likes', "DESC")
  .limit(1000)// 1000개 이상의 데이터가 없어도 남은 데이터 만큼 올라간다. 즉 데이터 집계 상한선이 1000개 라는 뜻
  .groupBy("trial.id")
  .getRawMany();

  return candidates
}

// 투표 데이터 집계 매서드(조회수 수 기준)
private async aggVotesViewForHallOfFame(trials: Trials[]){
  const { start, end } = this.getThisMonthRange();

  const candidates = await this.trialsRepository
  .createQueryBuilder("trial")
  .select(['trial.id', 'trial.title', 'trial.content'])
  .addSelect("trial.view", "views")
  .where('trial.createdAt BETWEEN :start AND :end', { start: start.toISOString(), end: end.toISOString() })
  .having("views >= :minviews", { minviews: 100 }) // 투표 수 100 이상인 것만 조회
  .orderBy('views', "DESC")
  .limit(1000)// 1000개 이상의 데이터가 없어도 남은 데이터 만큼 올라간다. 즉 데이터 집계 상한선이 1000개 라는 뜻
  .groupBy("trial.id")
  .getRawMany();

  return candidates
}

// 투표 데이터 집계 매서드(투표 수 기준)
private async aggVotesForHallOfFame(votes: Votes[]){
  const { start, end } = this.getThisMonthRange();

  const candidates = await this.votesRepository
  .createQueryBuilder("vote")
  .select(['vote.id', 'vote.title1', 'vote.title2'])
  .addSelect("vote.voteCount1 + vote.voteCount2", "totalVotes")
  .where('vote.createdAt BETWEEN :start AND :end', { start: start.toISOString(), end: end.toISOString() })
  .having("totalVotes >= :minTotalVotes", { minTotalVotes: 100 }) // 투표 수 100 이상인 것만 조회
  .orderBy('totalVotes', "DESC")
  .limit(1000)// 1000개 이상의 데이터가 없어도 남은 데이터 만큼 올라간다. 즉 데이터 집계 상한선이 1000개 라는 뜻
  .groupBy("vote.id")
  .getRawMany();

  return candidates
}

// DB에 명예의 전당 데이터를 업데이트(배열형태로 받아서 한번에 저장) ver 1.(투표수)
private async updateHallOfFameDatabase(hallOfFameData: any){
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  try{
  // 한번에 저장
    const newHallOfFameEntries = hallOfFameData.map(data => {
    const newHallOfFameEntry = new TrialHallOfFames();
    newHallOfFameEntry.id = data.id // vote table의 id임다
    newHallOfFameEntry.userId = data.trial.userId // vote에는 userId가 없으므로 일대일관계인 trial에 가서 userId 가져옴
    newHallOfFameEntry.title = data.title1 + 'Vs' + data.title2
    newHallOfFameEntry.content = data.trial.content; // vote에는 content가 없으므로 일대일관계인 trial에 가서 content 가져옴
    newHallOfFameEntry.totalVotes = data.voteCount1 + data.voteCount2;
    newHallOfFameEntry.createdAt = new Date();
    newHallOfFameEntry.updatedAt = new Date();
    return newHallOfFameEntry;
  });
    // DB에 새로운 명전 저장
    await queryRunner.manager.save(TrialHallOfFames, newHallOfFameEntries)

    await queryRunner.commitTransaction();
  } catch (err) {
    console.log(err);
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
}


// DB에 명예의 전당 데이터를 업데이트(배열형태로 받아서 한번에 저장) ver 1.(좋아요)
private async updateLikeHallOfFameDatabase(hallOfFameData: any){
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try{
// 한번에 저장
  const newLikeHallOfFameEntries = hallOfFameData.map(data => {
  const newLikeHallOfFameEntry = new TrialLikeHallOfFames();
  newLikeHallOfFameEntry.id = data.id // vote table의 id임다
  newLikeHallOfFameEntry.userId = data.userId // vote에는 userId가 없으므로 일대일관계인 trial에 가서 userId 가져옴
  newLikeHallOfFameEntry.title = data.title;
  newLikeHallOfFameEntry.content = data.content; // vote에는 content가 없으므로 일대일관계인 trial에 가서 content 가져옴
  newLikeHallOfFameEntry.totallike = data.like;
  newLikeHallOfFameEntry.createdAt = new Date();
  newLikeHallOfFameEntry.updatedAt = new Date();
  return newLikeHallOfFameEntry;
});
  // DB에 새로운 명전 저장
  await queryRunner.manager.save(TrialLikeHallOfFames, newLikeHallOfFameEntries)

  await queryRunner.commitTransaction();
} catch (err) {
  console.log(err);
  await queryRunner.rollbackTransaction();
} finally {
  await queryRunner.release();
}
}


// DB에 명예의 전당 데이터를 업데이트(배열형태로 받아서 한번에 저장) ver 1.(조회수)
private async updateViewHallOfFameDatabase(hallOfFameData: any){
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try{
// 한번에 저장
  const newViewHallOfFameEntries = hallOfFameData.map(data => {
  const newViewHallOfFameEntry = new TrialViewHallOfFames();
  newViewHallOfFameEntry.id = data.id // vote table의 id임다
  newViewHallOfFameEntry.userId = data.userId // vote에는 userId가 없으므로 일대일관계인 trial에 가서 userId 가져옴
  newViewHallOfFameEntry.title = data.title;
  newViewHallOfFameEntry.content = data.content; // vote에는 content가 없으므로 일대일관계인 trial에 가서 content 가져옴
  newViewHallOfFameEntry.totalview = data.like;
  newViewHallOfFameEntry.createdAt = new Date();
  newViewHallOfFameEntry.updatedAt = new Date();
  return newViewHallOfFameEntry;
});
  // DB에 새로운 명전 저장
  await queryRunner.manager.save(TrialViewHallOfFames, newViewHallOfFameEntries)

  await queryRunner.commitTransaction();
} catch (err) {
  console.log(err);
  await queryRunner.rollbackTransaction();
} finally {
  await queryRunner.release();
}
}

0개의 댓글