Skip to content

Commit

Permalink
Merge pull request #344 from softeerbootcamp-2nd/dev
Browse files Browse the repository at this point in the history
Recommend System Main Merge
  • Loading branch information
tank3a authored Aug 20, 2023
2 parents dae4ed7 + 812f09a commit 3e57dad
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 23 deletions.
12 changes: 12 additions & 0 deletions backend-recommend/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask, request
import recommend

app = Flask(__name__)

@app.route('/recommend/apriori', methods=['POST'])
def apriori():
data = request.get_json()
return recommend.recByApriori(data)

if __name__ == '__main__':
app.run(port=5001, debug=True)
63 changes: 63 additions & 0 deletions backend-recommend/recommend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pandas as pd
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori, fpgrowth
from mlxtend.frequent_patterns import association_rules
import pymysql
import time
import os
from dotenv import load_dotenv

load_dotenv(verbose=True)

conn = pymysql.connect(host=os.getenv('host'), user=os.getenv('user'), password=os.getenv('password'), db=os.getenv('db'))
cur = conn.cursor()

def recByApriori(body):
start = time.time()
carId = int(body['carId'])
powerTrainId = int(body['powerTrain'])
bodyTypeId = int(body['bodyType'])
operationId = int(body['operation'])

input = []

optionList = body['options']

for i in range(len(optionList)):
input.append(optionList[i]['subOptionId'])

input = set(input)
dataset = []
cur.execute('SELECT hm.history_id, sh.sold_count, sh.sold_options_id FROM SalesHistory sh INNER JOIN HistoryModelMapper hm ON sh.history_id = hm.history_id WHERE sh.car_id = %s AND hm.model_id IN (%s, %s, %s) GROUP BY hm.history_id HAVING COUNT(DISTINCT hm.model_id) = 3;', (carId, powerTrainId, bodyTypeId, operationId))
dbRow = cur.fetchall()
for j in range(len(dbRow)):
oneRow = dbRow[j][2]
if(oneRow == ''):
continue
options = oneRow.split(",")
for i in range(int(dbRow[j][1])):
dataset.append(options)

start = time.time()
te = TransactionEncoder()
te_ary = te.fit(dataset).transform(dataset)
df = pd.DataFrame(te_ary, columns=te.columns_)
frequent_itemsets = fpgrowth(df, min_support=0.05, use_colnames=True)

result_itemsets = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.05)
matching_itemsets = {}

consequent_results = set()

for idx, row in result_itemsets.iterrows():
confidence = row.confidence

if set(row.antecedents).issubset(input) and len(row.consequents) <= 2 and not set(row.antecedents).union(set(row.consequents)).issubset(input):
key = tuple(row.consequents)
if key not in matching_itemsets or confidence > matching_itemsets[key]:
matching_itemsets[key] = confidence

sorted_items = sorted(matching_itemsets.items(), key=lambda x: x[1], reverse=True)
top_items = sorted_items[:4]
top_consequents = [consequent for consequent, _ in top_items]
return top_consequents
1 change: 1 addition & 0 deletions backend-recommend/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Invalid Access
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation group: 'com.h2database', name: 'h2', version: '2.2.220'
implementation 'org.springdoc:springdoc-openapi-ui:1.6.9'
implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'

}

Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/java/autoever2/cartag/OpenApiConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class OpenApiConfig {
public OpenAPI openAPI() {
Info info = new Info()
.version("v1.0.0")
.title("API 타이틀")
.title("A2-CARTAG API 명세서")
.description("API Description");

return new OpenAPI()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package autoever2.cartag.controller;

import autoever2.cartag.service.RecommendService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/recommend")
@RequiredArgsConstructor
public class RecommendController {

private final RecommendService recommendService;

@PostMapping("/list")
public String getRecommendedList() {
return recommendService.getList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package autoever2.cartag.service;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

@Service
public class RecommendService {

@Value("${python.url")
private String requestURL;

//TODO: 응답 존재 안할 시 예외처리
public String getList() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestURL))
.POST(HttpRequest.BodyPublishers.ofString(getJsonFromEstimate())).build();

try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
return body;
} catch (Exception e) {
e.getMessage();
}

return null;
}

public String getJsonFromEstimate() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("carId", 1);
jsonObject.put("powerTrain", 1);
jsonObject.put("bodyType", 3);
jsonObject.put("operation", 5);

JSONArray jsonArray = new JSONArray();
JSONObject subOption = new JSONObject();
subOption.put("subOptionId", 69);
jsonArray.add(subOption);
subOption.put("subOptionId", 70);
jsonArray.add(subOption);

jsonObject.put("options", jsonArray);


return jsonObject.toJSONString();
}
}
24 changes: 24 additions & 0 deletions frontend/src/components/modal/ErrorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { HTMLAttributes } from 'react';
import { DimmedBackground } from './DimmedBackground';
import WhiteModal from './WhiteModal';
import { styled } from 'styled-components';
import { flexCenterCss } from '../../utils/commonStyle';
import { HeadingEn2 } from '../../styles/typefaces';

interface IErrorModal extends HTMLAttributes<HTMLDivElement> {
message: string;
}
export default function ErrorModal({ message, ...props }: IErrorModal) {
return (
<DimmedBackground $displayDimmed={true} {...props}>
<Modal>{message}</Modal>
</DimmedBackground>
);
}

const Modal = styled(WhiteModal)`
${flexCenterCss}
${HeadingEn2}
width: 850px;
height: 520px;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PowerTrainData from '../../components/powerTrainData/PowerTrainData';
import { IHmgData, ModelTypeContext } from '../../context/ModelTypeProvider';
import { MODEL_TYPE_API, IMG_URL } from '../../utils/apis';
import { modelTypeToEn } from '../../utils/constants';
import ErrorModal from '../../components/modal/ErrorModal';

interface IModelTypeDetail {
modelImage: string;
Expand All @@ -20,10 +21,11 @@ interface IModelTypeDetail {
export default function ModelTypePage() {
const { modelType, currentModelTypeIdx, selectedModelType, setSelectedModelType } =
useContext(ModelTypeContext);

const { data: modelTypeDetail, loading: modelTypDetailLoading } = useFetch<IModelTypeDetail>(
`${MODEL_TYPE_API}/detail?modelid=${currentModelTypeIdx}`
);
const {
data: modelTypeDetail,
loading: modelTypDetailLoading,
error: modelTypeDetailError,
} = useFetch<IModelTypeDetail>(`${MODEL_TYPE_API}/detail?modelid=${currentModelTypeIdx}`);

const handleAddImgSrc = useCallback(() => {
if (!modelType || !modelTypeDetail) return;
Expand All @@ -38,7 +40,8 @@ export default function ModelTypePage() {
handleAddImgSrc();
}, [handleAddImgSrc]);

if (!modelType || !modelTypeDetail) return;
if (modelTypeDetailError) return <ErrorModal message={modelTypeDetailError.message} />;
if (!modelType || (!modelTypeDetail && modelTypeDetailError)) return;
const displaySeparator =
modelType[currentModelTypeIdx - 1].hmgData?.maxPs &&
modelType[currentModelTypeIdx - 1].hmgData?.maxKgfm;
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/containers/TrimPage/TrimBannerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function TrimBannerContainer() {
},
[setSelectedImgIdx]
);

const filterImageUrls = (trimData: ICartype[]) => {
trimData.forEach((data) => {
const innerImgUrl = data.innerImage !== '' && `${IMG_URL}${data.innerImage}`;
Expand All @@ -35,10 +36,10 @@ export default function TrimBannerContainer() {
const filteredImagesUrl = [innerImgUrl, outerImgImgUrl, wheelImgUrl].filter(
(url) => url
) as string[];

imageUrls.current.push(...filteredImagesUrl);
});
};

const downloadAndSaveImages = useCallback(async () => {
const imageBlobs = await Promise.all(
imageUrls.current.map(async (url) => {
Expand All @@ -53,24 +54,23 @@ export default function TrimBannerContainer() {
});
setImagesLoading(false);
}, [imageUrls, setImagesLoading]);

const setImages = useCallback(() => {
if (!trimData) return;
imageUrls.current = [];
filterImageUrls(trimData);
downloadAndSaveImages();
}, [trimData, downloadAndSaveImages]);

const displayImages = useCallback(() => {
if (!trimData) return;

const outerUrl = `${IMG_URL}${trimData[selectedTrimIdx].outerImage}`;
const innerUrl = `${IMG_URL}${trimData[selectedTrimIdx].innerImage}`;
const wheelUrl = `${IMG_URL}${trimData[selectedTrimIdx].wheelImage}`;
const imageUrls = [outerUrl, innerUrl, wheelUrl].filter((url) => url);

const imageComponents = imageUrls.map((url, idx) => {
const imgSrc = localStorage.getItem(url);
if (!imgSrc) return;

return (
<ImgWrapper key={idx} $selected={selectedImgIdx === idx}>
<Img $src={imgSrc} onClick={() => handleSelectImg(idx)} />
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ export function useFetch<T>(url: string): FetchResponse<T> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
throw new Error(`Server response was not ok: ${response.status}`);
}

const responseData: T = await response.json();
setData(responseData);
} catch (err) {
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/pages/InnerColorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import InnerColorSelectContainer from '../containers/InnerColorPage/InnerColorSe
import { useFetch } from '../hooks/useFetch';
import { INNER_COLOR_API } from '../utils/apis';
import { IInnerColor, InnerColorContext } from '../context/InnerColorProvider';
import ErrorModal from '../components/modal/ErrorModal';

export default function InnerColorPage() {
const { data: innerColorData } = useFetch<IInnerColor[]>(`${INNER_COLOR_API}?carid=${1}`);
const { data: innerColorData, error } = useFetch<IInnerColor[]>(`${INNER_COLOR_API}?carid=${1}`);
const { setData } = useContext(InnerColorContext);

useEffect(() => {
setData(innerColorData);
}, [innerColorData, setData]);

if (error) {
return <ErrorModal message={error.message} />;
}

return (
<>
<InnerColorBannerContainer />
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/pages/ModelTypePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ import { useFetch } from '../hooks/useFetch';
import { MODEL_TYPE_API } from '../utils/apis';
import { IModelType, ModelTypeContext } from '../context/ModelTypeProvider';
import { CAR_TYPE } from '../utils/constants';
import ErrorModal from '../components/modal/ErrorModal';

export default function ModelTypePage() {
const { data, loading } = useFetch<IModelType[]>(`${MODEL_TYPE_API}/list?carid=${CAR_TYPE}`);
const { data, loading, error } = useFetch<IModelType[]>(
`${MODEL_TYPE_API}/list?carid=${CAR_TYPE}`
);
const { setModelType, setLoading } = useContext(ModelTypeContext);

useEffect(() => {
setModelType(data);
setLoading(loading);
}, [data, loading, setModelType, setLoading]);

if (error) {
<ErrorModal message={error.message} />;
}
return (
<>
<ModelBannerContainer />
Expand Down
22 changes: 16 additions & 6 deletions frontend/src/pages/OptionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import OptionSelectContainer from '../containers/OptionPage/OptionSelectContaine
import OptionFooterContainer from '../containers/OptionPage/OptionFooterContainer';
import { ISubOption, SubOptionContext } from '../context/SubOptionProvider';
import { DefaultOptionContext, IDefaultOption } from '../context/DefaultOptionProvider';
import ErrorModal from '../components/modal/ErrorModal';

interface IOptionDetail {
categoryName: string;
Expand Down Expand Up @@ -42,12 +43,16 @@ export default function OptionPage() {
};
const { currentOptionIdx } = isDefault ? defaultOptionContext : subOptionContext;

const { data: subOption, loading: subOptionLoading } = useFetch<ISubOption[]>(
`${OPTION_API}/sublist?carid=${1}`
);
const { data: defaultOption, loading: defaultOptionLoading } = useFetch<IDefaultOption[]>(
`${OPTION_API}/defaultlist?carid=${1}`
);
const {
data: subOption,
loading: subOptionLoading,
error: subOptionError,
} = useFetch<ISubOption[]>(`${OPTION_API}/sublist?carid=${1}`);
const {
data: defaultOption,
loading: defaultOptionLoading,
error: defaultOptionError,
} = useFetch<IDefaultOption[]>(`${OPTION_API}/defaultlist?carid=${1}`);
const { setSubOption, setSubOptionLoading } = useContext(SubOptionContext);
const { setDefaultOption, setDefaultOptionLoading } = useContext(DefaultOptionContext);
const optionType = isDefault ? 'default' : 'sub';
Expand All @@ -72,6 +77,11 @@ export default function OptionPage() {
`${OPTION_API}/${optionType}/detail/?carid=${1}&optionid=${currentOptionIdx}`
);

if (subOptionError) {
return <ErrorModal message={subOptionError.message} />;
} else if (defaultOptionError) {
return <ErrorModal message={defaultOptionError.message} />;
}
return (
<>
{optionDetail && !optionDetailLoading && (
Expand Down
Loading

0 comments on commit 3e57dad

Please sign in to comment.