<|start_filename|>server/model/reviews.js<|end_filename|>
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const reviewSchema = new Schema({
    _id: Schema.Types.ObjectId,
    bookId: Schema.Types.ObjectId,
    review: { type: String, default: new Object() },
    analysis: Object,
    createdAt: { type: Date, expires: 60 * 60 * 1, default: Date.now },
});

const ReviewCollection = mongoose.model("Review", reviewSchema);

module.exports = ReviewCollection;


<|start_filename|>server/model/wishlist.js<|end_filename|>
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const wishlistSchema = new Schema({
  _id: Schema.Types.ObjectId,
  wishlists: { type: Schema.Types.Array, required: true },
});
//id,password,nickname,wishilist

const WishlistCollection = mongoose.model("Wishlist", wishlistSchema);

module.exports = WishlistCollection;


<|start_filename|>web/src/redux/reducer.js<|end_filename|>
import { LOGIN, LOGOFF, UPDATE_USERDATA } from "./action";
import { UPDATE_LIKEKEYWORD, UPDATE_HATEKEYWORD, SETSEARCHITEM, SETPRODUCTLIST } from "./action";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";

const reducer = (state = { isLogin: false, userData: {}, likeKwd: [], hateKwd: [], productlists: [] }, action) => {
    switch (action.type) {
        case LOGOFF:
            return { ...state, isLogin: false, userData: {} };
        case LOGIN:
            return { ...state, isLogin: true, userData: action.userData };
        case UPDATE_USERDATA:
            return { ...state, userData: action.userData };
        case UPDATE_LIKEKEYWORD:
            return { ...state, likeKwd: action.keywords };
        case UPDATE_HATEKEYWORD:
            return { ...state, hateKwd: action.keywords };
        case SETSEARCHITEM:
            return { ...state, searchItem: action.searchItem };
        case SETPRODUCTLIST:
            return { ...state, productlists: action.productlists };
        default:
            return state;
    }
};

const persistConfig = {
    key: "root",
    storage,
};

export default persistReducer(persistConfig, reducer);


<|start_filename|>server/index.js<|end_filename|>
const express = require("express");
const cors = require("cors");
const model = require("./model");
const authRouter = require("./routes/auth");
const recommendationRouter = require("./routes/recommendation");
const wishlistRouter = require("./routes/wishlist");
const userRouter = require("./routes/user");
const bookRouter = require("./routes/book");
const cookieParser = require("cookie-parser");
const swaggerUi = require("swagger-ui-express");
const bodyParser = require("body-parser");
const swaggerFile = require("./swagger-output");

const app = express();
const port = 5000;
const COOKIE_SECRET = require("./env").COOKIE_SECRET;

app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));
app.use(cookieParser(COOKIE_SECRET));
app.use(cors());

//router
app.use("/auth", authRouter);
app.use("/user", userRouter);
app.use("/wishlist", wishlistRouter);
app.use("/recommendation", recommendationRouter);
app.use("/book", bookRouter);

app.use("/swagger", swaggerUi.serve, swaggerUi.setup(swaggerFile));

//noRouter
app.use((req, res, next) => {
  const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

//error handler
app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = err;
  res.status(500);
  res.send("500", err.message);
});

app.listen(port, () => {
  console.log(`Service running at port ${port}`);
});


<|start_filename|>server/routes/wishlist.js<|end_filename|>
const router = require("express").Router();
const { jwtMiddleware } = require("./middlewares");
const Wishlist = require("../model/wishlist");
const User = require("../model/user");

const findWishlistId = async (id) => {
  const user = await User.findOne({ id });
  return user["wishlist"];
  //wishlistId는 ObjectId타입이므로 주의
};

//도서 찜. 도서 정보 "등록"이라 get보다 post를 쓰는 것이 맞지만 단순 uid만 전송하면 돼서 일단 get방식을 사용
router.get("/:id", jwtMiddleware, async (req, res) => {
  const id = req.userEmail;
  const bookId = req.params.id;
  const wishlistId = findWishlistId(id);
  Wishlist.updateOne({ _id: wishlistId }, { $push: { wishlists: bookId } })
    .then((success) => res.json({ success: true, msg: "찜을 성공했습니다." }))
    .catch((e) =>
      res.status(302).json({ success: false, msg: "찜 추가를 실패했습니다." })
    );
});

//도서 찜 삭제
router.delete("/:id", jwtMiddleware, async (req, res) => {
  const id = req.userEmail;
  const bookId = req.params.id;
  const wishlistId = findWishlistId(id);

  Wishlist.updateOne({ _id: wishlistId }, { $pull: { wishlists: bookId } })
    .then((success) =>
      res.json({ success: true, msg: "찜 삭제를 성공했습니다." })
    )
    .catch((e) =>
      res.status(302).json({ success: false, msg: "찜 삭제를 실패했습니다." })
    );
});

module.exports = router;


<|start_filename|>server/routes/user.js<|end_filename|>
const router = require("express").Router();
const User = require("../model/user");
const Wishlist = require("../model/wishlist");
const SearchKeyword = require("../model/searchKeyword");
const { jwtMiddleware } = require("./middlewares");

const findIds = async (id) => {
  const user = await User.findOne({ id });

  return {
    wishlistId: user["wishlist"],
    searchKeywordId: user["searchKeyword"],
  };
  //wishlistId는 ObjectId타입이므로 주의
};

router.get("/", jwtMiddleware, async (req, res) => {
  const id = req.userEmail;
  const { wishlistId, searchKeywordId } = findIds(id);
  let data;
  try {
    let wishlists = await Wishlist.findOne({ _id: wishlistId });
    let searchkeywords = await SearchKeyword.findOne({ _id: searchKeywordId });
    data = {
      wishlists: wishlists["wishlists"],
      searchkeywords: searchkeywords["keywords"],
    };
    return res.json({
      success: true,
      data: data,
      msg: "데이터 조회에 성공했습니다",
    });
  } catch (e) {
    return res
      .status(302)
      .json({ success: false, msg: "db조회에 실패했습니다" });
  }
});

module.exports = router;


<|start_filename|>labeling-tool/bert/tokenizer_config.json<|end_filename|>
{"do_lower_case": false, "unk_token": "[UNK]", "sep_token": "[SEP]", "pad_token": "[PAD]", "cls_token": "[CLS]", "mask_token": "[MASK]", "tokenize_chinese_chars": true, "strip_accents": null, "do_basic_tokenize": true, "never_split": null, "model_max_length": 512, "special_tokens_map_file": "C:\\Users\\ghks0/.cache\\huggingface\\transformers\\6bddca875f34b8afbae26136b9594ea80793c9598640f0bc94017555a0a1c113.31b83c6ab34462cefd974ed0df8dd4189e7b7b81b47315b7a10627f7ae120002", "name_or_path": "beomi/KcELECTRA-base", "tokenizer_class": "BertTokenizer"}

<|start_filename|>web/src/components/shared/Loading.js<|end_filename|>
const Loading = () => {
  return (
    <div className="full-page center black-3">
      <div className="loader"></div>
      <h5>로딩중</h5>
    </div>
  );
};

export default Loading;


<|start_filename|>web/src/redux/action.js<|end_filename|>
export const LOGIN = "LOGIN",
    LOGOFF = "LOGOFF",
    UPDATE_USERDATA = "UPDATE_USERDATA",
    UPDATE_LIKEKEYWORD = "UPDATE_LIKEKEYWORD",
    UPDATE_HATEKEYWORD = "UPDATE_HATEKEYWORD",
    SETSEARCHITEM = "SETSEARCHITEM",
    SETPRODUCTLIST = "SETPRODUCTLIST";

export const login = (userData) => {
    return {
        type: LOGIN,
        userData,
    };
};

export const logoff = () => {
    return { type: LOGOFF };
};

export const updateUserdata = (userData) => {
    return {
        type: UPDATE_USERDATA,
        userData,
    };
};

export const updateLikeKeyword = (keywords) => {
    return {
        type: UPDATE_LIKEKEYWORD,
        keywords,
    };
};

export const updateHateKeyword = (keywords) => {
    return {
        type: UPDATE_HATEKEYWORD,
        keywords,
    };
};

export const setSearchItem = (searchItem) => {
    return {
        type: SETSEARCHITEM,
        searchItem,
    };
};

export const setProductlist = (productlists) => {
    return {
        type: SETPRODUCTLIST,
        productlists,
    };
};


<|start_filename|>flutter-app/lib/model/model_product.dart<|end_filename|>
class Product {
  String id;
  String title;
  String cover;
  String author;
  String price;

  Product(
      {required this.id,
      required this.title,
      required this.cover,
      required this.author,
      required this.price});

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
        id: json["_id"],
        title: json['title'],
        cover: json['cover'],
        author: json['author'],
        price: json['price']);
  }
}


<|start_filename|>server/routes/middlewares.js<|end_filename|>
const jwt = require("jsonwebtoken");
const JWT_SECRET = require("../env").JWT_SECRET;
const User = require("../model/user");

const jwtMiddleware = async (req, res, next) => {
    let token = req.cookies.x_auth;
    if (!token || token.length == 0) {
        return res.status(501).json({ error: "저장된 token이 없습니다." });
    }
    await jwt.verify(token, JWT_SECRET, async (error, decoded) => {
        if (error) {
            return res.status(500).json({ error: "token을 decode하는데 실패했습니다." });
        }
        console.log(decoded);
        await User.findOne({ id: decoded.id }, (error, user) => {
            if (error) {
                return res.json({
                    error: "DB에서 회원정보를 찾는 도중 오류가 발생했습니다.",
                });
            }
            if (!user) {
                return res.status(400).json({ isAuth: false, error: "token에 해당하는 유저가 없습니다." });
            }
            if (user) {
                req.userEmail = user.id;
                req.name = user.nickname;
            }
        }).catch((e) => {
            console.error(e);
            throw e;
        });
    });
    next();
};

const isLogin = async (req, res, next) => {
    let token = req.cookies.x_auth;
    if (!token || token.length == 0) {
        req.isLogin = false;
    }
    await jwt.verify(token, JWT_SECRET, async (error, decoded) => {
        if (error) {
            return res.status(500).json({ error: "token을 decode하는데 실패했습니다." });
        }
        console.log(decoded);
        await User.findOne({ id: decoded.id }, (error, user) => {
            if (error) {
                return res.json({
                    error: "DB에서 회원정보를 찾는 도중 오류가 발생했습니다.",
                });
            }
            if (!user) {
                return res.status(400).json({ isAuth: false, error: "token에 해당하는 유저가 없습니다." });
            }
            if (user) {
                req.isLogin = true;
                req.userEmail = user.id;
                req.name = user.nickname;
            }
        }).catch((e) => console.error(e));
    });
    next();
};

module.exports = { jwtMiddleware, isLogin };


<|start_filename|>server/env/index.js<|end_filename|>
require("dotenv").config({ path: `${__dirname}/.env` });

const { env } = process;

module.exports = {
    ...env,
};


<|start_filename|>server/routes/recommendation.js<|end_filename|>
const router = require("express").Router();
const User = require("../model/user");
const SearchKeyword = require("../model/searchKeyword");
const { isLogin } = require("./middlewares");
const { get_booklist, get_reviews, get_keywords } = require("../celery/node-tasks");
const Book = require("../model/book");
const Review = require("../model/reviews");

//처음 검색
router.get("/:searchKeyword", async (req, res) => {
    const { searchKeyword } = req.params;
    const books = await get_booklist(searchKeyword);
    let datas = await Book.find({ index: { $in: books } }, { _id: 1, title: 1, itemId: 1 });
    let objectIds = datas.reduce((re, cur) => {
        re.push([cur._id, cur.itemId]);
        return re;
    }, []);
    try {
        arr = [];
        for (let i in objectIds) {
            arr.push(get_reviews(objectIds[i]));
        }
        const result = await Promise.all(arr);

        //arr = [get_keywords(result)];
        let books = objectIds.map((cur) => cur[0]);
        arr = [];
        for (let i in result) {
            arr.push(get_keywords(result[i]));
        }
        const result_ = await Promise.all(arr);

        let keywords = [];
        let reviews = [];
        for (let i in result_) {
            reviews = reviews.concat(result_[i][0]);
            keywords = keywords.concat(result_[i][1]);
        }
        //const result_ = await get_keywords(result);
        Review.insertMany(reviews);

        return res.json({
            success: true,
            keywords: [...new Set(keywords)],
            books: books,
        });
    } catch (e) {
        console.log(e);
        return res.json({
            success: false,
            msg: e,
        });
    }

    // if (req.isLogin) {
    //     const id = req.userEmail;
    //     const user = await User.findOne({ id });
    //     SearchKeyword.updateOne({ _id: user["searchKeyword"] }, { $push: { keywords: search } });
    //     //여기서 이제
    //     // 1. 유사한 책 찾아오기
    //     // 2. 키워드 반환.
    //     // 키워드 반환
    // } //로그인 되어 있을 시 searchKeyword에 추가

    /*Update this.rabbitmq 연결하고 키워드 받아오는 거 추가*/
});

//키워드 선택
router.post("/keywords", async (req, res) => {
    //[1순위,2순위,3순위,4순위,5순위]이렇게 해서 보내주기!
    const { books, keywords } = req.body;

    let datas = await Review.find({ bookId: { $in: books } }, { _id: 0, bookId: 1, analysis: 1 });
    let score = books.reduce((re, cur) => {
        re[cur] = keywords.reduce((re, cur) => {
            re[cur] = { POS: 0, NEG: 0 };
            return re;
        }, {});
        return re;
    }, {});

    datas.forEach((i) => {
        for (const j in i.analysis) {
            if (keywords.includes(j)) {
                score[i.bookId][j]["NEG"] += i.analysis[j]["NEG"];
                score[i.bookId][j]["POS"] += i.analysis[j]["POS"];
            }
        }
    });

    let result = [];

    for (const book in score) {
        point = 0;
        for (let i = 0; i < keywords.length; i++) {
            vect = 0;
            let POS = score[book][keywords[i]]["POS"];
            let NEG = score[book][keywords[i]]["NEG"];
            if (POS == 0 && NEG == 0) {
                vect = 0;
            } else vect = (POS - NEG) / (POS + NEG);
            point = point + vect * (100000 / 10 ** (i + 1));
        }
        let temp = new Object();
        temp[book] = point;
        result.push(temp);
    }

    function compare(a, b) {
        a_score = Object.values(a)[0];
        b_score = Object.values(b)[0];
        if (a_score < b_score) {
            return 1;
        }
        if (a_score > b_score) {
            return -1;
        }
        return 0;
    }
    result.sort(compare);
    const result_ = result.map((i) => {
        return Object.keys(i)[0];
    });

    result = [];
    const msg = await Promise.all(
        result_.map((i) => {
            const t = Book.findById(i);
            return t;
        })
    );

    return res.json({
        success: true,
        data: msg,
    });
});

module.exports = router;


<|start_filename|>web/src/utils/hooks.js<|end_filename|>
import { useState, useCallback } from "react";
import { useEffect } from "react";

export function useInput(initValue) {
  const [values, setValues] = useState(initValue);

  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    setValues((values) => ({ ...values, [name]: value }));
  }, []);

  const onFileChange = useCallback((e) => {
    setValues((values) => ({ ...values, ["file"]: e.taraget.file[0] }));
  }, []);

  const reset = useCallback(() => {
    setValues(initValue);
  }, [initValue]);
  return { values, onChange, onFileChange, reset };
}
//form field 입력받을 때 사용하는 훅.

export function useKeywords(mode, initValue) {
  const [values, setValues] = useState({ [mode]: initValue });

  const addKeyword = useCallback(
    (keyword) => {
      setValues((values) => ({ [mode]: [...values[mode], keyword] }));
    },
    [mode]
  );

  const deleteKeyword = useCallback(
    (deleteKeyword) => {
      let modified = values[mode];
      modified = modified.filter((keyword) => keyword !== deleteKeyword);
      setValues((values) => ({ ...values, [mode]: modified }));
    },
    [mode, values]
  );

  const reset = useCallback(() => setValues({}), []);

  return { values, addKeyword, deleteKeyword, reset };
}
//선택한 키워드 저장할 때 사용하는 훅.
//선호 키워드할 때는 mode prop을 'like'
//불호 키워드할 때는 mode props을 'hate'로 전달해주면 됨.

//!!구현해야 할 것. 다음 페이지 넘어갈 시 reset 호출 됨.
// => 리덕스에 키워드 저장할 것
// 새로운 검색 시 리덕스 값들 초기화하는 것 잊지 말 것!
export function useScript(url) {
  useEffect(()=> {
    const script = document.createElement('script');
    script.src = url;
    script.async = true;

    document.body.appendChild(script);

    return ()=> {
      document.body.removeChild(script);
    }
  }, [url]);
}

<|start_filename|>web/src/components/layout/Content.js<|end_filename|>
import "./Content.css";

export default function Content({ children }) {
  return <div className="main-content">{children}</div>;
}


<|start_filename|>server/routes/book.js<|end_filename|>
const router = require("express").Router();
const Book = require("../model/book");
const Review = require("../model/reviews");
const ObjectId = require("mongoose").Types.ObjectId;

router.get("/:bookId", async (req, res) => {
    const bookId = ObjectId(req.params.bookId);
    try {
        const t = await Book.findOne({ _id: bookId });
        const p = await Review.find({ bookId: bookId }, { _id: 0, review: 1, analysis: 1 });
        return res.json({
            success: true,
            msg: "책 정보 조회 성공",
            data: {
                book_info: t,
                reviews: p,
            },
        });
    } catch (e) {
        res.status(302).json({ success: false, msg: "책 정보 조회 실패" });
    }
});

module.exports = router;


<|start_filename|>server/celery/node-tasks.js<|end_filename|>
const celery = require("celery-node");
const { create } = require("../model/user");

const client = celery.createClient("amqp://localhost", "redis://", "general");
const analysis_client = celery.createClient("amqp://localhost", "redis://", "analysis");

async function get_booklist(searchKeyword) {
    const task = client.createTask("tasks.multi");
    const result = task.applyAsync([searchKeyword]);
    const data = await result.get();
    return data;
}

async function get_reviews(itemId) {
    const task = client.createTask("tasks.crawl");
    const result = task.applyAsync([itemId]);
    const data = await result.get();
    return data;
}

async function get_keywords(books) {
    const task = analysis_client.createTask("analysis-task.analysis_");
    const result = task.applyAsync([books]);
    const data = await result.get();
    return data;
}

module.exports = { get_booklist: get_booklist, get_reviews: get_reviews, get_keywords: get_keywords };


<|start_filename|>flutter-app/lib/screen/screen_keyword.dart<|end_filename|>
import 'dart:async';
import 'package:app/model/model_keyword.dart';
import 'package:app/screen/screen_home.dart';
import 'package:app/screen/screen_product.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class KeywordScreen extends StatefulWidget {
  String searchKeyword;
  KeywordScreen({required this.searchKeyword});

  @override
  _KeywordScreen createState() => _KeywordScreen();
}

class _KeywordScreen extends State<KeywordScreen> {
  List<int> selectedKeyword = [];
  late Future<Keyword> keyword;
  late Keyword oriKeyword;

  @override
  void initState() {
    super.initState();
    keyword = _fetchKeyword();
  }

  Future<Keyword> _fetchKeyword() async {
    final res = await http.get(Uri.parse(
        'http://172.16.58.3:5000/recommendation/' + widget.searchKeyword));

    if (res.statusCode == 200) {
      var result = Keyword.fromJson(json.decode(utf8.decode(res.bodyBytes)));
      oriKeyword = result;
      if (oriKeyword.books.isEmpty) {
        Timer(const Duration(milliseconds: 2000), () {
          Navigator.push(
              context, MaterialPageRoute(builder: (context) => HomeScreen()));
        });
      }
      return result;
    } else {
      throw Exception('Failed to load post');
    }
  }

  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double width = screenSize.width;
    double height = screenSize.height;

    return WillPopScope(
        onWillPop: () {
          return Future(() => false);
        },
        child: SafeArea(
            child: Scaffold(
                appBar: AppBar(
                    title: const Text("키북키북"),
                    backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                    leading: Container()),
                body: Center(
                    child: FutureBuilder<Keyword>(
                  future: keyword,
                  builder: (context, snapshot) {
                    if (snapshot.hasData) {
                      if (snapshot.data!.books.isEmpty) {
                        return const Text("결과가 존재하지 않습니다.");
                      }
                      return Column(children: [
                        Container(
                            margin: const EdgeInsets.all(10),
                            child: const Text(
                              "선택된 키워드",
                              style: TextStyle(fontSize: 20),
                            )),
                        SizedBox(
                            //margin: const EdgeInsets.fromLTRB(10, 0, 10, 0),
                            height: 50,
                            child: ListView.separated(
                              scrollDirection: Axis.horizontal,
                              itemBuilder: (context, index) {
                                if (selectedKeyword.contains(index)) {
                                  return Container(
                                    margin: const EdgeInsets.fromLTRB(
                                        10, 10, 10, 10),
                                    child: ElevatedButton(
                                        style: ButtonStyle(backgroundColor:
                                            MaterialStateProperty.resolveWith(
                                                (states) {
                                          return const Color.fromRGBO(
                                              0x78, 0x5D, 0x12, 1);
                                        })),
                                        onPressed: () {
                                          setState(() {
                                            selectedKeyword.remove(index);
                                          });
                                        },
                                        child: Text(oriKeyword.keywords[index]
                                            .toString())),
                                  );
                                }
                                return const SizedBox.shrink();
                              },
                              itemCount: oriKeyword.keywords.length,
                              separatorBuilder:
                                  (BuildContext context, int index) =>
                                      const Divider(),
                            )),
                        SizedBox(
                            height: height * 0.65,
                            child: ListView.separated(
                                addRepaintBoundaries: false,
                                itemBuilder: (context, index) {
                                  if (!selectedKeyword.contains(index)) {
                                    return ElevatedButton(
                                        style: ButtonStyle(backgroundColor:
                                            MaterialStateProperty.resolveWith(
                                                (states) {
                                          return Colors.white;
                                        })),
                                        onPressed: () {
                                          setState(() {
                                            selectedKeyword.add(index);
                                          });
                                        },
                                        child: Text(
                                            snapshot.data!.keywords[index]
                                                .toString(),
                                            style: const TextStyle(
                                                color: Color.fromRGBO(
                                                    138, 138, 138, 1))));
                                  }
                                  return Visibility(
                                    child: Container(),
                                    visible: false,
                                  );
                                },
                                itemCount: snapshot.data!.keywords.length,
                                separatorBuilder:
                                    (BuildContext context, int index) {
                                  if (selectedKeyword.contains(index)) {
                                    return Visibility(
                                      child: Container(),
                                      visible: false,
                                    );
                                  }
                                  return const Divider(
                                    color: Colors.white,
                                  );
                                })),
                        Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              SizedBox(
                                width: width * 0.35,
                                child: ElevatedButton(
                                    style: ButtonStyle(backgroundColor:
                                        MaterialStateProperty.resolveWith(
                                            (states) {
                                      return Colors.white;
                                    })),
                                    onPressed: () {
                                      setState(() {
                                        selectedKeyword.clear();
                                      });
                                    },
                                    child: const Text(
                                      "초기화",
                                      style: TextStyle(
                                          color: Color.fromRGBO(
                                              0x78, 0x5D, 0x12, 1)),
                                    )),
                              ),
                              const Padding(padding: EdgeInsets.all(10)),
                              SizedBox(
                                  width: width * 0.35,
                                  child: ElevatedButton(
                                      style: ButtonStyle(backgroundColor:
                                          MaterialStateProperty.resolveWith(
                                              (states) {
                                        return const Color.fromRGBO(
                                            0x78, 0x5D, 0x12, 1);
                                      })),
                                      onPressed: () {
                                        List<String> keywords_ = [];
                                        for (int i = 0;
                                            i < selectedKeyword.length;
                                            i++) {
                                          keywords_.add(oriKeyword
                                              .keywords[selectedKeyword[i]]);
                                        }
                                        Navigator.push(
                                            context,
                                            MaterialPageRoute(
                                                builder: (context) =>
                                                    ProductScreen(
                                                        keywords: keywords_,
                                                        books:
                                                            oriKeyword.books)));
                                      },
                                      child: Text("결과보기")))
                            ])
                      ]);
                    } else if (snapshot.hasError) {
                      return Text("${snapshot.error}");
                    }

                    return const CircularProgressIndicator(
                      color: Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                    );
                  },
                )),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {
                    Navigator.push(context,
                        MaterialPageRoute(builder: (context) => HomeScreen()));
                  },
                  child: const Icon(Icons.arrow_back),
                  backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                ))));
  }
}


<|start_filename|>server/routes/auth.js<|end_filename|>
const router = require("express").Router();
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const JWT_SECRET = require("../env").JWT_SECRET;
const User = require("../model/user");
const Wishlist = require("../model/wishlist");
const SearchKeyword = require("../model/searchKeyword");
const { jwtMiddleware } = require("./middlewares");
const ObjectId = require("mongoose").Types.ObjectId;

const hashingPwd = async (password) => {
    let genSalt = await bcrypt.genSalt(10);
    let hashing = await bcrypt.hash(password, genSalt);
    return hashing;
}; //비밀번호 암호화

router.get("/", (req, res) => {
    res.send("Auth router test");
});

//회원가입
router.post("/register", (req, res) => {
    User.findOne({ id: req.body.id }).then(async (user) => {
        if (user) {
            return res.status(404).json({ msg: "해당 이메일을 가진 사용자가 이미 존재합니다." });
        } else {
            const searchKeywordId = new ObjectId();
            const wishlistId = new ObjectId();
            const newUser = new User({
                _id: new ObjectId(),
                id: req.body.id,
                nickname: req.body.nickname,
                password: <PASSWORD>,
                searchKeyword: searchKeywordId,
                wishlist: wishlistId,
            });

            const wishlist = new Wishlist({
                _id: wishlistId,
                wishlists: [],
            });
            const searchKeyword = new SearchKeyword({
                _id: searchKeywordId,
                keywords: [],
            });
            await hashingPwd(newUser.password)
                .then((hashing) => {
                    newUser.password = hashing;
                })
                .catch((err) => console.log("password hashing err :" + err));

            try {
                wishlist.save();
                searchKeyword.save();
                newUser.save();
                return res.json({ success: true, user: { id: req.body.id, nickname: req.body.nickname } });
            } catch (e) {
                return res.status(302).json({ success: false, msg: "db생성 실패" });
            }
        }
    });
});

//로그인
router.post("/login", (req, res) => {
    const id = req.body.id;
    const password = req.body.password;

    User.findOne({ id }).then((user) => {
        if (!user) {
            const error = "해당하는 회원이 존재하지 않습니다.";
            return res.status(401).json({ errors: error });
        }

        bcrypt.compare(password, user.password).then((isMatch) => {
            if (isMatch) {
                const payload = { id: user.id, nickname: user.nickname };

                jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" }, (err, token) => {
                    return res
                        .cookie("x_auth", token, { maxAge: 1000 * 60 * 60 * 24 * 7 })
                        .status(200)
                        .json({
                            success: true,
                            userData: { id: user.id, nickname: user.nickname },
                        });
                });
            } else {
                return res.status(400).json({ error: "패스워드가 일치하지 않습니다." });
            }
        });
    });
});

//로그아웃
router.get("/logout", jwtMiddleware, (req, res) => {
    res.clearCookie("x_auth");
    return res.json({ success: true });
});

//로그인 상태 확인
router.get("/status", jwtMiddleware, (req, res) => {
    return res.json({
        success: true,
        userData: { id: req.id, nickname: req.nickname },
    });
});

module.exports = router;


<|start_filename|>server/model/book.js<|end_filename|>
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const bookSchema = new Schema({
    _id: Schema.Types.ObjectId,
    title: { type: String, required: true },
    isbn: String,
    isbn13: String,
    itemId: String,
    intro: String,
    contents: String,
    author: String,
    price: String,
    index: Number,
});
//id,password,nickname,<PASSWORD>ist

const BookCollection = mongoose.model("Book", bookSchema);

module.exports = BookCollection;


<|start_filename|>flutter-app/lib/model/model_keyword.dart<|end_filename|>
class Keyword {
  List<String> keywords;
  List<String> books;

  Keyword({required this.keywords, required this.books});

  factory Keyword.fromJson(Map<String, dynamic> json) {
    try {
      return Keyword(
          keywords: json['keywords'].cast<String>(),
          books: json['books'].cast<String>());
    } on Error catch (_, ex) {
      return Keyword(keywords: [], books: []);
    }
  }
}


<|start_filename|>server/model/index.js<|end_filename|>
const mongoose = require("mongoose");
const root = encodeURI("root");

mongoose.connect(`mongodb://localhost:27017/book`, function (err, res) {
  if (res) {
    console.log(`DB연결성공 ${res}`);
  }
  if (err) {
    console.log(`DB연결실패 ${err}`);
  }
});


<|start_filename|>flutter-app/lib/model/model_detail.dart<|end_filename|>
class Detail {
  String title;
  String intro;
  String contents;
  String author;
  String price;
  List<Map<String, dynamic>> reveiws;

  Detail(
      {required this.title,
      required this.intro,
      required this.contents,
      required this.author,
      required this.price,
      required this.reveiws});

  factory Detail.fromJson(Map<String, dynamic> json) {
    var intro = "";
    var contents = "";
    var bookInfo = json['book_info'];
    List<Map<String, dynamic>> reviews =
        json['reviews'].cast<Map<String, dynamic>>();

    try {
      intro = bookInfo['intro'];
    } on Error catch (_, err) {}

    try {
      contents = bookInfo['contents'];
    } on Error catch (_, err) {}

    return Detail(
        title: bookInfo['title'],
        intro: intro,
        contents: contents,
        author: bookInfo['author'],
        price: bookInfo['price'],
        reveiws: reviews);
  }
}


<|start_filename|>flutter-app/lib/screen/screen_home.dart<|end_filename|>
import 'package:app/screen/screen_keyword.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  String searchKeyword = "";

  Future<bool> _onWillPop() async {
    return (await showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('Are you sure?'),
            content: const Text('Do you want to exit an App'),
            actions: <Widget>[
              TextButton(
                onPressed: () => Navigator.of(context).pop(false),
                child: const Text('No'),
              ),
              TextButton(
                onPressed: () =>
                    SystemChannels.platform.invokeMethod('SystemNavigator.pop'),
                child: const Text('Yes'),
              ),
            ],
          ),
        )) ??
        false;
  }

  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double width = screenSize.width;
    double height = screenSize.height;

    return WillPopScope(
        onWillPop: _onWillPop,
        child: SafeArea(
            child: Scaffold(
          appBar: AppBar(
              title: const Text("키북키북"),
              backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
              leading: Container()),
          body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                Container(
                    margin: EdgeInsets.fromLTRB(0, 0, 0, height * 0.03),
                    child: const Center(
                        child: Text("저격당하실분?",
                            style: TextStyle(
                              fontSize: 40,
                              fontWeight: FontWeight.bold,
                            )))),
                Container(
                    margin: EdgeInsets.fromLTRB(0, 0, 0, height * 0.1),
                    child: const Center(
                        child: Text("도서 취향 저격..",
                            style:
                                TextStyle(fontSize: 15, color: Colors.grey)))),
                SizedBox(
                  width: width * 0.8,
                  child: TextField(
                    cursorColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                    style: const TextStyle(
                        color: Color.fromRGBO(0x78, 0x5D, 0x12, 1)),
                    onChanged: (text) {
                      setState(() {
                        searchKeyword = text;
                      });
                    },
                    decoration: const InputDecoration(
                        enabledBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.all(Radius.circular(10.0)),
                          borderSide: BorderSide(color: Colors.grey),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.all(Radius.circular(10.0)),
                          borderSide: BorderSide(
                              color: Color.fromRGBO(0x78, 0x5D, 0x12, 1)),
                        ),
                        border: OutlineInputBorder(),
                        labelText: '찾고싶은 책의 키워드를 입력하세요.',
                        labelStyle: TextStyle(
                            color: Color.fromRGBO(0x78, 0x5D, 0x12, 1))),
                  ),
                ),
                SizedBox(
                    width: width * 0.8,
                    child: ElevatedButton.icon(
                      style: ButtonStyle(
                          shape:
                              MaterialStateProperty.all<RoundedRectangleBorder>(
                                  RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(10.0),
                            //side: BorderSide(color: Colors.red) // border line color
                          )),
                          backgroundColor:
                              MaterialStateProperty.resolveWith((states) {
                            return const Color.fromRGBO(0x78, 0x5D, 0x12, 1);
                          })),
                      onPressed: () {
                        Navigator.push(
                            context,
                            MaterialPageRoute(
                                builder: (context) => KeywordScreen(
                                      searchKeyword: searchKeyword,
                                    )));
                      },
                      icon: const Icon(Icons.search, size: 18),
                      label: const Text("책 추천 받기"),
                    )),
                Padding(
                  padding: EdgeInsets.fromLTRB(0, 0, 0, height * 0.1),
                )
              ])),
        )));
  }
}


<|start_filename|>flutter-app/lib/screen/screen_detail.dart<|end_filename|>
import 'package:app/model/model_detail.dart';
import 'package:app/model/model_product.dart';
import 'package:app/screen/screen_home.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class DetailScreen extends StatefulWidget {
  late String bookId;
  String tempCover;
  DetailScreen({required this.bookId, required this.tempCover});

  @override
  _DetailScreen createState() => _DetailScreen();
}

class _DetailScreen extends State<DetailScreen> {
  late Future<Detail> detail;

  @override
  void initState() {
    super.initState();
    detail = _fetchDetail();
  }

  Future<Detail> _fetchDetail() async {
    final res = await http
        .get(Uri.parse('http://172.16.17.32:5000/book/' + widget.bookId));

    if (res.statusCode == 200) {
      var result =
          Detail.fromJson(json.decode(utf8.decode(res.bodyBytes))['data']);
      return result;
    } else {
      throw Exception('Failed to load post');
    }
  }

  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double width = screenSize.width;
    double height = screenSize.height;

    return WillPopScope(
        onWillPop: () {
          return Future(() => false);
        },
        child: SafeArea(
            child: Scaffold(
                appBar: AppBar(
                    title: const Text("키북키북"),
                    backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                    leading: Container()),
                body: Center(
                    child: FutureBuilder<Detail>(
                  future: detail,
                  builder: (context, snapshot) {
                    if (snapshot.hasData) {
                      return Container(
                          margin: EdgeInsets.fromLTRB(
                              width * 0.05, height * 0.05, width * 0.05, 0),
                          child: Column(children: [
                            SizedBox(
                                height: height * 0.55,
                                child: Column(children: [
                                  SizedBox(
                                      height: height * 0.3,
                                      child: Image.network(widget.tempCover)),
                                  Padding(
                                      padding: EdgeInsets.all(height * 0.02)),
                                  Container(
                                      margin: const EdgeInsets.fromLTRB(
                                          10, 10, 10, 10),
                                      child: Text(
                                          snapshot.data!.title.toString(),
                                          style:
                                              const TextStyle(fontSize: 25))),
                                  Container(
                                      margin: const EdgeInsets.fromLTRB(
                                          10, 10, 10, 10),
                                      child: Text(
                                          snapshot.data!.intro.isEmpty
                                              ? "책 소개가 존재하지 않습니다."
                                              : snapshot.data!.intro.toString(),
                                          style:
                                              const TextStyle(fontSize: 15))),
                                  Container(
                                      margin:
                                          const EdgeInsets.fromLTRB(5, 5, 5, 5),
                                      child: Text(
                                          snapshot.data!.author.toString(),
                                          style:
                                              const TextStyle(fontSize: 15))),
                                  Container(
                                      margin:
                                          const EdgeInsets.fromLTRB(5, 5, 5, 5),
                                      child: Text(
                                          snapshot.data!.price.toString() + "원",
                                          style: const TextStyle(fontSize: 15)))
                                ])),
                            Expanded(
                                child: ListView.separated(
                              scrollDirection: Axis.vertical,
                              itemBuilder: (context, index) {
                                return Container(
                                  margin:
                                      const EdgeInsets.fromLTRB(10, 0, 10, 10),
                                  child: Text(snapshot
                                      .data!.reveiws[index]['review']
                                      .toString()),
                                );
                              },
                              itemCount: snapshot.data!.reveiws.length,
                              separatorBuilder:
                                  (BuildContext context, int index) =>
                                      const Divider(),
                            ))
                          ]));
                    } else if (snapshot.hasError) {
                      return Text("${snapshot.error}");
                    }

                    return const CircularProgressIndicator(
                      color: Colors.deepPurple,
                    );
                  },
                )),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {
                    Navigator.of(context).pop(true);
                  },
                  child: const Icon(Icons.arrow_back),
                  backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                ))));
  }
}


<|start_filename|>server/model/searchKeyword.js<|end_filename|>
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const searchSchema = new Schema({
  _id: Schema.Types.ObjectId,
  keywords: { type: Schema.Types.Array, required: true },
});
//id,password,nickname,wishilist

const SearchKeywordCollection = mongoose.model("SearchKeyword", searchSchema);

module.exports = SearchKeywordCollection;


<|start_filename|>flutter-app/lib/screen/screen_product.dart<|end_filename|>
import 'package:app/model/model_product.dart';
import 'package:app/screen/screen_detail.dart';
import 'package:app/screen/screen_home.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class ProductScreen extends StatefulWidget {
  List<String> books;
  List<String> keywords;
  ProductScreen({required this.books, required this.keywords});

  @override
  _ProductScreen createState() => _ProductScreen();
}

class _ProductScreen extends State<ProductScreen> {
  late Future<List<Product>> products;

  @override
  void initState() {
    super.initState();
    products = _fetchProducts();
  }

  Future<List<Product>> _fetchProducts() async {
    Map data = {"keywords": widget.keywords, "books": widget.books};
    var body = json.encode(data);

    final res = await http.post(
        Uri.parse('http://172.16.58.3:5000/recommendation/keywords'),
        headers: {"Content-Type": "application/json"},
        body: body);

    if (res.statusCode == 200) {
      final parsed = json
          .decode(utf8.decode(res.bodyBytes))['data']
          .cast<Map<String, dynamic>>();
      var result =
          parsed.map<Product>((json) => Product.fromJson(json)).toList();
      return result;
    } else {
      throw Exception('Failed to load post');
    }
  }

  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double width = screenSize.width;
    double height = screenSize.height;

    return WillPopScope(
        onWillPop: () {
          return Future(() => false);
        },
        child: SafeArea(
            child: Scaffold(
                appBar: AppBar(
                    title: const Text("키북키북"),
                    backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                    leading: Container()),
                body: Center(
                    child: FutureBuilder<List<Product>>(
                  future: products,
                  builder: (context, snapshot) {
                    if (snapshot.hasData) {
                      return Column(children: [
                        Container(
                            alignment: Alignment.centerLeft,
                            margin: const EdgeInsets.fromLTRB(30, 15, 15, 15),
                            child: const Text("추천 도서",
                                style: TextStyle(
                                  color: Colors.black,
                                  fontWeight: FontWeight.w500,
                                  fontSize: 35,
                                ))),
                        Expanded(
                          child: ListView.separated(
                            itemBuilder: (context, index) {
                              return GestureDetector(
                                  onTap: () {
                                    Navigator.push(
                                        context,
                                        MaterialPageRoute(
                                            builder: (context) => DetailScreen(
                                                  bookId:
                                                      snapshot.data![index].id,
                                                  tempCover: snapshot
                                                      .data![index].cover
                                                      .toString(),
                                                )));
                                  },
                                  child: Container(
                                      margin: const EdgeInsets.fromLTRB(
                                          10, 10, 10, 10),
                                      child: Row(children: <Widget>[
                                        SizedBox(
                                            width: width * 0.3,
                                            //height: height * 0.10,
                                            child: Image.network(snapshot
                                                .data![index].cover
                                                .toString())),
                                        Container(
                                            height: height * 0.15,
                                            margin: EdgeInsets.fromLTRB(
                                                width * 0.05,
                                                height * 0.01,
                                                0,
                                                0),
                                            width: width * 0.5,
                                            child: Column(children: [
                                              Container(
                                                  alignment:
                                                      Alignment.centerLeft,
                                                  child: Text(
                                                      snapshot
                                                          .data![index].title
                                                          .toString(),
                                                      style: const TextStyle(
                                                          fontSize: 15,
                                                          fontWeight: FontWeight
                                                              .bold))),
                                              Container(
                                                  alignment:
                                                      Alignment.centerLeft,
                                                  margin: EdgeInsets.fromLTRB(
                                                      0,
                                                      height * 0.01,
                                                      0,
                                                      height * 0.01),
                                                  child: Text(
                                                      snapshot
                                                          .data![index].author
                                                          .toString(),
                                                      style: const TextStyle(
                                                          fontSize: 15))),
                                              Container(
                                                  alignment:
                                                      Alignment.centerLeft,
                                                  child: Text(
                                                      snapshot.data![index]
                                                              .price
                                                              .toString() +
                                                          "원",
                                                      style: const TextStyle(
                                                          fontSize: 15)))
                                            ]))
                                      ])));
                            },
                            itemCount: snapshot.data!.length,
                            separatorBuilder:
                                (BuildContext context, int index) =>
                                    const Divider(),
                          ),
                        )
                      ]);
                    } else if (snapshot.hasError) {
                      return Text("${snapshot.error}");
                    }

                    return const CircularProgressIndicator(
                      color: Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                    );
                  },
                )),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {
                    Navigator.push(context,
                        MaterialPageRoute(builder: (context) => HomeScreen()));
                  },
                  child: const Icon(Icons.arrow_back),
                  backgroundColor: const Color.fromRGBO(0x78, 0x5D, 0x12, 1),
                ))));
  }
}


<|start_filename|>web/src/components/container/withAuth.js<|end_filename|>
import { connect } from "react-redux";
import { login, logoff } from "../../redux/action";

const mapStateToProps = (state) => ({ isLogin: state.isLogin, userData: state.userData });
const mapDispatchToProps = (dispatch) => ({
    login: (userData) => dispatch(login(userData)),
    logoff: () => dispatch(logoff()),
});

export default function Container(wrappedComponent) {
    return connect(mapStateToProps, mapDispatchToProps)(wrappedComponent);
}


<|start_filename|>web/src/components/layout/Nav.css<|end_filename|>
ul{
    list-style-type: none;
    display: flex;
    justify-content: flex-end;
    
}

li{
    display: inline;
    color:white;
}

li:before{
    content:"";
    height:auto;
    padding-right:30px;
}

a{
    text-decoration:none;
    color:white;
}

nav{
    padding:5px 250px;
    background-color: #0A1B62;
    display: flex;
    justify-content: space-between;
}

li:first-child{
    margin:0;
    border:0;
}

<|start_filename|>server/model/user.js<|end_filename|>
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const userSchema = new Schema({
  _id: Schema.Types.ObjectId,
  id: { type: String, unique: true, required: true },
  password: { type: String, required: true },
  nickname: { type: String, required: true },
  wishlist: { type: Schema.Types.ObjectId, ref: "Wishlist" },
  searchKeyword: { type: Schema.Types.ObjectId, ref: "SearchKeyword" },
});
//id,password,nickname,wishilist

const UserCollection = mongoose.model("User", userSchema);

module.exports = UserCollection;


<|start_filename|>server/swagger.js<|end_filename|>
const swaggerAutogen = require("swagger-autogen")();

const options = {
  definition: {
    openapi: "3.0.0",
    info: {
      title: "도서추천서비스 API",
      version: "0.1.0",
      description: "익스프레스로 만들어진 간단한 CRUD api",
      license: { name: "MIT", url: "https://spdx.org/licenses/MIT.html" },
      contact: {
        name: "도서추천서비스",
        url: "http://localhost:3000",
        email: "<EMAIL>",
      },
    },
    // servers: [{}],
  },
  apis: [
    "./routes/auth.js",
    "./routes/book.js",
    "./routes/recommendation.js",
    "./routes/user.js",
    "./routes/wishlist.js",
  ],
};

const outputFile = "./swagger-output.json";
const endpointsFiles = ["./index.js"];

swaggerAutogen(outputFile, endpointsFiles, options);
