From eecc484ee1deb37193cbaa9ef57a300d563222b1 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Thu, 1 Feb 2024 00:03:46 +0900 Subject: [PATCH 1/2] refactor: convert contents util to function --- src/contents/contents.module.ts | 11 +- src/contents/contents.service.ts | 21 ++-- src/contents/util/content.util.ts | 167 +++++++++++++++--------------- 3 files changed, 93 insertions(+), 106 deletions(-) diff --git a/src/contents/contents.module.ts b/src/contents/contents.module.ts index 8e1364a..71e5f5e 100644 --- a/src/contents/contents.module.ts +++ b/src/contents/contents.module.ts @@ -2,25 +2,16 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ContentsController } from './contents.controller'; import { ContentsService } from './contents.service'; -import { Category } from '../categories/category.entity'; import { Content } from './entities/content.entity'; -import { CategoryUtil } from './util/category.util'; import { ContentRepository } from './repository/content.repository'; import { CategoryRepository } from '../categories/category.repository'; import { UsersModule } from '../users/users.module'; -import { ContentUtil } from './util/content.util'; import { OpenaiModule } from '../openai/openai.module'; @Module({ imports: [TypeOrmModule.forFeature([Content]), UsersModule, OpenaiModule], controllers: [ContentsController], - providers: [ - ContentsService, - ContentRepository, - ContentUtil, - CategoryRepository, - CategoryUtil, - ], + providers: [ContentsService, ContentRepository, CategoryRepository], exports: [ContentsService], }) export class ContentsModule {} diff --git a/src/contents/contents.service.ts b/src/contents/contents.service.ts index cf52f76..3891fab 100644 --- a/src/contents/contents.service.ts +++ b/src/contents/contents.service.ts @@ -27,10 +27,10 @@ import { Content } from './entities/content.entity'; import { LoadReminderCountOutput } from './dtos/load-personal-remider-count.dto'; import { UserRepository } from '../users/repository/user.repository'; import { ContentRepository } from './repository/content.repository'; -import { CategoryUtil } from './util/category.util'; import { CategoryRepository } from '../categories/category.repository'; -import { ContentUtil } from './util/content.util'; +import { getLinkInfo } from './util/content.util'; import { GetLinkInfoResponseDto } from './dtos/get-link.response.dto'; +import { checkContentDuplicateAndAddCategorySaveLog } from './util/category.util'; @Injectable() export class ContentsService { @@ -39,8 +39,6 @@ export class ContentsService { private readonly contentRepository: ContentRepository, private readonly summaryService: SummaryService, private readonly categoryRepository: CategoryRepository, - private readonly categoryUtil: CategoryUtil, - private readonly contentUtil: ContentUtil, private readonly dataSource: DataSource, ) {} @@ -73,7 +71,7 @@ export class ContentsService { siteName, description, coverImg, - } = await this.contentUtil.getLinkInfo(link); + } = await getLinkInfo(link); title = title ? title : linkTitle; let category: Category | null = null; @@ -85,7 +83,7 @@ export class ContentsService { queryRunner.manager, ); - await this.categoryUtil.checkContentDuplicateAndAddCategorySaveLog( + await checkContentDuplicateAndAddCategorySaveLog( link, category, userInDb, @@ -141,11 +139,12 @@ export class ContentsService { ); } for (const link of contentLinks) { - const { title, description, coverImg, siteName } = - await this.contentUtil.getLinkInfo(link); + const { title, description, coverImg, siteName } = await getLinkInfo( + link, + ); if (category) { - await this.categoryUtil.checkContentDuplicateAndAddCategorySaveLog( + await checkContentDuplicateAndAddCategorySaveLog( link, category, userInDb, @@ -220,7 +219,7 @@ export class ContentsService { queryRunnerManager, ); - await this.categoryUtil.checkContentDuplicateAndAddCategorySaveLog( + await checkContentDuplicateAndAddCategorySaveLog( link, category, userInDb, @@ -399,7 +398,7 @@ export class ContentsService { } async getLinkInfo(link: string) { - const data = await this.contentUtil.getLinkInfo(link); + const data = await getLinkInfo(link); return new GetLinkInfoResponseDto(data); } diff --git a/src/contents/util/content.util.ts b/src/contents/util/content.util.ts index f2fca95..f58a2ce 100644 --- a/src/contents/util/content.util.ts +++ b/src/contents/util/content.util.ts @@ -1,98 +1,95 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import * as cheerio from 'cheerio'; import axios from 'axios'; -@Injectable() -export class ContentUtil { - async getLinkInfo(link: string) { - let title: string | undefined = ''; - let coverImg: string | undefined = ''; - let description: string | undefined = ''; - let siteName: string | undefined; +export const getLinkInfo = async (link: string) => { + let title: string | undefined = ''; + let coverImg: string | undefined = ''; + let description: string | undefined = ''; + let siteName: string | undefined; - if (!link.match(/^(http|https):\/\//)) { - link = `http://${link}`; - } + if (!link.match(/^(http|https):\/\//)) { + link = `http://${link}`; + } - await axios - .get(link) - .then((res) => { - if (res.status !== 200) { - throw new BadRequestException('잘못된 링크입니다.'); - } else { - const data = res.data; - if (typeof data === 'string') { - const $ = cheerio.load(data); - title = $('title').text() !== '' ? $('title').text() : 'Untitled'; - $('meta').each((i, el) => { - const meta = $(el); - if (meta.attr('property') === 'og:image') { - coverImg = meta.attr('content'); - } - if (meta.attr('property') === 'og:description') { - description = meta.attr('content'); - } - if (meta.attr('property') === 'og:site_name') { - siteName = meta.attr('content'); - } - }); - } + await axios + .get(link) + .then((res) => { + if (res.status !== 200) { + throw new BadRequestException('잘못된 링크입니다.'); + } else { + const data = res.data; + if (typeof data === 'string') { + const $ = cheerio.load(data); + title = $('title').text() !== '' ? $('title').text() : 'Untitled'; + $('meta').each((i, el) => { + const meta = $(el); + if (meta.attr('property') === 'og:image') { + coverImg = meta.attr('content'); + } + if (meta.attr('property') === 'og:description') { + description = meta.attr('content'); + } + if (meta.attr('property') === 'og:site_name') { + siteName = meta.attr('content'); + } + }); } - }) - .catch((e) => { - // Control unreachable link - // if(e.message === 'Request failed with status code 403') { - // 403 에러가 발생하는 링크는 크롤링이 불가능한 링크이다. - // } - for (let idx = 1; idx < 3; idx++) { - if (link.split('/').at(-idx) !== '') { - title = link.split('/').at(-idx); - break; - } + } + }) + .catch((e) => { + // Control unreachable link + // if(e.message === 'Request failed with status code 403') { + // 403 에러가 발생하는 링크는 크롤링이 불가능한 링크이다. + // } + for (let idx = 1; idx < 3; idx++) { + if (link.split('/').at(-idx) !== '') { + title = link.split('/').at(-idx); + break; } - title = title ? title : 'Untitled'; - }); + } + title = title ? title : 'Untitled'; + }); - return { - title, - description, - coverImg, - siteName, - }; - } + return { + title, + description, + coverImg, + siteName, + }; +}; - async getLinkContent(link: string) { - let content: string | undefined = ''; +export const getLinkContent = async (link: string) => { + let content: string | undefined = ''; - if (!link.match(/^(http|https):\/\//)) { - link = `http://${link}`; - } + if (!link.match(/^(http|https):\/\//)) { + link = `http://${link}`; + } - await axios - .get(link) - .then((res) => { - if (res.status !== 200) { - throw new BadRequestException('잘못된 링크입니다.'); - } else { - const data = res.data; - if (typeof data === 'string') { - const $ = cheerio.load(data); - content = ''; - $('p').each((i, elem) => { - // 모든 p 태그에 대해 작업을 수행합니다. - content += $(elem).text() + '\n'; // 각 p 태그의 텍스트를 가져와서 누적합니다. - }); - } + await axios + .get(link) + .then((res) => { + if (res.status !== 200) { + throw new BadRequestException('잘못된 링크입니다.'); + } else { + const data = res.data; + if (typeof data === 'string') { + const $ = cheerio.load(data); + content = ''; + $('p').each((i, elem) => { + // 모든 p 태그에 대해 작업을 수행합니다. + content += $(elem).text() + '\n'; // 각 p 태그의 텍스트를 가져와서 누적합니다. + }); } - }) - .catch((e) => { - // Control unreachable link - // if(e.message === 'Request failed with status code 403') { - // 403 에러가 발생하는 링크는 크롤링이 불가능한 링크이다. - // } - content = ''; - }); + } + }) + .catch((e) => { + // Control unreachable link + // if(e.message === 'Request failed with status code 403') { + // 403 에러가 발생하는 링크는 크롤링이 불가능한 링크이다. + // } + content = ''; + }); - return content; - } -} + return content; +}; From 72f7e17b3fde6be899c91e3970e13b202eef0479 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Thu, 1 Feb 2024 00:03:57 +0900 Subject: [PATCH 2/2] refactor: convert category util to function --- src/categories/category.module.ts | 10 +- src/categories/category.repository.ts | 11 +- src/categories/category.service.ts | 42 ++-- src/contents/util/category.util.ts | 342 +++++++++++++------------- 4 files changed, 192 insertions(+), 213 deletions(-) diff --git a/src/categories/category.module.ts b/src/categories/category.module.ts index dc880e9..be433a9 100644 --- a/src/categories/category.module.ts +++ b/src/categories/category.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { ContentsModule } from '../contents/contents.module'; import { CategoryService } from './category.service'; -import { CategoryUtil } from '../contents/util/category.util'; import { OpenaiModule } from '../openai/openai.module'; import { UsersModule } from '../users/users.module'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -9,7 +8,6 @@ import { Category } from './category.entity'; import { CategoryController } from './category.controller'; import { ContentRepository } from '../contents/repository/content.repository'; import { CategoryRepository } from './category.repository'; -import { ContentUtil } from '../contents/util/content.util'; @Module({ imports: [ @@ -19,13 +17,7 @@ import { ContentUtil } from '../contents/util/content.util'; UsersModule, ], controllers: [CategoryController], - providers: [ - CategoryService, - CategoryUtil, - ContentRepository, - CategoryRepository, - ContentUtil, - ], + providers: [CategoryService, ContentRepository, CategoryRepository], exports: [CategoryRepository], }) export class CategoryModule {} diff --git a/src/categories/category.repository.ts b/src/categories/category.repository.ts index 6c77802..3fcf59b 100644 --- a/src/categories/category.repository.ts +++ b/src/categories/category.repository.ts @@ -5,15 +5,12 @@ import { NotFoundException, } from '@nestjs/common'; import { Category } from './category.entity'; -import { CategoryUtil } from '../contents/util/category.util'; import { User } from '../users/entities/user.entity'; +import { generateSlug } from '../contents/util/category.util'; @Injectable() export class CategoryRepository extends Repository { - constructor( - private readonly dataSource: DataSource, - private readonly categoryUtil: CategoryUtil, - ) { + constructor(private readonly dataSource: DataSource) { super(Category, dataSource.createEntityManager()); } @@ -34,7 +31,7 @@ export class CategoryRepository extends Repository { queryRunnerManager: EntityManager, ): Promise { // generate category name and slug - const { categorySlug } = this.categoryUtil.generateSlug(categoryName); + const { categorySlug } = generateSlug(categoryName); if (parentId) { // category depth should be 3 @@ -112,7 +109,7 @@ export class CategoryRepository extends Repository { .values( defaultCategories.map((categoryName) => ({ name: categoryName, - slug: this.categoryUtil.generateSlug(categoryName).categorySlug, + slug: generateSlug(categoryName).categorySlug, user: user, })), ) diff --git a/src/categories/category.service.ts b/src/categories/category.service.ts index ad2587c..ced4ae9 100644 --- a/src/categories/category.service.ts +++ b/src/categories/category.service.ts @@ -23,20 +23,23 @@ import { Category } from './category.entity'; import { Content } from '../contents/entities/content.entity'; import { CategoryRepository } from './category.repository'; import { ContentRepository } from '../contents/repository/content.repository'; -import { CategoryUtil } from '../contents/util/category.util'; -import { ContentUtil } from '../contents/util/content.util'; +import { getLinkContent, getLinkInfo } from '../contents/util/content.util'; import { OpenaiService } from '../openai/openai.service'; import { User } from '../users/entities/user.entity'; import { UserRepository } from '../users/repository/user.repository'; +import { + generateCategoriesTree, + generateSlug, + loadLogs, + makeCategoryListWithSaveCount, +} from '../contents/util/category.util'; @Injectable() export class CategoryService { constructor( private readonly contentRepository: ContentRepository, private readonly categoryRepository: CategoryRepository, - private readonly categoryUtil: CategoryUtil, private readonly userRepository: UserRepository, - private readonly contentUtil: ContentUtil, private readonly openaiService: OpenaiService, ) {} @@ -52,7 +55,7 @@ export class CategoryService { throw new NotFoundException('User not found'); } - const { categorySlug } = this.categoryUtil.generateSlug(categoryName); + const { categorySlug } = generateSlug(categoryName); if (parentId) { // category depth should be 3 @@ -152,7 +155,7 @@ export class CategoryService { if (category) { // Check if user has category with same slug if (categoryName) { - const { categorySlug } = this.categoryUtil.generateSlug(categoryName); + const { categorySlug } = generateSlug(categoryName); if ( userInDb.categories?.filter( (category) => @@ -296,8 +299,7 @@ export class CategoryService { } // make categories tree by parentid - const categoriesTree = - this.categoryUtil.generateCategoriesTree(categories); + const categoriesTree = generateCategoriesTree(categories); return { categoriesTree, @@ -312,8 +314,7 @@ export class CategoryService { ): Promise { try { // 로그 파일 내의 기록을 불러온다. - const recentCategoryList: RecentCategoryList[] = - this.categoryUtil.loadLogs(user.id); + const recentCategoryList: RecentCategoryList[] = loadLogs(user.id); // 캐시 내의 카테고리 리스트를 최신 순으로 정렬하고, 동시에 저장된 횟수를 추가한다. @@ -331,12 +332,11 @@ export class CategoryService { // 10개의 로그를 확인한다. i += 10; - recentCategoriesWithSaveCount = - this.categoryUtil.makeCategoryListWithSaveCount( - recentCategoryList, - recentCategoriesWithSaveCount, - i, - ); + recentCategoriesWithSaveCount = makeCategoryListWithSaveCount( + recentCategoryList, + recentCategoriesWithSaveCount, + i, + ); // 10개의 로그를 확인했으므로 남은 로그 수를 10개 감소시킨다. remainLogCount -= 10; @@ -430,10 +430,9 @@ export class CategoryService { categories.push(category.name); } }); - const { title, siteName, description } = - await this.contentUtil.getLinkInfo(link); + const { title, siteName, description } = await getLinkInfo(link); - const content = await this.contentUtil.getLinkContent(link); + const content = await getLinkContent(link); let questionLines = [ "You are a machine tasked with auto-categorizing articles based on information obtained through web scraping. You can only answer a single category name. Here is the article's information:", @@ -489,15 +488,14 @@ export class CategoryService { ): Promise { try { const { link, categories } = autoCategorizeBody; - const { title, siteName, description } = - await this.contentUtil.getLinkInfo(link); + const { title, siteName, description } = await getLinkInfo(link); /** * TODO: 본문 크롤링 개선 필요 * 현재 p 태그만 크롤링하는데, 불필요한 내용이 포함되는 경우가 많음 * 그러나 하나하나 예외 처리하는 방법을 제외하곤 방법을 못 찾은 상황 */ - const content = await this.contentUtil.getLinkContent(link); + const content = await getLinkContent(link); let questionLines = [ "You are a machine tasked with auto-categorizing articles based on information obtained through web scraping. You can only answer a single category name. Here is the article's information:", diff --git a/src/contents/util/category.util.ts b/src/contents/util/category.util.ts index 9b87b32..b2799b0 100644 --- a/src/contents/util/category.util.ts +++ b/src/contents/util/category.util.ts @@ -6,210 +6,202 @@ import { RecentCategoryListWithSaveCount, } from '../../categories/dtos/category.dto'; import { User } from '../../users/entities/user.entity'; -import { ConflictException, Injectable } from '@nestjs/common'; +import { ConflictException } from '@nestjs/common'; import * as fs from 'fs'; -@Injectable() -export class CategoryUtil { - generateSlug(name: string): CategorySlug { - const categoryName = name.trim().toLowerCase(); - const categorySlug = categoryName.replace(/ /g, '-'); +export const generateSlug = (name: string): CategorySlug => { + const categoryName = name.trim().toLowerCase(); + const categorySlug = categoryName.replace(/ /g, '-'); - return { categorySlug }; - } + return { categorySlug }; +}; - // make categories tree by parentId - generateCategoriesTree(categories: Category[]): CategoryTreeNode[] { - const categoriesTree: CategoryTreeNode[] = categories; - for (let i = 0; i < categoriesTree.length; i++) { - if (categoriesTree[i].parentId) { - // 세세부 카테고리 우선 작업 - const parent = categoriesTree.find( - (category) => - category.id === categoriesTree[i].parentId && category.parentId, - ); - if (parent) { - if (!parent.children) parent.children = []; - parent.children.push(categoriesTree[i]); - categoriesTree.splice(i, 1); - i--; - } +// make categories tree by parentId +export const generateCategoriesTree = ( + categories: Category[], +): CategoryTreeNode[] => { + const categoriesTree: CategoryTreeNode[] = categories; + for (let i = 0; i < categoriesTree.length; i++) { + if (categoriesTree[i].parentId) { + // 세세부 카테고리 우선 작업 + const parent = categoriesTree.find( + (category) => + category.id === categoriesTree[i].parentId && category.parentId, + ); + if (parent) { + if (!parent.children) parent.children = []; + parent.children.push(categoriesTree[i]); + categoriesTree.splice(i, 1); + i--; } } + } - for (let i = 0; i < categoriesTree.length; i++) { - if (categoriesTree[i].parentId) { - // 중간 카테고리 작업(세부 카테고리) - const parent = categoriesTree.find( - (category) => category.id === categoriesTree[i].parentId, - ); - if (parent) { - if (!parent.children) parent.children = []; - parent.children.push(categoriesTree[i]); - categoriesTree.splice(i, 1); - i--; - } + for (let i = 0; i < categoriesTree.length; i++) { + if (categoriesTree[i].parentId) { + // 중간 카테고리 작업(세부 카테고리) + const parent = categoriesTree.find( + (category) => category.id === categoriesTree[i].parentId, + ); + if (parent) { + if (!parent.children) parent.children = []; + parent.children.push(categoriesTree[i]); + categoriesTree.splice(i, 1); + i--; } } - - return categoriesTree; } - findCategoryFamily( - categories: Category[] | undefined, - category: Category, - ): CategoryTreeNode[] { - if (!categories) return [category]; - - const topParentId = findTopParentCategory(categories, category); - const categoriesTree: CategoryTreeNode[] = - this.generateCategoriesTree(categories); + return categoriesTree; +}; - return categoriesTree.filter((category) => category.id === topParentId); - } +export const findCategoryFamily = ( + categories: Category[] | undefined, + category: Category, +): CategoryTreeNode[] => { + if (!categories) return [category]; - /** - * 대 카테고리를 기준으로 중복 체크하고, - * 최상위 카테고리의 카운트를 올려줌 - * - * @param link - * @param category - * @param userInDb - */ - async checkContentDuplicateAndAddCategorySaveLog( - link: string | undefined, - category: Category, - userInDb: User, - ): Promise { - // 최상위 카테고리부터 시작해서 하위 카테고리까지의 그룹을 찾아옴 - const categoryFamily: CategoryTreeNode[] = this.findCategoryFamily( - userInDb?.categories, - category, - ); + const topParentId = findTopParentCategory(categories, category); + const categoriesTree: CategoryTreeNode[] = generateCategoriesTree(categories); - /* - * 카테고리의 중복을 체크하고, 중복이 없다면 최상위 카테고리의 count를 증가시킴 - */ - - // 카테고리 그룹을 flat 시킴 - const flatDeep = ( - arr: CategoryTreeNode[], - d: number, - ): CategoryTreeNode[] => { - return d > 0 - ? arr.reduce((acc: CategoryTreeNode[], cur) => { - const forConcat = [cur]; - return acc.concat( - cur.children - ? forConcat.concat(flatDeep(cur.children, d - 1)) - : cur, - ); - }, []) - : arr.slice(); - }; + return categoriesTree.filter((category) => category.id === topParentId); +}; - const flatCategoryFamily: CategoryTreeNode[] = flatDeep( - categoryFamily, - Infinity, - ); +/** + * 대 카테고리를 기준으로 중복 체크하고, + * 최상위 카테고리의 카운트를 올려줌 + * + * @param link + * @param category + * @param userInDb + */ +export const checkContentDuplicateAndAddCategorySaveLog = async ( + link: string | undefined, + category: Category, + userInDb: User, +): Promise => { + // 최상위 카테고리부터 시작해서 하위 카테고리까지의 그룹을 찾아옴 + const categoryFamily: CategoryTreeNode[] = findCategoryFamily( + userInDb?.categories, + category, + ); + + /* + * 카테고리의 중복을 체크하고, 중복이 없다면 최상위 카테고리의 count를 증가시킴 + */ - // 유저의 대 category 내에 같은 link로 된 content가 있는지 체크 - if (userInDb.contents) { - for (const contentInFilter of userInDb.contents) { - if ( - contentInFilter.link === link && - flatCategoryFamily.filter( - (categoryInFamily) => - categoryInFamily.id === contentInFilter.category?.id, - ).length > 0 - ) { - throw new ConflictException( - 'Content with that link already exists in same category family.', + // 카테고리 그룹을 flat 시킴 + const flatDeep = (arr: CategoryTreeNode[], d: number): CategoryTreeNode[] => { + return d > 0 + ? arr.reduce((acc: CategoryTreeNode[], cur) => { + const forConcat = [cur]; + return acc.concat( + cur.children + ? forConcat.concat(flatDeep(cur.children, d - 1)) + : cur, ); - } + }, []) + : arr.slice(); + }; + + const flatCategoryFamily: CategoryTreeNode[] = flatDeep( + categoryFamily, + Infinity, + ); + + // 유저의 대 category 내에 같은 link로 된 content가 있는지 체크 + if (userInDb.contents) { + for (const contentInFilter of userInDb.contents) { + if ( + contentInFilter.link === link && + flatCategoryFamily.filter( + (categoryInFamily) => + categoryInFamily.id === contentInFilter.category?.id, + ).length > 0 + ) { + throw new ConflictException( + 'Content with that link already exists in same category family.', + ); } } + } - /* - * 최상위 카테고리의 count를 증가시킨 후, - * 해당 카테고리의 저장 기록을 유저 로그 파일에 추가함 - */ + /* + * 최상위 카테고리의 count를 증가시킨 후, + * 해당 카테고리의 저장 기록을 유저 로그 파일에 추가함 + */ - // 최상위 카테고리 분리 - const updatedTopCategory: Category = categoryFamily[0]; + // 최상위 카테고리 분리 + const updatedTopCategory: Category = categoryFamily[0]; - // 최상위 카테고리의 count 증가 - const log = `{"categoryId": ${ - updatedTopCategory.id - },"savedAt": ${new Date().getTime()}}\n`; + // 최상위 카테고리의 count 증가 + const log = `{"categoryId": ${ + updatedTopCategory.id + },"savedAt": ${new Date().getTime()}}\n`; - // 유저 로그 파일에 로그 추가 - fs.appendFileSync( - `${__dirname}/../../../user_logs/${userInDb.id}.txt`, - log, - ); - } + // 유저 로그 파일에 로그 추가 + fs.appendFileSync(`${__dirname}/../../../user_logs/${userInDb.id}.txt`, log); +}; - /** - * 파일에서 로그를 불러오는 함수 - * @param id - * @returns RecentCategoryList[] - */ - loadLogs(id: number): RecentCategoryList[] { - const logList: string[] = fs - .readFileSync(`${__dirname}/../../user_logs/${id}.txt`) - .toString() - .split('\n'); - logList.pop(); // 마지막 줄은 빈 줄이므로 제거 - - // logList를 RecentCategoryList[]로 변환 - const recentCategoryList: RecentCategoryList[] = logList.map((str) => { - const categoryId = +str.split('"categoryId": ')[1].split(',')[0]; - const savedAt = +str.split('"savedAt": ')[1].split('}')[0]; - return { - categoryId, - savedAt, - }; - }); - - // 최신 순으로 정렬 후 반환 - return recentCategoryList.reverse(); - } +/** + * 파일에서 로그를 불러오는 함수 + * @param id + * @returns RecentCategoryList[] + */ +export const loadLogs = (id: number): RecentCategoryList[] => { + const logList: string[] = fs + .readFileSync(`${__dirname}/../../user_logs/${id}.txt`) + .toString() + .split('\n'); + logList.pop(); // 마지막 줄은 빈 줄이므로 제거 + + // logList를 RecentCategoryList[]로 변환 + const recentCategoryList: RecentCategoryList[] = logList.map((str) => { + const categoryId = +str.split('"categoryId": ')[1].split(',')[0]; + const savedAt = +str.split('"savedAt": ')[1].split('}')[0]; + return { + categoryId, + savedAt, + }; + }); - /** - * 불러온 로그를 바탕으로 카테고리당 저장된 카운트와 함께 배열을 만드는 함수(매번 10개씩 조회한다.) - * @param recentCategoryList - * @param recentCategoriesWithSaveCount - * @param till - * @returns - */ - makeCategoryListWithSaveCount( - recentCategoryList: RecentCategoryList[], - recentCategoriesWithSaveCount: RecentCategoryListWithSaveCount[], - till: number, - ): RecentCategoryListWithSaveCount[] { - const start: number = till - 10; - const end: number = till; - for (let i = start; i < end && i < recentCategoryList.length; i++) { - const inNewList = recentCategoriesWithSaveCount.find( - (category) => category.categoryId === recentCategoryList[i].categoryId, - ); - if (inNewList) { - inNewList.saveCount++; - } else { - recentCategoriesWithSaveCount.push({ - ...recentCategoryList[i], - saveCount: 1, - }); - } - } + // 최신 순으로 정렬 후 반환 + return recentCategoryList.reverse(); +}; - return recentCategoriesWithSaveCount; +/** + * 불러온 로그를 바탕으로 카테고리당 저장된 카운트와 함께 배열을 만드는 함수(매번 10개씩 조회한다.) + * @param recentCategoryList + * @param recentCategoriesWithSaveCount + * @param till + * @returns + */ +export const makeCategoryListWithSaveCount = ( + recentCategoryList: RecentCategoryList[], + recentCategoriesWithSaveCount: RecentCategoryListWithSaveCount[], + till: number, +): RecentCategoryListWithSaveCount[] => { + const start: number = till - 10; + const end: number = till; + for (let i = start; i < end && i < recentCategoryList.length; i++) { + const inNewList = recentCategoriesWithSaveCount.find( + (category) => category.categoryId === recentCategoryList[i].categoryId, + ); + if (inNewList) { + inNewList.saveCount++; + } else { + recentCategoriesWithSaveCount.push({ + ...recentCategoryList[i], + saveCount: 1, + }); + } } -} -const findTopParentCategory = ( + return recentCategoriesWithSaveCount; +}; + +export const findTopParentCategory = ( categories: Category[], category: Category, ): number => {