Merge pull request #181 from yathoo88/develop_backend_yhs

[BE-template] Modify Recommend template algorithm _ing
This commit is contained in:
HeeseonYoon
2022-10-27 18:51:14 +09:00
committed by GitHub
4 changed files with 353 additions and 58 deletions

View File

@@ -5,7 +5,6 @@ export class CreateDashboardDto {
@IsOptional()
title: string;
@IsString()
@IsNotEmpty()
layout: DashboardLayout[];
}

View File

@@ -18,6 +18,5 @@ export class ItemInfoDto {
this.w = templateItem.width;
this.h = templateItem.height;
this.category = templateItem.recommendCategory;
this.type = templateItem.recommendType;
}
}

View File

@@ -59,7 +59,6 @@ export class TemplateController {
* 템플릿 추천 목록 조회
* @param widgets
*/
//todo:: yhs:: 추천 알고리즘 적용해서 조회해 와야함
@Post('/recommend')
findRecommendAll(@Body() body) {
return this.templateService.findRecommendTemplates(body.widgets);

View File

@@ -3,18 +3,16 @@ import { CreateTemplateDto } from './dto/create-template.dto';
import { UpdateTemplateDto } from './dto/update-template.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Template } from './entities/template.entity';
import { In, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { YesNo } from '../common/enum/yn.enum';
import { TemplateItem } from './entities/template-item.entity';
import { CreateTemplateItemDto } from './dto/create-template-item.dto';
import { UpdateTemplateItemDto } from './dto/update-template-item.dto';
import { TemplateInfoDto } from './dto/template-info.dto';
import { ItemInfoDto } from './dto/item-info.dto';
import { async } from 'rxjs';
import { ResponseStatus } from '../common/enum/response-status.enum';
import { Widget } from '../widget/entities/widget.entity';
import { Component } from '../component/entities/component.entity';
import { ComponentType } from '../common/enum/component-type.enum';
import { DashboardLayout } from '../dashboard/dto/create-dashboard.dto';
@Injectable()
export class TemplateService {
@@ -216,37 +214,19 @@ export class TemplateService {
'component.category as componentCategory',
'component.type as componentType',
])
// .select([
// `sum(case when component.category = 'HORIZONTAL' then 1 else 0 end) as horizontalCnt`,
// `sum(case when component.category = 'VERTICAL' then 1 else 0 end) as verticalCnt`,
// `sum(case when component.category = 'SQUARE' then 1 else 0 end) as squareCnt`,
// `sum(case when component.category = 'SCORE' then 1 else 0 end) as scoreCnt`,
// `sum(1) as widgetCnt`,
// ])
.innerJoin(widgetInfo, 'widget', 'widget.componentId = component.id')
.setParameter('id', widgets)
.getRawMany();
// .getRawOne();
// 템플릿 추천 알고리즘
await this.getRecommendTemplates(widgetList);
const templateComponentInfoList = await this.getRecommendTemplates(widgetList);
const returnArr = [];
const tempTemplateInfo = new TemplateItem();
returnArr.push(tempTemplateInfo);
// 결과값 정렬 순서 변경
const result = templateComponentInfoList.sort(
(a, b) => b.totalRecommendScore - a.totalRecommendScore,
);
const templateList = await this.templateRepository.find({
select: {
id: true,
title: true,
description: true,
},
where: {
useYn: YesNo.YES,
},
});
return { status: ResponseStatus.SUCCESS, data: templateList };
return { status: ResponseStatus.SUCCESS, data: result };
}
/**
@@ -265,13 +245,11 @@ export class TemplateService {
const widgetList = await this.componentRepository
.createQueryBuilder('component')
.select(['widgetInfo.*', 'component.type as componentType'])
.select(['widgetInfo.*', 'component.type as componentType', 'component.category as category'])
.innerJoin(widgetInfo, 'widgetInfo', 'widgetInfo.componentId = component.id')
.setParameter('ids', widgets)
.getRawMany();
// const widgetList = await this.widgetRepository.find({ where: { id: In(widgets) } });
let templateInfo: TemplateInfoDto;
// 템플릿 기본 정보 조회
@@ -296,13 +274,15 @@ export class TemplateService {
},
});
const layout = [];
layoutList.map(item => {
layoutList.forEach(item => {
const itemInfo: ItemInfoDto = new ItemInfoDto(item);
layout.push(itemInfo);
});
templateInfo.layout = layout;
}
templateInfo = await this.mappingLayout(widgetList, templateInfo);
widgetList.forEach((item, i) => {
item.option = JSON.parse(item.option);
// templateInfo.widgets.push(item);
@@ -311,13 +291,6 @@ export class TemplateService {
templateInfo.widgets = widgetList;
//
// widgetList[0].option = JSON.parse(widgetList[0].option);
// widgetList[1].option = JSON.parse(widgetList[1].option);
//
// templateInfo.widgets = [widgetList[0], widgetList[1]];
// templateInfo.layout[0].i = widgetList[0].id;
// templateInfo.layout[1].i = widgetList[1].id;
return { status: ResponseStatus.SUCCESS, data: templateInfo };
}
@@ -337,7 +310,7 @@ export class TemplateService {
widgetCnt: widgetList.length,
};
widgetList.map(item => {
widgetList.forEach(item => {
switch (item.componentCategory) {
case ComponentType.HORIZONTAL:
widgetComponentInfo.horizontalCnt += 1;
@@ -362,15 +335,21 @@ export class TemplateService {
const templates = this.templateRepository
.createQueryBuilder()
.subQuery()
.select(['template.id as id'])
.select([
'template.id as id',
'template.title as title',
'template.description as description',
])
.from(Template, 'template')
.where(`useYn = 'Y'`)
.getQuery();
const templateComponentInfo = await this.templateItemRepository
const templateComponentInfoList = await this.templateItemRepository
.createQueryBuilder('templateItems')
.select([
'template.id as id',
'template.title as title',
'template.description as description',
`sum(case when templateItems.recommendCategory = 'HORIZONTAL' then 1 else 0 end) as horizontalCnt`,
`sum(case when templateItems.recommendCategory = 'VERTICAL' then 1 else 0 end) as verticalCnt`,
`sum(case when templateItems.recommendCategory = 'SQUARE' then 1 else 0 end) as squareCnt`,
@@ -379,9 +358,12 @@ export class TemplateService {
])
.innerJoin(templates, 'template', 'template.id = templateItems.templateId')
.groupBy('template.id')
.addGroupBy('template.title')
.addGroupBy('template.description')
.getRawMany();
templateComponentInfo.forEach(item => {
for (let i = 0; i < templateComponentInfoList.length; i++) {
const item = templateComponentInfoList[i];
// 갯수로 점수 산출
let cntScore = 0;
if (item.totalCnt === widgetComponentInfo.widgetCnt) {
@@ -397,13 +379,34 @@ export class TemplateService {
item.cntScore = cntScore;
// 컴포넌트 타입으로 점수 산출
item.recommendScore = this.calRecommendScore(item, widgetComponentInfo);
});
const scoreItemCnt =
item.totalCnt > widgetComponentInfo.widgetCnt
? widgetComponentInfo.widgetCnt
: item.totalCnt;
const recommendScore = await this.calRecommendScore(item, widgetComponentInfo);
item.recommendScore = recommendScore / scoreItemCnt;
// console.log(templateComponentInfo);
item.totalRecommendScore = cntScore + recommendScore;
}
return templateComponentInfoList;
}
private async calRecommendScore(templateInfo, widgetInfo) {
/**
* 템플릿 아이템 추천 점수 산출
+-------------+----------+--------+------+-----+
|componentType|horizontal|vertical|square|score|
+-------------+----------+--------+------+-----+
|HORIZONTAL |100 |70 |80 |60 |
|VERTICAL |70 |100 |80 |60 |
|SQUARE |90 |90 |100 |90 |
|SCORE |70 |70 |90 |100 |
|TABLE |100 |100 |100 |50 |
+-------------+----------+--------+------+-----+
* @param templateInfo
* @param widgetInfo
* @private
*/
private async calRecommendScore(templateInfo, widgetInfo): Promise<number> {
let recommendScore = 0;
let templateCount = {
@@ -438,16 +441,311 @@ export class TemplateService {
}
}
// 남은 것중에 table 100점 짜리 제거
// restWigetList.map(widget => {
// if (widget.componentCategory === ComponentType.TABLE) {
// console.log('test');
// // componentTypeCount.
// }
// });
// 1. table -> horizontal, vertical, square
if (
widgetCount.TABLE > 0 &&
(templateCount.HORIZONTAL > 0 || templateCount.VERTICAL > 0 || templateCount.SQUARE > 0)
) {
const templateSum = templateCount.HORIZONTAL + templateCount.VERTICAL + templateCount.SQUARE;
const mappingCount = widgetCount.TABLE < templateSum ? widgetCount.TABLE : templateSum;
for (let i = 0; i < mappingCount; i++) {
widgetCount.TABLE -= 1;
if (templateCount.HORIZONTAL > 0) {
templateCount.HORIZONTAL -= 1;
} else if (templateCount.VERTICAL > 0) {
templateCount.VERTICAL -= 1;
} else {
templateCount.SQUARE -= 1;
}
}
recommendScore += 100 * mappingCount;
}
console.log(templateInfo);
// 2. square => any
if (
widgetCount.SQUARE > 0 &&
(templateCount.HORIZONTAL > 0 || templateCount.VERTICAL > 0 || templateCount.SCORE)
) {
const templateSum = templateCount.HORIZONTAL + templateCount.VERTICAL + templateCount.SCORE;
const mappingCount = widgetCount.SQUARE < templateSum ? widgetCount.SQUARE : templateSum;
for (let i = 0; i < mappingCount; i++) {
widgetCount.SQUARE -= 1;
if (templateCount.SCORE > 0) {
templateCount.SCORE -= 1;
} else if (templateCount.VERTICAL > 0) {
templateCount.VERTICAL -= 1;
} else {
templateCount.HORIZONTAL -= 1;
}
}
recommendScore += 90 * mappingCount;
}
// 3. score => square
if (widgetCount.SCORE > 0 && templateCount.SQUARE > 0) {
const templateSum = templateCount.SQUARE;
const mappingCount = widgetCount.SCORE < templateSum ? widgetCount.SCORE : templateSum;
for (let i = 0; i < mappingCount; i++) {
widgetCount.SCORE -= 1;
templateCount.SQUARE -= 1;
}
recommendScore += 90 * mappingCount;
}
// 4. horizontal, vertical => square)
if ((widgetCount.HORIZONTAL > 0 || widgetCount.VERTICAL) && templateCount.SQUARE > 0) {
const templateSum = templateCount.SQUARE;
const horizontalMappingCount =
widgetCount.HORIZONTAL < templateSum ? widgetCount.HORIZONTAL : templateSum;
const verticalMappingCount =
widgetCount.VERTICAL < templateSum - horizontalMappingCount
? widgetCount.VERTICAL
: templateSum - horizontalMappingCount;
for (let i = 0; i < horizontalMappingCount; i++) {
widgetCount.HORIZONTAL -= 1;
templateCount.SQUARE -= 1;
}
for (let i = 0; i < verticalMappingCount; i++) {
widgetCount.VERTICAL -= 1;
templateCount.SQUARE -= 1;
}
recommendScore += 80 * (horizontalMappingCount + verticalMappingCount);
}
// 5-1. horizontal => vaertical
if (widgetCount.HORIZONTAL > 0 && templateCount.VERTICAL > 0) {
const mappingCount =
widgetCount.HORIZONTAL < templateCount.VERTICAL
? widgetCount.HORIZONTAL
: templateCount.VERTICAL;
for (let i = 0; i < mappingCount; i++) {
widgetCount.HORIZONTAL -= 1;
templateCount.VERTICAL -= 1;
}
recommendScore += 70 * mappingCount;
}
// 5-2. vertical -> horizontal
if (widgetCount.VERTICAL > 0 && templateCount.HORIZONTAL > 0) {
const mappingCount =
widgetCount.VERTICAL < templateCount.HORIZONTAL
? widgetCount.VERTICAL
: templateCount.HORIZONTAL;
for (let i = 0; i < mappingCount; i++) {
widgetCount.VERTICAL -= 1;
templateCount.HORIZONTAL -= 1;
}
recommendScore += 70 * mappingCount;
}
// 5-3. score -> horizontal, vertical
if (widgetCount.SCORE > 0 && (templateCount.HORIZONTAL > 0 || templateCount.VERTICAL)) {
const templateSum = templateCount.HORIZONTAL + templateCount.VERTICAL;
const mappingCount = widgetCount.SCORE < templateSum ? widgetCount.SCORE : templateSum;
for (let i = 0; i < mappingCount; i++) {
widgetCount.SCORE -= 1;
if (templateCount.HORIZONTAL > 0) {
templateCount.HORIZONTAL -= 1;
} else {
templateCount.VERTICAL -= 1;
}
}
recommendScore += 70 * mappingCount;
}
// 6. horizontal, viertical => score
if ((widgetCount.HORIZONTAL > 0 || widgetCount.VERTICAL) && templateCount.SCORE > 0) {
const templateSum = templateCount.SCORE;
const horizontalMappingCount =
widgetCount.HORIZONTAL < templateSum ? widgetCount.HORIZONTAL : templateSum;
const verticalMappingCount =
widgetCount.VERTICAL < templateSum - horizontalMappingCount
? widgetCount.VERTICAL
: templateSum - horizontalMappingCount;
for (let i = 0; i < horizontalMappingCount; i++) {
widgetCount.HORIZONTAL -= 1;
templateCount.SCORE -= 1;
}
for (let i = 0; i < verticalMappingCount; i++) {
widgetCount.VERTICAL -= 1;
templateCount.SCORE -= 1;
}
recommendScore += 60 * (horizontalMappingCount + verticalMappingCount);
}
// 7. table => score
if (widgetCount.TABLE > 0 && templateCount.SCORE > 0) {
const mappingCount =
widgetCount.TABLE < templateCount.SCORE ? widgetCount.TABLE : templateCount.SCORE;
for (let i = 0; i < mappingCount; i++) {
widgetCount.TABLE -= 1;
templateCount.SCORE -= 1;
}
recommendScore += 50 * mappingCount;
}
return recommendScore;
}
/**
* layout에 선택한 widget 매핑하기
* @param widgetList
* @param templateInfo
* @private
*/
private async mappingLayout(widgetList, templateInfo) {
const templateItemList = templateInfo.layout;
console.log(templateInfo);
const templateCount = { HORIZONTAL: 0, VERTICAL: 0, SQUARE: 0, SCORE: 0, TABLE: 0, TOTAL: 0 };
templateItemList.forEach(templateItem => {
templateCount[templateItem.category] += 1;
templateCount.TOTAL += 1;
});
const differntWidgetList = [];
// 같은 타입 넣어주기
widgetList.forEach(widget => {
for (let i = 0; i < templateItemList.length; i++) {
if (!templateItemList[i].i && templateItemList[i].category === widget.category) {
templateItemList[i].i = widget.id;
templateCount[widget.category] -= 1;
templateCount.TOTAL -= 1;
break;
} else if (templateCount.TOTAL <= 0 || i === templateItemList.length - 1) {
differntWidgetList.push(widget);
break;
}
}
});
const leastWidgetList = [];
differntWidgetList.forEach(widget => {
if (templateCount.TOTAL > 0) {
//todo:: heeseon::: 이거 순서대로 돌아가는게 아닌거같은데....처음부터 다시해야되나.....
// 다른 타입 점수 높은거 골라서 넣어주기
let index = -1;
if (
widget.category === ComponentType.TABLE &&
(templateCount.HORIZONTAL > 0 || templateCount.VERTICAL > 0 || templateCount.SQUARE > 0)
) {
// 1. table -> horizontal, vertical, square : h->v->s 순서로 우선순위가 존재함.
const horizontalIndex = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.HORIZONTAL,
);
const verticalIndex = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.VERTICAL,
);
const squareIndex = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.SQUARE,
);
if (horizontalIndex >= 0) {
index = horizontalIndex;
templateCount[ComponentType.HORIZONTAL] -= 1;
} else if (verticalIndex >= 0) {
index = verticalIndex;
templateCount[ComponentType.VERTICAL] -= 1;
} else {
index = squareIndex;
templateCount[ComponentType.SQUARE] -= 1;
}
} else if (
widget.category === ComponentType.SQUARE &&
(templateCount.HORIZONTAL > 0 || templateCount.VERTICAL > 0 || templateCount.SCORE > 0)
) {
// 2. square -> any : score->v->h 순서로 우선순위 존재함.
const scoreIndex = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.SCORE,
);
const verticalIndex = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.VERTICAL,
);
const horizontalIndex = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.HORIZONTAL,
);
if (scoreIndex >= 0) {
index = scoreIndex;
templateCount[ComponentType.SCORE] -= 1;
} else if (verticalIndex >= 0) {
index = verticalIndex;
templateCount[ComponentType.VERTICAL] -= 1;
} else {
index = horizontalIndex;
templateCount[ComponentType.HORIZONTAL] -= 1;
}
} else if (
(widget.category === ComponentType.HORIZONTAL ||
widget.category === ComponentType.VERTICAL ||
widget.category === ComponentType.SCORE) &&
templateCount.SQUARE > 0
) {
// 3,4. score, horizontal, vertical -> square
index = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.SQUARE,
);
templateCount[ComponentType.SQUARE] -= 1;
} else if (
(widget.category === ComponentType.HORIZONTAL ||
widget.category === ComponentType.SCORE) &&
templateCount.VERTICAL > 0
) {
// 5-1. horizontal, score -> vertical
index = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.VERTICAL,
);
templateCount[ComponentType.VERTICAL] -= 1;
} else if (
(widget.category === ComponentType.VERTICAL || widget.category === ComponentType.SCORE) &&
templateCount.HORIZONTAL > 0
) {
// 5-2. vertical, score -> HORIZONTAL
index = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.HORIZONTAL,
);
templateCount[ComponentType.HORIZONTAL] -= 1;
} else if (
(widget.category === ComponentType.VERTICAL ||
widget.category === ComponentType.HORIZONTAL) &&
templateCount.SCORE > 0
) {
// 6. vertical, horizontal -> SCORE
index = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.SCORE,
);
templateCount[ComponentType.SCORE] -= 1;
} else if (widget.category === ComponentType.TABLE && templateCount.SCORE > 0) {
// 7. table -> SCORE
index = templateItemList.findIndex(
item => !item.i && item.category === ComponentType.SCORE,
);
templateCount[ComponentType.SCORE] -= 1;
}
templateItemList[index].i = widget.id;
templateCount.TOTAL -= 1;
} else {
console.log(templateInfo);
leastWidgetList.push(widget);
// templateInfo.layout에 새로운 layout object 넣어주기(template에서 넘친 widget 목록)
}
});
leastWidgetList.forEach((leastWidget, index) => {
const layout = new DashboardLayout();
layout.x = 0;
layout.y =
templateItemList[templateItemList.length - 1].y +
templateItemList[templateItemList.length - 1].h +
index * 5;
layout.w = 5;
layout.h = 5;
layout.i = leastWidget.id;
templateItemList.push(layout);
});
return templateInfo;
}
}