├── Back-End
├── nodemon.json
├── src
│ ├── api
│ │ ├── Cart
│ │ │ ├── seeCart
│ │ │ │ ├── seeCart.graphql
│ │ │ │ └── seeCart.js
│ │ │ ├── deleteCart
│ │ │ │ ├── deleteCart.graphql
│ │ │ │ └── deleteCart.js
│ │ │ ├── editCart
│ │ │ │ ├── editCart.graphql
│ │ │ │ └── editCart.js
│ │ │ ├── addCart
│ │ │ │ ├── addCart.graphql
│ │ │ │ └── addCart.js
│ │ │ └── Cart.js
│ │ ├── User
│ │ │ ├── me
│ │ │ │ ├── me.graphql
│ │ │ │ └── me.js
│ │ │ ├── login
│ │ │ │ ├── login.graphql
│ │ │ │ └── login.js
│ │ │ ├── User.js
│ │ │ ├── editUser
│ │ │ │ ├── editUser.graphql
│ │ │ │ └── editUser.js
│ │ │ └── createAccount
│ │ │ │ ├── createAccount.graphql
│ │ │ │ └── createAccount.js
│ │ ├── Payment
│ │ │ ├── deletePayment
│ │ │ │ ├── deletePayment.graphql
│ │ │ │ └── deletePayment.js
│ │ │ ├── seePayment
│ │ │ │ ├── seePayment.graphql
│ │ │ │ └── seePayment.js
│ │ │ ├── addPayment
│ │ │ │ ├── addPayment.graphql
│ │ │ │ └── addPayment.js
│ │ │ └── Payment.js
│ │ ├── Product
│ │ │ ├── deleteProduct
│ │ │ │ ├── deleteProduct.graphql
│ │ │ │ └── deleteProduct.js
│ │ │ ├── Upload
│ │ │ │ ├── upload.graphql
│ │ │ │ └── upload.js
│ │ │ ├── Product.js
│ │ │ ├── seeProduct
│ │ │ │ ├── seeProduct.graphql
│ │ │ │ └── seeProduct.js
│ │ │ └── editProduct
│ │ │ │ ├── editProduct.graphql
│ │ │ │ └── editProduct.js
│ │ ├── BuyList
│ │ │ ├── seeBuyList
│ │ │ │ ├── seeBuyList.graphql
│ │ │ │ └── seeBuyList.js
│ │ │ ├── addBuyList
│ │ │ │ ├── addBuyList.graphql
│ │ │ │ └── addBuyList.js
│ │ │ └── BuyList.js
│ │ └── models.graphql
│ ├── env.js
│ ├── utils.js
│ ├── middlewares.js
│ ├── server.js
│ ├── Schema.js
│ └── passport.js
├── .babelrc
├── package.json
├── .gitignore
└── datamodel.prisma
├── Design
├── Page
│ ├── 1_Main.md
│ ├── 2_Login.md
│ ├── 4_Admin.md
│ ├── 5_Store.md
│ ├── 7_Order.md
│ ├── 8_MyPage.md
│ ├── 3_SignUp.md
│ └── 6_ProductView.md
├── img
│ ├── Admin.png
│ ├── Login.png
│ ├── Main.png
│ ├── MyPage.png
│ ├── Order.png
│ ├── SignUp.png
│ ├── Store.png
│ └── ProductView.png
└── Function
│ └── 1_functionDesign.md
├── Front-End
├── src
│ ├── Routes
│ │ ├── Auth
│ │ │ ├── index.js
│ │ │ ├── AuthQueries.js
│ │ │ ├── AuthPresenter.js
│ │ │ └── AuthContainer.js
│ │ ├── Admin
│ │ │ ├── index.js
│ │ │ ├── AdminQueries.js
│ │ │ └── AdminPresenter.js
│ │ ├── Main
│ │ │ ├── index.js
│ │ │ ├── MainQueries.js
│ │ │ ├── MainContainer.js
│ │ │ └── MainPresenter.js
│ │ ├── Store
│ │ │ ├── index.js
│ │ │ ├── StoreQueries.js
│ │ │ ├── StorePresenter.js
│ │ │ └── StoreContainer.js
│ │ ├── Mypage
│ │ │ ├── index.js
│ │ │ ├── MyPageQueries.js
│ │ │ └── MyPagePresenter.js
│ │ ├── Payment
│ │ │ ├── index.js
│ │ │ ├── PaymentQueries.js
│ │ │ ├── PaymentPresenter.js
│ │ │ └── PaymentContainer.js
│ │ └── Product
│ │ │ ├── index.js
│ │ │ └── ProductQueries.js
│ ├── Components
│ │ ├── SharedFunction.js
│ │ ├── DaumAPI.js
│ │ ├── Button.js
│ │ ├── Loader.js
│ │ ├── Input.js
│ │ ├── header.js
│ │ ├── Footer.js
│ │ ├── ItemBox.js
│ │ ├── SharedQueries.js
│ │ ├── Routes.js
│ │ ├── App.js
│ │ ├── Navigator.js
│ │ ├── StoreContent.js
│ │ ├── SignUpForm.js
│ │ ├── Icons.js
│ │ ├── EditProduct.js
│ │ ├── DaumPostCode.js
│ │ ├── ProductTable.js
│ │ └── ProductEditForm.js
│ ├── Hooks
│ │ └── useInput.js
│ ├── Apollo
│ │ ├── Client.js
│ │ └── LocalState.js
│ ├── index.js
│ ├── Styles
│ │ ├── Theme.js
│ │ └── GlobalStyles.js
│ └── Firebase
│ │ └── index.js
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ ├── slick
│ │ ├── slick.min.css
│ │ └── slick-theme.min.css
│ └── index.html
├── .gitignore
├── package.json
└── README.md
└── README.md
/Back-End/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "ext": "js graphql"
3 | }
--------------------------------------------------------------------------------
/Design/Page/1_Main.md:
--------------------------------------------------------------------------------
1 | # Main Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/Page/2_Login.md:
--------------------------------------------------------------------------------
1 | # Login Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/Page/4_Admin.md:
--------------------------------------------------------------------------------
1 | # Admin Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/Page/5_Store.md:
--------------------------------------------------------------------------------
1 | # Store Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/Page/7_Order.md:
--------------------------------------------------------------------------------
1 | # Order Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/Page/8_MyPage.md:
--------------------------------------------------------------------------------
1 | # MyPage
2 |
3 |
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/seeCart/seeCart.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | seeCart: [Cart!]!
3 | }
--------------------------------------------------------------------------------
/Design/Page/3_SignUp.md:
--------------------------------------------------------------------------------
1 | # SignUp Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/img/Admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/Admin.png
--------------------------------------------------------------------------------
/Design/img/Login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/Login.png
--------------------------------------------------------------------------------
/Design/img/Main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/Main.png
--------------------------------------------------------------------------------
/Design/img/MyPage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/MyPage.png
--------------------------------------------------------------------------------
/Design/img/Order.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/Order.png
--------------------------------------------------------------------------------
/Design/img/SignUp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/SignUp.png
--------------------------------------------------------------------------------
/Design/img/Store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/Store.png
--------------------------------------------------------------------------------
/Design/Page/6_ProductView.md:
--------------------------------------------------------------------------------
1 | # ProductView Page
2 |
3 |
--------------------------------------------------------------------------------
/Design/img/ProductView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Design/img/ProductView.png
--------------------------------------------------------------------------------
/Front-End/src/Routes/Auth/index.js:
--------------------------------------------------------------------------------
1 | import AppContainer from "./AuthContainer";
2 | export default AppContainer;
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/deleteCart/deleteCart.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | deleteCart(id: [String!]!) : Boolean!
3 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/editCart/editCart.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | editCart(id: String! count: Int!): Cart!
3 | }
--------------------------------------------------------------------------------
/Front-End/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woochan94/REACT_ChanStyle/HEAD/Front-End/public/favicon.ico
--------------------------------------------------------------------------------
/Front-End/src/Routes/Admin/index.js:
--------------------------------------------------------------------------------
1 | import AdminContainer from "./AdminContainer";
2 | export default AdminContainer;
--------------------------------------------------------------------------------
/Front-End/src/Routes/Main/index.js:
--------------------------------------------------------------------------------
1 | import MainContainer from "./MainContainer";
2 |
3 | export default MainContainer;
--------------------------------------------------------------------------------
/Front-End/src/Routes/Store/index.js:
--------------------------------------------------------------------------------
1 | import StoreContainer from "./StoreContainer";
2 | export default StoreContainer;
--------------------------------------------------------------------------------
/Back-End/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["@babel/plugin-transform-runtime"]
4 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/me/me.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | me: User!
3 | }
4 |
5 | type Mutation {
6 | me: User!
7 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Mypage/index.js:
--------------------------------------------------------------------------------
1 | import MyPageContainer from "./MyPageContainer";
2 | export default MyPageContainer;
--------------------------------------------------------------------------------
/Back-End/src/api/User/login/login.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | login(email: String!, password: String!): String!
3 | }
4 |
--------------------------------------------------------------------------------
/Front-End/src/Routes/Payment/index.js:
--------------------------------------------------------------------------------
1 | import PaymentContainer from "./PaymentContainer";
2 | export default PaymentContainer;
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/deletePayment/deletePayment.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | deletePayment(id: [String!]!) : Boolean!
3 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Product/index.js:
--------------------------------------------------------------------------------
1 | import ProductContainer from "./ProductContainer";
2 |
3 | export default ProductContainer;
--------------------------------------------------------------------------------
/Back-End/src/env.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | import path from "path";
3 |
4 | dotenv.config({ path: path.resolve(__dirname, ".env")});
--------------------------------------------------------------------------------
/Back-End/src/utils.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | export const generateToken = (id) => jwt.sign({ id }, process.env.JWT_SECRET);
4 |
--------------------------------------------------------------------------------
/Back-End/src/api/Product/deleteProduct/deleteProduct.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | deleteProduct (
3 | id: String!
4 | ): Boolean!
5 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/seePayment/seePayment.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | seePayment: [Payment!]!
3 | }
4 |
5 | type Mutation {
6 | seePayment: [Payment!]!
7 | }
--------------------------------------------------------------------------------
/Back-End/src/api/BuyList/seeBuyList/seeBuyList.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | seeBuyList: [BuyList!]!
3 | }
4 |
5 | type Mutation {
6 | seeBuyList2(first: Int, skip: Int): [BuyList!]!
7 | }
--------------------------------------------------------------------------------
/Back-End/src/middlewares.js:
--------------------------------------------------------------------------------
1 | export const isAuthenticated = (request) => {
2 | if(!request.user) {
3 | throw Error('You need to log in to perform this action');
4 | }
5 | return;
6 | }
--------------------------------------------------------------------------------
/Front-End/src/Components/SharedFunction.js:
--------------------------------------------------------------------------------
1 | export const addComma = (num) => {
2 | var regexp = /\B(?=(\d{3})+(?!\d))/g; // 1000의 자리마다 ,를 찍어주는 정규식
3 | return num.toString().replace(regexp, ',');
4 | }
--------------------------------------------------------------------------------
/Back-End/src/api/BuyList/addBuyList/addBuyList.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | addBuyList(
3 | product: [String]
4 | size: [String]
5 | color: [String]
6 | quantity: [Int]
7 | ):Boolean!
8 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/addCart/addCart.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | addCart(
3 | product: [String!]
4 | sizeId: [String!]
5 | colorId: [String!]
6 | stockId: [String!]
7 | count: [Int!]
8 | ):Boolean!
9 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/User.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../generated/prisma-client";
2 |
3 | export default {
4 | User: {
5 | cart: ({ id }) => prisma.user({ id }).cart(),
6 | buyList: ({ id }) => prisma.user({ id }).buyList()
7 | }
8 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/addPayment/addPayment.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | addPayment(
3 | product: [String!]
4 | size: [String!]
5 | color: [String!]
6 | stock: [String!]
7 | count: [Int!]
8 | cart: [String!]
9 | ):Boolean!
10 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/editUser/editUser.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | editUser(
3 | name: String
4 | zipCode: String
5 | address: String
6 | addressDetail: String
7 | phone: String
8 | password: String
9 | confirmPassword: String
10 | ): Boolean!
11 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/createAccount/createAccount.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | createAccount(
3 | name: String!
4 | email: String!
5 | password: String!
6 | zipCode: String!
7 | address: String!
8 | addressDetail: String!
9 | phone: String!
10 | ): Boolean!
11 | }
--------------------------------------------------------------------------------
/Front-End/src/Hooks/useInput.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default defaultValue => {
4 | const [value, setValue] = useState(defaultValue);
5 |
6 | const onChange = e => {
7 | const {
8 | target: {value}
9 | } = e;
10 | setValue(value);
11 | }
12 |
13 | return { value, setValue, onChange };
14 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Main/MainQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const MAIN_SEEITEM = gql`
4 | query seeproduct($sort: String!) {
5 | seeproduct(sort: $sort) {
6 | id
7 | name
8 | price
9 | files {
10 | id
11 | url
12 | }
13 | }
14 | }
15 | `;
--------------------------------------------------------------------------------
/Front-End/src/Apollo/Client.js:
--------------------------------------------------------------------------------
1 | import ApolloClient from "apollo-boost";
2 | import { defaults, resolvers } from "./LocalState";
3 |
4 | export default new ApolloClient({
5 | uri: "http://localhost:4000/",
6 | clientState: {
7 | defaults,
8 | resolvers
9 | },
10 | headers: {
11 | Authorization: `Bearer ${localStorage.getItem("token")}`
12 | }
13 | })
--------------------------------------------------------------------------------
/Front-End/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Front-End/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './Components/App';
4 | import Client from "./Apollo/Client";
5 | import { ApolloProvider } from "react-apollo-hooks";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 |
--------------------------------------------------------------------------------
/Front-End/src/Components/DaumAPI.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PostCode from "./DaumPostCode";
3 |
4 | export default ({ isOpen, handleAddress }) => {
5 |
6 | let dialog = ()
7 |
8 | if(!isOpen) {
9 | dialog = null;
10 | }
11 | return (
12 | <>
13 | {dialog}
14 | >
15 | )
16 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/Upload/upload.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | upload(
3 | name: String!
4 | price: Int!
5 | mainCategory: String!
6 | subCategory: String!
7 | files: [String!]!
8 | productDetailFiles: [String!]
9 | productSizeFiles: [String!]
10 | sizes: [String!]!
11 | colors: [String!]!
12 | stocks: [Int!]!
13 | ): Product!
14 | }
--------------------------------------------------------------------------------
/Front-End/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
--------------------------------------------------------------------------------
/Back-End/src/api/BuyList/BuyList.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../generated/prisma-client";
2 |
3 | export default {
4 | BuyList: {
5 | product: ({ id }) => prisma.buyList({ id }).product(),
6 | user: ({ id }) => prisma.buyList({ id }).user(),
7 | size: ({ id }) => prisma.buyList({ id }).size(),
8 | color: ({ id }) => prisma.buyList({ id }).color(),
9 | quantity: ({ id }) => prisma.buyList({ id }).quantity()
10 | }
11 | }
--------------------------------------------------------------------------------
/Front-End/src/Styles/Theme.js:
--------------------------------------------------------------------------------
1 | const boxBorder = "1px solid #e6e6e6";
2 | const borderRadius = "4px";
3 |
4 | export default {
5 | maxWidth: "1000px",
6 | bgColor: "#FAFAFA",
7 | whiteColor: "#fff",
8 | boxBorder,
9 | borderRadius,
10 | borderBottom:"1px solid #a9a9a9",
11 | titleFontSize: "32px",
12 | confirmColor: "#2c3e50",
13 | whiteBox: `border-radius:${borderRadius};
14 | border:${boxBorder};
15 | background-color: white;`
16 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/Cart.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../generated/prisma-client";
2 |
3 | export default {
4 | Cart: {
5 | user: ({ id }) => prisma.cart({ id }).user(),
6 | product: ({ id }) => prisma.cart({ id }).product(),
7 | sizeId: ({ id }) => prisma.cart({ id }).sizeId(),
8 | colorId: ({ id }) => prisma.cart({ id }).colorId(),
9 | stockId: ({ id }) => prisma.cart({ id }).stockId(),
10 | count: ({ id }) => prisma.cart({ id }).count()
11 | }
12 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/deleteProduct/deleteProduct.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | deleteProduct: async (_, args) => {
6 | const { id } = args;
7 | try {
8 | await prisma.deleteProduct({
9 | id
10 | });
11 | return true;
12 | } catch {
13 | return false;
14 | }
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/seeCart/seeCart.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Query: {
5 | seeCart: (_, __, { request , isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { user } = request;
8 | return prisma.carts({
9 | where: {
10 | user: {
11 | id: user.id
12 | }
13 | }
14 | })
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/editCart/editCart.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | editCart: (_, args, { request, isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { id, count } = args;
8 | return prisma.updateCart({
9 | where: {
10 | id
11 | },
12 | data: {
13 | count
14 | }
15 | })
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/Product.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../generated/prisma-client";
2 |
3 | export default {
4 | Product: {
5 | files: ({ id }) => prisma.product({ id }).files(),
6 | sizes: ({ id }) => prisma.product({ id }).sizes(),
7 | colors: ({ id }) => prisma.product({ id }).colors(),
8 | stocks: ({ id }) => prisma.product({ id }).stocks(),
9 | productDetailFile: ({ id }) => prisma.product({ id }).productDetailFile(),
10 | productSizeFile: ({ id }) => prisma.product({ id }).productSizeFile()
11 | }
12 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/Payment.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../generated/prisma-client";
2 |
3 | export default {
4 | Payment: {
5 | user: ({ id }) => prisma.payment({ id }).user(),
6 | product: ({ id }) => prisma.payment({ id }).product(),
7 | size: ({ id }) => prisma.payment({ id }).size(),
8 | color: ({ id }) => prisma.payment({ id }).color(),
9 | stock: ({ id }) => prisma.payment({ id }).stock(),
10 | count: ({ id }) => prisma.payment({ id }).count(),
11 | cart: ({ id }) => prisma.payment({ id }).cart()
12 | }
13 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/me/me.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Query: {
5 | me:(_, __, { request, isAuthenticated}) => {
6 | isAuthenticated(request);
7 | const { user } = request;
8 | return prisma.user({ id: user.id });
9 | }
10 | },
11 | Mutation: {
12 | me:(_, __, { request, isAuthenticated}) => {
13 | isAuthenticated(request);
14 | const { user } = request;
15 | return prisma.user({ id: user.id });
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/Back-End/src/server.js:
--------------------------------------------------------------------------------
1 | import "./env";
2 | import { GraphQLServer } from "graphql-yoga";
3 | import logger from "morgan";
4 | import schema from "./Schema";
5 | import "./passport";
6 | import { authenticateJwt } from "./passport";
7 | import { isAuthenticated } from './middlewares';
8 |
9 |
10 | const server = new GraphQLServer({
11 | schema,
12 | context: ({request}) => ({ request, isAuthenticated })
13 | });
14 |
15 | server.express.use(logger("dev"));
16 | server.express.use(authenticateJwt);
17 |
18 | server.start(() => console.log(`✅ Server is running on localhost:${process.env.PORT}`));
19 |
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/deleteCart/deleteCart.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | deleteCart: async (_, args, { request, isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { id } = args;
8 | try {
9 | await id.map(async(item) => {
10 | await prisma.deleteCart({
11 | id: item
12 | })
13 | })
14 | return true;
15 | } catch {
16 | return false;
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/seeProduct/seeProduct.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | seeproduct(
3 | id: String
4 | sort: String
5 | mainCategory: String
6 | subCategory: String
7 | ): [Product!]!
8 | }
9 |
10 | type Mutation {
11 | seeProductBest(
12 | id: String
13 | sort: String!
14 | mainCategory: String
15 | subCategory: String
16 | ): [Product!]!
17 | }
18 |
19 | type Mutation {
20 | seeProductAll(
21 | id: String
22 | sort: String!
23 | mainCategory: String
24 | subCategory: String
25 | first: Int
26 | skip: Int
27 | ): [Product!]!
28 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/deletePayment/deletePayment.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | deletePayment: async(_, args, { request, isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { id } = args;
8 | try {
9 | await id.map(async(item) => {
10 | await prisma.deletePayment({
11 | id: item
12 | })
13 | })
14 | return true;
15 | } catch {
16 | return false;
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/Front-End/src/Firebase/index.js:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import 'firebase/storage';
3 |
4 | // Your web app's Firebase configuration
5 | var firebaseConfig = {
6 | apiKey: process.env.REACT_APP_APIKEY,
7 | authDomain: process.env.REACT_APP_AUTHDOMAIN,
8 | databaseURL: process.env.REACT_APP_DATABASEURL,
9 | projectId: process.env.REACT_APP_PROJECTID,
10 | storageBucket: process.env.REACT_APP_STOREBUCKET,
11 | messagingSenderId: process.env.REACT_APP_MESSAGINGSENERID,
12 | appId: process.env.REACT_APP_APPID
13 | };
14 | // Initialize Firebase
15 | firebase.initializeApp(firebaseConfig);
16 |
17 | const storage = firebase.storage();
18 |
19 | export { storage, firebase as default };
--------------------------------------------------------------------------------
/Front-End/src/Apollo/LocalState.js:
--------------------------------------------------------------------------------
1 | export const defaults = {
2 | // localStorage에서 token의 유무에 따라 로그인 여부 판단해줌
3 | isLoggedIn: Boolean(localStorage.getItem("token")) || false
4 | }
5 |
6 | export const resolvers = {
7 | Mutation: {
8 | logUserIn: (_, { token }, { cache }) => {
9 | localStorage.setItem("token", token);
10 | cache.writeData({
11 | data: {
12 | isLoggedIn: true
13 | }
14 | });
15 | return null;
16 | },
17 | logUserOut: (_, __, { cache }) => {
18 | localStorage.removeItem("token");
19 | window.location.reload();
20 | return null;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/Front-End/src/Components/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 |
5 | const Container = styled.button`
6 | width: 100%;
7 | border: 0;
8 | border-radius: ${props => props.theme.borderRadius};
9 | text-align: center;
10 | cursor: pointer;
11 | padding: 10px;
12 | font-size: 16px;
13 | `;
14 |
15 | const Button = ({ text, onClick, type = "button", id }) => {text}
16 |
17 | Button.propTypes = {
18 | text: PropTypes.string.isRequired,
19 | onClick: PropTypes.func,
20 | type: PropTypes.string,
21 | id: PropTypes.string
22 | };
23 |
24 | export default Button;
--------------------------------------------------------------------------------
/Front-End/src/Styles/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 | import reset from "styled-reset";
3 |
4 | export default createGlobalStyle`
5 | ${reset};
6 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700');
7 | * {
8 | box-sizing: border-box;
9 | }
10 | body {
11 | font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
12 | padding-top: 123px;
13 | background-color: ${props => props.theme.bgColor};
14 | @media (max-width: 600px) {
15 | padding-top: 160px;
16 | }
17 | }
18 | a {
19 | text-decoration: none;
20 | color: black;
21 | }
22 | `;
--------------------------------------------------------------------------------
/Front-End/src/Components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled, { keyframes } from "styled-components";
3 | import { Logo } from "./Icons";
4 |
5 | const Animation = keyframes`
6 | 0%{
7 | opacity:0
8 | }
9 | 50%{
10 | opacity:1
11 | }
12 | 100%{
13 | opacity:0;
14 | }
15 | `;
16 |
17 | const Loader = styled.div`
18 | display: flex;
19 | animation: ${Animation} 1s linear infinite;
20 | width: 100%;
21 | justify-content: center;
22 | align-items: center;
23 | min-height: 76vh;
24 | @media (max-width: 768px) {
25 | min-height: 73vh;
26 | }
27 | @media (max-width: 600px) {
28 | min-height: 68vh;
29 | }
30 | @media (max-width: 480px) {
31 | min-height: 64vh;
32 | }
33 | `;
34 |
35 | export default () => (
36 |
37 |
38 |
39 | )
--------------------------------------------------------------------------------
/Back-End/src/Schema.js:
--------------------------------------------------------------------------------
1 | // 모든 파일들을 이 파일(schema.js)에서 합칠것임
2 | import path from "path";
3 | import {makeExecutableSchema}from "graphql-tools";
4 | import { mergeTypes, mergeResolvers, fileLoader } from 'merge-graphql-schemas';
5 |
6 | // fileLoader 함수를 사용하여 지정된 폴더에서 모든 파일을 가져온다.
7 | // fileLoader를 통해 typeDefs와 resolvers를 모두 가져올 것이다 (.graphql 파일, .js 파일)
8 | // 때문에 api 폴더 밑에는 resolver이나 graphql이 아닌 파일을 두면 안된다.
9 | // 예를 들어 resolver이 아닌 js파일이 api폴더 밑에 생성된다면 현재파일(schema.js)에서 그 js파일을
10 | // resolver로 인식하기 때문에 에러가 발생할 수 있음
11 | const allTypes = fileLoader(path.join(__dirname, "/api/**/*.graphql"));
12 | const allResolvers = fileLoader(path.join(__dirname, "/api/**/*.js"));
13 |
14 | // makeExecutableSchema를 이용해서 GraphQL Schema를 만들 수 있음 ( typeDefs, resolvers )
15 | const schema = makeExecutableSchema({
16 | typeDefs: mergeTypes(allTypes),
17 | resolvers: mergeResolvers(allResolvers)
18 | });
19 |
20 | export default schema;
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/seePayment/seePayment.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Query: {
5 | seePayment: (_, __, { request, isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { user } = request;
8 | return prisma.payments({
9 | where: {
10 | user: {
11 | id: user.id
12 | }
13 | }
14 | })
15 | }
16 | },
17 | Mutation: {
18 | seePayment: (_, __, { request, isAuthenticated}) => {
19 | isAuthenticated(request);
20 | const { user } = request;
21 | return prisma.payments({
22 | where: {
23 | user: {
24 | id: user.id
25 | }
26 | }
27 | })
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Store/StoreQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const SEE_ALL_BESTITEM = gql`
4 | mutation seeProductBest($sort: String!, $mainCategory: String, $subCategory: String) {
5 | seeProductBest(sort: $sort, mainCategory: $mainCategory, subCategory: $subCategory) {
6 | id
7 | name
8 | price
9 | files {
10 | id
11 | url
12 | }
13 | }
14 | }
15 | `;
16 |
17 | export const SEE_PRODUCT = gql`
18 | mutation seeProductAll($id: String, $sort: String!, $mainCategory: String, $subCategory: String, $first: Int, $skip: Int) {
19 | seeProductAll(id:$id, sort:$sort, mainCategory:$mainCategory, subCategory:$subCategory, first:$first, skip:$skip) {
20 | id
21 | name
22 | price
23 | files {
24 | id
25 | url
26 | }
27 | }
28 | }
29 | `;
--------------------------------------------------------------------------------
/Front-End/src/Components/Input.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 |
5 | const Container = styled.input`
6 | border: 0;
7 | border: ${props => props.theme.boxBorder};
8 | border-radius: ${props => props.theme.borderRadius};
9 | padding: 10px;
10 | `;
11 |
12 | const Input = ({
13 | placeholder,
14 | required = true,
15 | value,
16 | onChange,
17 | type = "text",
18 | id,
19 | defaultValue
20 | }) => ()
29 |
30 | Input.propTypes = {
31 | placeholder: PropTypes.string,
32 | require: PropTypes.bool,
33 | value: PropTypes.string,
34 | onChange: PropTypes.func,
35 | type: PropTypes.string,
36 | id: PropTypes.string
37 | }
38 |
39 | export default Input;
--------------------------------------------------------------------------------
/Back-End/src/api/BuyList/seeBuyList/seeBuyList.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Query: {
5 | seeBuyList: (_, __, { request, isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { user } = request;
8 | return prisma.buyLists({
9 | where: {
10 | user: {
11 | id: user.id
12 | }
13 | }
14 | })
15 | }
16 | },
17 | Mutation: {
18 | seeBuyList2: (_, args, { request, isAuthenticated }) => {
19 | isAuthenticated(request);
20 | const { user } = request;
21 | const { first, skip } = args;
22 | return prisma.buyLists({
23 | where: {
24 | user: {
25 | id: user.id
26 | }
27 | },
28 | first,
29 | skip
30 | })
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Auth/AuthQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const LOG_IN = gql`
4 | mutation login($email: String!, $password: String!) {
5 | login(email: $email, password: $password)
6 | }
7 | `;
8 |
9 | export const CREATE_ACCOUNT = gql`
10 | mutation createAccount(
11 | $name: String!,
12 | $email: String!,
13 | $password: String!,
14 | $zipCode: String!,
15 | $address: String!,
16 | $addressDetail: String!,
17 | $phone: String!
18 | ) {
19 | createAccount(
20 | name: $name,
21 | email: $email,
22 | password: $password,
23 | zipCode: $zipCode,
24 | address: $address,
25 | addressDetail: $addressDetail,
26 | phone: $phone
27 | )
28 | }
29 | `;
30 |
31 | // LocalState에 작성한 Local용 mutation을 통해 token을 생성하는 query문
32 | export const LOCAL_LOG_IN = gql`
33 | mutation logUserIn($token: String!) {
34 | logUserIn(token: $token) @client
35 | }
36 | `;
--------------------------------------------------------------------------------
/Front-End/src/Components/header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 |
5 | const Header = styled.header`
6 | width: 100%;
7 | position: fixed;
8 | top: 0;
9 | left: 0;
10 | border-bottom: ${props => props.theme.boxBorder};
11 | background-color: black;
12 | z-index: 9999;
13 | `;
14 |
15 | const HeaderWrapper = styled.div`
16 | width: 100%;
17 | max-width: ${props => props.theme.maxWidth};
18 | margin: 0 auto;
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 | @media (max-width: 600px) {
23 | height:120px;
24 | }
25 | `;
26 |
27 | const Logo = styled.h1`
28 | padding: 25px 0;
29 | font-size: 32px;
30 | font-weight: 600;
31 | a {
32 | color: white;
33 | }
34 | `;
35 |
36 | export default () => {
37 | return (
38 |
39 |
40 |
41 | CHANSTYLE
42 |
43 |
44 |
45 | )
46 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/login/login.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 | import { generateToken } from '../../../utils';
3 | import bcrypt from 'bcrypt';
4 |
5 | // 비밀번호 정규식 설정 (영문, 숫자, 특수문자 조합, 8~16자리)
6 | const chk_password = /^(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9])(?=.*[0-9]).{8,16}$/;
7 |
8 | export default {
9 | Mutation: {
10 | login: async (_, args) => {
11 | const { email, password } = args;
12 | const user = await prisma.user({ email });
13 | if( user !== null ) {
14 | // 비밀번호 정규식 확인
15 | if (chk_password.test(password) === false) {
16 | throw Error("비밀번호는 영문, 숫자, 특수문자 조합의 8~16자리여야 합니다.");
17 | }
18 | // 비밀번호 암호화 확인
19 | if(bcrypt.compareSync(password, user.password)) {
20 | return generateToken(user.id);
21 | } else {
22 | throw Error("Wrong email/Password combination");
23 | }
24 | } else {
25 | throw Error("존재하지 않는 이메일입니다.");
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Back-End/src/passport.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 | import { Strategy, ExtractJwt } from "passport-jwt";
3 | import { prisma } from './../generated/prisma-client/';
4 |
5 | const jwtOptions = {
6 | // Authorization 헤더에서 jwt를 찾는 역할
7 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
8 | secretOrKey: process.env.JWT_SECRET
9 | };
10 |
11 | const verifyUser = async (payload, done) => {
12 | try {
13 | const user = await prisma.user({ id: payload.id });
14 | if (user !== null) {
15 | return done(null, user);
16 | } else {
17 | return done(null, false);
18 | }
19 | } catch (err) {
20 | return done(err, false);
21 | }
22 | }
23 |
24 | export const authenticateJwt = (req, res, next) => passport.authenticate("jwt", { sessions: false }, (err, user) => {
25 | if(user) {
26 | // verifyUser에서 사용자를 받아온 후, 사용자 정보를 req객체에 붙여주는 역할
27 | req.user = user;
28 | }
29 | next();
30 | })(req, res, next);
31 |
32 | // JWT는 토큰을 입력받아서 정보를 해석한 후 해석된 정보를 콜백 함수로 전달해 준다.
33 | // strategy가 모든 작업을 한 후 결과물을 payload에 전달해줌
34 | passport.use(new Strategy(jwtOptions, verifyUser));
35 | passport.initialize();
--------------------------------------------------------------------------------
/Back-End/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Back-End",
3 | "version": "1.0.0",
4 | "description": "ChanStyle Backend with Prisma + GrpahQL + Express ",
5 | "main": "index.js",
6 | "license": "MIT",
7 | "devDependencies": {
8 | "@babel/plugin-transform-runtime": "^7.5.5",
9 | "nodemon": "^1.19.1"
10 | },
11 | "dependencies": {
12 | "@babel/cli": "^7.4.4",
13 | "@babel/core": "^7.4.5",
14 | "@babel/node": "^7.4.5",
15 | "@babel/preset-env": "^7.4.5",
16 | "@babel/runtime": "^7.5.5",
17 | "bcrypt": "^3.0.6",
18 | "copy": "^0.3.2",
19 | "dotenv": "^8.0.0",
20 | "graphql-tools": "^4.0.4",
21 | "graphql-yoga": "^1.17.4",
22 | "jsonwebtoken": "^8.5.1",
23 | "merge-graphql-schemas": "^1.5.8",
24 | "morgan": "^1.9.1",
25 | "passport": "^0.4.0",
26 | "passport-jwt": "^4.0.0",
27 | "prisma-client-lib": "^1.34.0"
28 | },
29 | "scripts": {
30 | "deploy": "prisma deploy",
31 | "generate": "prisma generate",
32 | "prisma": "yarn run deploy && yarn run generate",
33 | "dev": "nodemon --exec babel-node src/server.js",
34 | "build": "babel src -d build",
35 | "postbuild": "cd src && npx copy ./api/**/*.graphql ../build/api/",
36 | "start" : "node build/server.js"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Front-End/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "front-end",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-boost": "^0.4.2",
7 | "firebase": "^6.2.4",
8 | "graphql": "^14.3.1",
9 | "prop-types": "^15.7.2",
10 | "react": "^16.8.6",
11 | "react-apollo-hooks": "^0.4.5",
12 | "react-dom": "^16.8.6",
13 | "react-helmet": "^5.2.1",
14 | "react-modal": "^3.8.1",
15 | "react-router-dom": "^5.0.1",
16 | "react-scripts": "3.0.1",
17 | "react-scroll": "^1.7.12",
18 | "react-slick": "^0.24.0",
19 | "react-toastify": "^5.3.0",
20 | "semantic-ui-react": "^0.87.2",
21 | "styled-components": "^4.3.1",
22 | "styled-reset": "^2.0.15",
23 | "underscore": "^1.9.1"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": "react-app"
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Front-End/src/Components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Footer = styled.footer`
5 | display: flex;
6 | background-color: black;
7 | padding: 30px 0 10px;
8 | @media (max-width: 480px) {
9 | height: 23vh;
10 | }
11 | `;
12 |
13 | const FooterWrapper = styled.div`
14 | width: 100%;
15 | max-width: ${props => props.theme.maxWidth};
16 | margin: 0 auto;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | flex-direction: column;
21 | @media (max-width: 480px) {
22 | padding-bottom: 50px;
23 | }
24 | `;
25 |
26 | const GIT = styled.div`
27 | margin-bottom: 15px;
28 | a{
29 | color: white;
30 | }
31 | svg {
32 | color: white;
33 | }
34 | `;
35 |
36 |
37 |
38 | const Copyright = styled.div`
39 | color: white;
40 | `;
41 |
42 | export default () => (
43 |
51 | )
--------------------------------------------------------------------------------
/Front-End/src/Components/ItemBox.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import { addComma } from "./SharedFunction";
5 |
6 | export default ({ imgSrc, title, price, id }) => {
7 | const ItemDiv = styled.article`
8 | display: flex;
9 | flex-direction: column;
10 | margin: 0 10px;
11 | justify-content: space-between;
12 | `;
13 |
14 | const Img = styled.img`
15 | ${props => props.theme.whiteBox};
16 | width: 100%;
17 | box-shadow: 0px 0px 0px rgba(0,0,0,0), 0px 0px 10px rgba(0,0,0,0.1);
18 | `;
19 |
20 | const TextDiv = styled.div`
21 | display: flex;
22 | flex-direction: column;
23 | margin-top: 10px;
24 | `;
25 |
26 | const P = styled.p`
27 | padding-bottom: 5px;
28 | color: #555555;
29 | font-size: 14px;
30 | font-weight: 600;
31 | `;
32 |
33 |
34 |
35 | return (
36 |
37 |
38 |
39 |
40 | {title}
41 | ₩{addComma(price)}
42 |
43 |
44 |
45 | )
46 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/editProduct/editProduct.graphql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | editProduct (
3 | id: String!
4 | name: String
5 | price: Int
6 | mainCategory: String
7 | subCategory: String
8 | ): Product!
9 | }
10 |
11 | type Mutation {
12 | editNumberOfSales (
13 | id: [String!]!
14 | saleCount: [Int!]!
15 | ): Boolean!
16 | }
17 |
18 | type Mutation {
19 | editFile (
20 | fileId: String
21 | file: [String]
22 | ): Boolean!
23 | }
24 |
25 | type Mutation {
26 | editSize (
27 | productId: String
28 | sizeId: [String]
29 | sizeValue: [String]
30 | ): Boolean!
31 | }
32 |
33 | type Mutation {
34 | editColor (
35 | productId: String
36 | colorId: [String]
37 | colorValue: [String]
38 | ): Boolean!
39 | }
40 |
41 | type Mutation {
42 | editStock (
43 | productId: String
44 | stockId: [String]
45 | stockValue: [Int]
46 | ): Boolean!
47 | }
48 |
49 | type Mutation {
50 | editProductDetailFile (
51 | id: String!
52 | productDetailFile: String!
53 | ): Product!
54 | }
55 |
56 | type Mutation {
57 | editProductSizeFile (
58 | id: String!
59 | productSizeFile: String!
60 | ): Product!
61 | }
--------------------------------------------------------------------------------
/Back-End/src/api/User/createAccount/createAccount.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 | import bcrypt from "bcrypt";
3 |
4 | // 비밀번호 정규식 설정 (영문, 숫자, 특수문자 조합, 8~16자리)
5 | const chk_password = /^(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9])(?=.*[0-9]).{8,16}$/;
6 |
7 | export default {
8 | Mutation: {
9 | createAccount: async (_, args) => {
10 | const { name, email, password, zipCode, address, addressDetail, phone } = args;
11 | // 이메일 중복확인을 위한 코드
12 | // 입력한 이메일이 이미 db에 있으면 true값을 반환
13 | const exists = await prisma.$exists.user({ email });
14 | if (exists) {
15 | throw Error("This email is already taken");
16 | } else {
17 | // 비밀번호 정규식 확인
18 | if (chk_password.test(password) === false) {
19 | throw Error("비밀번호는 영문, 숫자, 특수문자 조합의 8~16자리여야 합니다.");
20 | }
21 | // 비밀번호 암호화 진행
22 | bcrypt.hash(password, 10, async function(err, hash) {
23 | await prisma.createUser({
24 | name,
25 | email,
26 | password: hash,
27 | zipCode,
28 | address,
29 | addressDetail,
30 | phone
31 | });
32 | });
33 | return true;
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Payment/PaymentQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const SEE_PAYMENT = gql`
4 | {
5 | seePayment {
6 | id
7 | product {
8 | id
9 | name
10 | price
11 | numberOfSales
12 | files {
13 | id
14 | url
15 | }
16 | }
17 | size {
18 | id
19 | size
20 | }
21 | color {
22 | id
23 | color
24 | }
25 | stock {
26 | id
27 | stock
28 | }
29 | count {
30 | id
31 | count
32 | }
33 | cart {
34 | id
35 | }
36 | }
37 | }
38 | `;
39 |
40 | export const EDIT_STOCK = gql`
41 | mutation editStock($id: [String!]!, $stock: [Int!]!) {
42 | editStock(id: $id, stock: $stock)
43 | }
44 | `;
45 |
46 | export const EDIT_NUMBEROFSALES = gql`
47 | mutation editNumberOfSales($id: [String!]!, $saleCount: [Int!]!) {
48 | editNumberOfSales(id: $id, saleCount: $saleCount)
49 | }
50 | `
51 |
52 | export const ADD_BUYLIST = gql`
53 | mutation addBuyList($product: [String!]!, $size: [String!]!, $color: [String!]!, $quantity: [Int!]!) {
54 | addBuyList(product: $product, size: $size, color: $color, quantity: $quantity)
55 | }
56 | `;
--------------------------------------------------------------------------------
/Front-End/src/Routes/Product/ProductQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const SEEITEM = gql`
4 | query seeproduct($id: String!) {
5 | seeproduct(id: $id) {
6 | id
7 | name
8 | price
9 | mainCategory
10 | subCategory
11 | files {
12 | id
13 | url
14 | }
15 | colors {
16 | id
17 | color
18 | }
19 | sizes {
20 | id
21 | size
22 | }
23 | stocks {
24 | id
25 | stock
26 | }
27 | productDetailFile {
28 | id
29 | productDetailFile
30 | }
31 | productSizeFile {
32 | id
33 | productSizeFile
34 | }
35 | }
36 | }
37 | `;
38 |
39 | export const ADD_CART = gql`
40 | mutation addCart($product: [String!]!, $sizeId: [String!]!, $colorId: [String!]!, $stockId: [String!]!, $count: [Int!]!) {
41 | addCart(product: $product, sizeId: $sizeId, colorId: $colorId, stockId: $stockId, count: $count)
42 | }
43 | `
44 |
45 | export const ADD_PAYMENT = gql`
46 | mutation addPayment($product: [String!]!, $size: [String!]!, $color: [String!]!, $stock: [String!]!, $count: [Int!]!, $cart: [String!]) {
47 | addPayment(product: $product, size: $size, color: $color, stock: $stock, count: $count, cart: $cart)
48 | }
49 | `;
--------------------------------------------------------------------------------
/Front-End/src/Components/SharedQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const QUERY = gql`
4 | {
5 | isLoggedIn @client
6 | }
7 | `;
8 |
9 | export const ME = gql`
10 | {
11 | me {
12 | id
13 | name
14 | email
15 | zipCode
16 | address
17 | addressDetail
18 | phone
19 | }
20 | }
21 | `;
22 |
23 | export const MEMUTATION = gql`
24 | mutation me {
25 | me {
26 | id
27 | name
28 | email
29 | zipCode
30 | address
31 | addressDetail
32 | phone
33 | }
34 | }
35 | `;
36 |
37 | export const DELETE_PAYMENT = gql`
38 | mutation deletePayment($id: [String!]!) {
39 | deletePayment(id: $id)
40 | }
41 | `;
42 |
43 | export const SEE_PAYMENT2 = gql`
44 | mutation seePayment {
45 | seePayment {
46 | id
47 | product {
48 | id
49 | name
50 | price
51 | numberOfSales
52 | files {
53 | id
54 | url
55 | }
56 | }
57 | size {
58 | id
59 | size
60 | }
61 | color {
62 | id
63 | color
64 | }
65 | stock {
66 | id
67 | stock
68 | }
69 | count {
70 | id
71 | count
72 | }
73 | cart {
74 | id
75 | }
76 | }
77 | }
78 | `
--------------------------------------------------------------------------------
/Back-End/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
90 | generated
91 | prisma.yml
92 | build
--------------------------------------------------------------------------------
/Front-End/public/slick/slick.min.css:
--------------------------------------------------------------------------------
1 | .slick-list,.slick-slider,.slick-track{
2 | position:relative;
3 | display:block;
4 | }
5 |
6 | .slick-loading .slick-slide,.slick-loading .slick-track{
7 | visibility:hidden
8 | }
9 |
10 | .slick-slider{
11 | box-sizing:border-box;
12 | -webkit-user-select:none;
13 | -moz-user-select:none;
14 | -ms-user-select:none;
15 | user-select:none;
16 | -webkit-touch-callout:none;
17 | -khtml-user-select:none;
18 | -ms-touch-action:pan-y;
19 | touch-action:pan-y;
20 | -webkit-tap-highlight-color:transparent
21 | }
22 |
23 | .slick-list{
24 | overflow:hidden;
25 | margin:0;
26 | padding:0
27 | }
28 |
29 | .slick-list:focus{
30 | outline:0
31 | }
32 |
33 | .slick-list.dragging{
34 | cursor:pointer;cursor:hand
35 | }
36 |
37 | .slick-slider .slick-list,.slick-slider .slick-track{
38 | -webkit-transform:translate3d(0,0,0);
39 | -moz-transform:translate3d(0,0,0);
40 | -ms-transform:translate3d(0,0,0);
41 | -o-transform:translate3d(0,0,0);
42 | transform:translate3d(0,0,0)
43 | }
44 |
45 | .slick-track{
46 | top:0;
47 | left:0
48 | }
49 |
50 | .slick-track:after,.slick-track:before{
51 | display:table;content:''
52 | }
53 |
54 | .slick-track:after{
55 | clear:both
56 | }
57 |
58 | .slick-slide{
59 | display:none;
60 | float:left;
61 | height:100%;
62 | min-height:1px
63 | }
64 |
65 | [dir=rtl] .slick-slide{
66 | float:right
67 | }
68 |
69 | .slick-slide img{
70 | display:block
71 | }
72 |
73 | .slick-slide.slick-loading img{
74 | display:none
75 | }
76 |
77 | .slick-slide.dragging img{
78 | pointer-events:none
79 | }
80 |
81 | .slick-initialized .slick-slide{
82 | display:block
83 | }
84 |
85 | .slick-vertical .slick-slide{
86 | display:block;
87 | height:auto;
88 | border:1px solid transparent;
89 | }
90 |
91 | .slick-arrow.slick-hidden{
92 | display:none
93 | }/*# sourceMappingURL=slick.min.css.map */
--------------------------------------------------------------------------------
/Back-End/src/api/User/editUser/editUser.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 | import bcrypt from "bcrypt";
3 |
4 | // 비밀번호 정규식 설정 (영문, 숫자, 특수문자 조합, 8~16자리)
5 | const chk_password = /^(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9])(?=.*[0-9]).{8,16}$/;
6 |
7 | export default {
8 | Mutation: {
9 | editUser: async (_, args, { request, isAuthenticated }) => {
10 | isAuthenticated(request);
11 | const { name, zipCode, address, addressDetail, phone, password, confirmPassword } = args;
12 |
13 | const { user } = request;
14 |
15 | if(password !== "" && confirmPassword !== "" && password === confirmPassword) {
16 | if(chk_password.test(password) === false) {
17 | throw Error("비밀번호는 영문, 숫자, 특수문자 조합의 8~16자리여야 합니다.");
18 | } else {
19 | bcrypt.hash(password, 10, async function(err, hash) {
20 | await prisma.updateUser({
21 | where: {
22 | id: user.id
23 | },
24 | data: {
25 | name,
26 | zipCode,
27 | address,
28 | addressDetail,
29 | phone,
30 | password: hash
31 | }
32 | })
33 | })
34 | return true;
35 | }
36 | } else {
37 | await prisma.updateUser({
38 | where: {
39 | id: user.id
40 | },
41 | data: {
42 | name, zipCode, address, addressDetail, phone
43 | }
44 | });
45 | return true;
46 | }
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/Design/Function/1_functionDesign.md:
--------------------------------------------------------------------------------
1 | # 1_functionDesign
2 |
3 | - 초기 기능 구상 ( 2019. 06. 12 )
4 |
5 |
6 |
7 | ## 1. 회원가입 및 로그인
8 | ### 1.1 회원가입
9 | ```
10 | ▶ 이메일
11 | - 비동기 중복체크
12 | ▶ 비밀번호
13 | - 암호화
14 | ▶ 이름
15 | ▶ 주소
16 | - 주소검색 api
17 | ```
18 |
19 | ### 1.2 로그인
20 | ```
21 | ▶ 이메일
22 | ▶ 비밀번호
23 | ```
24 |
25 |
26 | ## 2. 헤더
27 | ### 2.1 메뉴바
28 | ```
29 | ▶ Logo
30 | ▶ Home
31 | ▶ Store
32 | ▶ Sale
33 | ▶ Login or MyPage
34 | - 로그인 여부에 따라 바뀌게끔
35 | ```
36 |
37 |
38 |
39 | ## 3. 메인페이지
40 | ### 3.1 이미지 슬라이더
41 | ```
42 | sticky 사용
43 | ```
44 | ### 3.2 베스트 상품
45 | ### 3.3 신상품
46 | ### 3.4 Footer
47 |
48 |
49 |
50 | ## 4. 스토어
51 | ### 4.1 좌측 카테고리
52 | ```
53 | ▶ 상의
54 | - All
55 | - 티셔츠
56 | - 셔츠
57 | ▶ 하의
58 | - All
59 | - 청바지
60 | - 슬랙스
61 | ```
62 | ### 4.2 상품 View
63 | ```
64 | ▶ 상품 정보
65 | - 상품 이름
66 | - 판매가
67 | - 남은 재고
68 | ▶ 옵션 선택
69 | - 색상
70 | - 사이즈
71 | - 수량
72 | ▶ 장바구니 담기
73 | - 로그인 상태에서만 가능
74 | - 비로그인 일시 로그인화면으로 이동
75 | ▶ 바로구매
76 | - 장바구니와 동일
77 | ▶ 제품상세
78 | - 제품 상세
79 | - 사이즈 기준표
80 | - 판매자 정보
81 | ```
82 |
83 |
84 |
85 | ## 5. 마이페이지
86 | ### 5.1 개인정보 수정
87 | ```
88 | ▶ 비밀번호 수정
89 | ▶ 주소 수정
90 | ```
91 | ### 5.2 장바구니
92 | ```
93 | ▶ 장바구니 내 상품 합계
94 | ▶ 체크박스 형식
95 | - 체크된 상품들만 총 합계에 포함
96 | ▶ 수량변경
97 | ```
98 | ### 5.3 구매목록
99 | ### 5.4 로그아웃
100 |
101 |
102 |
103 | ## 6. 관리자페이지
104 | ### 6.1 상품관리
105 | ```
106 | ▶ 등록
107 | - 제품 이미지
108 | - 제품명
109 | - 제품 분류
110 | - 제품 가격
111 | - 제품 사이즈 (toggle Button 이용)
112 | - 재고량
113 | - 제품 상세 ( 상세 설명(생략), 사이즈표, 판매자 정보)
114 | ▶ 수정
115 | ▶ 삭제
116 | ▶ 관리
117 | - 제품별 재고량 체크 할수 있는 Component
118 | ```
119 |
120 |
121 |
122 | ## 7. 결제
123 | ### 7.1 결제페이지
124 | ```
125 | ▶ 구매 상품 정보
126 | ▶ 주문자 정보
127 | ```
128 | ### 7.2 결제
129 | ```
130 | ▶ 아이엠포트 (결제모듈) 이용
131 | ```
--------------------------------------------------------------------------------
/Back-End/src/api/Cart/addCart/addCart.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | addCart: async (_, args, { request, isAuthenticated }) => {
6 | isAuthenticated(request);
7 | const { user } = request;
8 | const { product, sizeId, colorId, stockId, count } = args;
9 | try {
10 | await count.map(async(item, index) => {
11 | const countId = await prisma.createCount({
12 | count: item
13 | });
14 | await prisma.createCart({
15 | user: {
16 | connect: {
17 | id: user.id
18 | }
19 | },
20 | product: {
21 | connect: {
22 | id: product[index]
23 | }
24 | },
25 | sizeId: {
26 | connect: {
27 | id: sizeId[index]
28 | }
29 | },
30 | colorId: {
31 | connect: {
32 | id: colorId[index]
33 | }
34 | },
35 | stockId: {
36 | connect: {
37 | id: stockId[index]
38 | }
39 | },
40 | count: {
41 | connect: {
42 | id: countId.id
43 | }
44 | }
45 | })
46 | })
47 | return true;
48 | } catch(err) {
49 | console.log(err);
50 | return false;
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/Front-End/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 |
23 |
24 |
25 |
26 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Front-End/src/Components/Routes.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { Route, Switch, withRouter } from "react-router-dom";
4 | import Main from "../Routes/Main";
5 | import MyPage from "../Routes/Mypage";
6 | import Auth from "../Routes/Auth";
7 | import Store from "../Routes/Store";
8 | import Product from "../Routes/Product";
9 | import Payment from "../Routes/Payment";
10 | import Admin from "../Routes/Admin";
11 | import { useMutation } from "react-apollo-hooks";
12 | import { DELETE_PAYMENT, SEE_PAYMENT2 } from "./SharedQueries";
13 |
14 | const AppRouter = withRouter(({ isLoggedIn, isAdmin, location }) => {
15 | let idArrayTemp = [];
16 |
17 | const deletePaymentMutation = useMutation(DELETE_PAYMENT, { variables: {
18 | id:idArrayTemp
19 | }})
20 |
21 | const seePaymentMutation = useMutation(SEE_PAYMENT2);
22 |
23 | const seePaymentFunction = async() => {
24 | const { data } = await seePaymentMutation();
25 | if(data) {
26 | data.seePayment.map(item => (
27 | idArrayTemp.push(item.id)
28 | ))
29 | deletePaymentFunction();
30 | }
31 | }
32 |
33 | const deletePaymentFunction = async () => {
34 | const { data } = await deletePaymentMutation();
35 | if(data) {
36 | idArrayTemp = [];
37 | }
38 | }
39 |
40 | // 다른 페이지로 이동할 때마다 payment필드에 값이 있으면 제거한다.
41 | useEffect(() => {
42 | if(isLoggedIn && location.pathname !== "/payment") {
43 | seePaymentFunction();
44 | }
45 | // eslint-disable-next-line react-hooks/exhaustive-deps
46 | },[location])
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | )
56 | })
57 |
58 | // Router는 항상 proptypes를 가져야 함
59 | AppRouter.propTypes = {
60 | isLoggedIn: PropTypes.bool.isRequired
61 | }
62 |
63 | export default AppRouter;
--------------------------------------------------------------------------------
/Front-End/src/Components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ThemeProvider } from "styled-components";
3 | import GlobalStyles from "../Styles/GlobalStyles";
4 | import Theme from '../Styles/Theme';
5 | import Routes from './Routes';
6 | import { HashRouter as Router } from "react-router-dom";
7 | import { useQuery, useMutation } from 'react-apollo-hooks';
8 | import { ToastContainer, toast } from 'react-toastify';
9 | import "react-toastify/dist/ReactToastify.css";
10 | import Header from "./Header";
11 | import Footer from './Footer';
12 | import Navigator from './Navigator';
13 | import { QUERY, MEMUTATION } from './SharedQueries';
14 | import { useState } from 'react';
15 | import { useEffect } from 'react';
16 |
17 | export default () => {
18 | const [isAdmin, setIsAdmin] = useState(false);
19 | const [loading, setLoading] = useState(false);
20 |
21 | const {
22 | data: { isLoggedIn }
23 | } = useQuery(QUERY);
24 |
25 | const meMutation = useMutation(MEMUTATION);
26 | const meFunction = async () => {
27 | const { data } = await meMutation();
28 | if (data.me.email === process.env.REACT_APP_ADMIN) {
29 | setIsAdmin(true);
30 | }
31 | setLoading(true);
32 | }
33 |
34 | useEffect(() => {
35 | if(isLoggedIn) {
36 | meFunction();
37 | }
38 | // eslint-disable-next-line react-hooks/exhaustive-deps
39 | }, [isLoggedIn])
40 |
41 |
42 | return (
43 |
44 | <>
45 |
46 |
47 | {isLoggedIn && loading && (
48 | <>
49 |
50 |
51 |
52 |
53 |
54 | >
55 | )}
56 | {!isLoggedIn && (
57 | <>
58 |
59 |
60 |
61 |
62 |
63 | >
64 | )}
65 |
66 | >
67 |
68 | );
69 | }
--------------------------------------------------------------------------------
/Back-End/src/api/BuyList/addBuyList/addBuyList.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | addBuyList: async (_, args, { request, isAuthenticated}) => {
6 | isAuthenticated(request);
7 | const { user } = request;
8 | const { product, size, color, quantity } = args;
9 | try {
10 | await quantity.map(async(item, index) => {
11 | const quantityId = await prisma.createQuantity({
12 | quantity: item
13 | });
14 | await prisma.createBuyList({
15 | user: {
16 | connect: {
17 | id: user.id
18 | }
19 | },
20 | product: {
21 | connect: {
22 | id: product[index]
23 | }
24 | },
25 | size: {
26 | connect: {
27 | id: size[index]
28 | }
29 | },
30 | color: {
31 | connect: {
32 | id: color[index]
33 | }
34 | },
35 | quantity: {
36 | connect: {
37 | id: quantityId.id
38 | }
39 | }
40 | })
41 | })
42 | return true;
43 | } catch {
44 | return false;
45 | }
46 | // return prisma.createBuyList({
47 | // user: {
48 | // connect: {
49 | // id: user.id
50 | // }
51 | // },
52 | // product: {
53 | // connect: {
54 | // id: product
55 | // }
56 | // },
57 | // size,
58 | // color,
59 | // count
60 | // })
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # REACT_ChanStyle
2 |
3 | - [완성된 사이트 바로가기]
4 |
5 |
6 |
7 | **※ 상품을 등록하거나 수정하는 기능은 관리자 계정으로만 이용할 수 있습니다.**
8 | ```
9 | Email : Admin@admin.com
10 | Password : 123qwe!@#
11 | ```
12 | **※ 회원가입이 번거로우신 분들은 아래의 계정을 이용해 주세요.**
13 | ```
14 | Email : guest@guest.com
15 | Password: 123qwe!@#
16 | ```
17 |
18 |
19 |
20 | ## 1. 기능 소개
21 |
22 | ### 1. Auth 🔐
23 | ```
24 | 1-1. 로그인
25 | - Passport.js와 JWT를 이용하여 로그인 기능 구현
26 | 1-2. 회원가입
27 | - bcrypt를 이용하여 비밀번호 암호화 기능
28 | - 카카오 주소 API 활용
29 | ```
30 |
31 | ### 2. Main ⭐
32 | ```
33 | 2-1. 이미지 슬라이더
34 | - Slick.js를 이용하여 Main 이미지 슬라이더 구현 (일정 시간마다 자동으로 넘어감)
35 |
36 | 2-2. BEST, NEW ITEM
37 | - 전체 상품중 판매량을 기준으로 BEST ITEM 선정, 상품 등록시간을 기준으로 NEW ITEM을 선정하여
38 | MAIN 페이지에 노출
39 | (ViewPort 크기에 따라 슬라이더로 구현)
40 | ```
41 |
42 | ### 3. Store 🏡
43 | ```
44 | 3-1. 카테고리에 따른 상품 분류
45 |
46 | 3-2. Pagination
47 | - 하단의 "더보기" 버튼을 이용한 paging 기능 구현
48 | ```
49 |
50 | ### 4. Product 🥼
51 | ```
52 | 4-1. 상품의 상세정보를 보여주는 페이지
53 |
54 | 4-2. 옵션 선택
55 | - Color 값에 따라 Size값 선택가능
56 | - Size값과 함께 남은 재고량 표시
57 |
58 | 4-3. 장바구니, 구매
59 | - 장바구니에 담거나 바로 구매페이지로 넘어갈 수 있음
60 | - 해당 기능은 로그인 후에만 이용 가능
61 |
62 | 4-4. Floating Button
63 | - 스크롤을 내릴시 페이지 맨위로 바로 올라가는 기능을 가진 Floating Button 구현
64 | ```
65 |
66 | ### 5. MyPage 🛒
67 | ```
68 | 5-1. 로그아웃
69 |
70 | 5-2. 장바구니
71 | - 체크박스를 이용하여 여러개의 상품 동시 주문 가능
72 | - 삭제버튼을 클릭하여 장바구니에서 상품 제거
73 | - 가격과 개수에 따른 가격 표시
74 | - 장바구니 내의 total 값 표시
75 |
76 | 5-3. 구매목록
77 | - 자신이 구매한 상품 표시
78 | - 번호 Paging 구현
79 |
80 | 5-4. 개인정보 수정
81 | ```
82 |
83 | ### 6. Admin 🧑
84 | ```
85 | 6-1. 로그아웃
86 |
87 | 6-2. 상품 등록
88 | - Firebase Storage를 이용하여 상품 이미지 업로드
89 | - 옵션 값 추가를 위한 동적 테이블
90 | - 대분류 값에 따른 소분류값
91 |
92 | 6-3. 상품 수정
93 |
94 | 6-4. 상품 삭제
95 | ```
96 |
97 | ### 7. Payment 🎁
98 | ```
99 | 7-1. 결제
100 | - import 모듈을 이용하여 결제 기능 구현
101 | (개발자용이기 때문에 결제 금액은 일정시간이 지난 후 자동으로 결제 취소가 됩니다.)
102 | ```
103 |
104 |
105 |
106 | ## 2. 사용 도구
107 | ### Front-End
108 | - REACT
109 | - Apollo
110 | - Styled-Component
111 | - React Hooks
112 | - Firebase Storage
113 |
114 | ### Back-end
115 | - Prisma
116 | - GraphQL
117 | - Node.JS (Express)
118 | - Heroku
119 |
120 |
121 |
122 | [완성된 사이트 바로가기]:https://chanstyle.netlify.com
--------------------------------------------------------------------------------
/Front-End/src/Routes/Mypage/MyPageQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const EDIT_PROFILE = gql`
4 | mutation editUser(
5 | $name: String,
6 | $zipCode: String,
7 | $address: String,
8 | $addressDetail: String,
9 | $phone: String,
10 | $password: String,
11 | $confirmPassword: String
12 | ) {
13 | editUser(
14 | name: $name,
15 | zipCode: $zipCode,
16 | address: $address,
17 | addressDetail:$addressDetail,
18 | phone:$phone,
19 | password: $password,
20 | confirmPassword: $confirmPassword
21 | )
22 | }
23 | `;
24 |
25 | export const SEE_CART = gql`
26 | {
27 | seeCart {
28 | id
29 | product {
30 | files {
31 | url
32 | }
33 | id
34 | name
35 | price
36 | }
37 | sizeId {
38 | id
39 | size
40 | }
41 | colorId {
42 | id
43 | color
44 | }
45 | stockId {
46 | id
47 | stock
48 | }
49 | count {
50 | id
51 | count
52 | }
53 | }
54 | }
55 | `;
56 |
57 | export const DELETE_CART = gql`
58 | mutation deleteCart($id: [String!]!) {
59 | deleteCart(id:$id)
60 | }
61 | `;
62 |
63 | export const SEE_BUYLIST = gql`
64 | mutation seeBuyList2($first:Int $skip:Int) {
65 | seeBuyList2(first: $first, skip: $skip) {
66 | id
67 | product {
68 | id
69 | name
70 | price
71 | }
72 | size {
73 | id
74 | size
75 | }
76 | color {
77 | id
78 | color
79 | }
80 | quantity {
81 | id
82 | quantity
83 | }
84 | }
85 | }
86 | `;
87 |
88 | export const BUYLIST_QUERY = gql`
89 | {
90 | seeBuyList {
91 | id
92 | }
93 | }
94 | `;
95 |
96 | export const LOG_OUT = gql`
97 | mutation logUserOut {
98 | logUserOut @client
99 | }
100 | `;
--------------------------------------------------------------------------------
/Back-End/src/api/models.graphql:
--------------------------------------------------------------------------------
1 | scalar DateTime
2 |
3 | type User {
4 | id: ID!
5 | name: String!
6 | email: String!
7 | salt: String
8 | password: String!
9 | zipCode: String!
10 | address: String!
11 | addressDetail: String!
12 | phone: String!
13 | cart: [Cart!]!
14 | payment: [Payment!]!
15 | buyList: [BuyList!]!
16 | createdAt: DateTime!
17 | updatedAt: DateTime!
18 | }
19 |
20 | type Product {
21 | id: ID!
22 | name: String!
23 | price: Int!
24 | mainCategory: String!
25 | subCategory: String!
26 | files: [File!]!
27 | colors: [Color!]!
28 | sizes: [Size!]!
29 | stocks: [Stock!]!
30 | productDetailFile: [ProductDetailFile!]!
31 | productSizeFile: [ProductSizeFile!]!
32 | numberOfSales: Int!
33 | createdAt: DateTime!
34 | updatedAt: DateTime!
35 | }
36 |
37 | type File {
38 | id: ID!
39 | url: String!
40 | product: Product!
41 | }
42 |
43 | type ProductDetailFile {
44 | id: ID!
45 | productDetailFile: String!
46 | product: Product!
47 | }
48 |
49 | type ProductSizeFile {
50 | id: ID!
51 | productSizeFile: String!
52 | product: Product!
53 | }
54 |
55 | type Size {
56 | id: ID!
57 | size: String!
58 | product: Product!
59 | cart: Cart!
60 | payment: Payment!
61 | buyList: BuyList!
62 | }
63 |
64 | type Color {
65 | id: ID!
66 | color: String!
67 | product: Product!
68 | cart: Cart!
69 | payment: Payment!
70 | buyList: BuyList!
71 | }
72 |
73 | type Stock {
74 | id: ID!
75 | stock: Int!
76 | product: Product!
77 | cart: Cart!
78 | payment: Payment!
79 | }
80 |
81 | type Count {
82 | id: ID!
83 | count: Int!
84 | cart: Cart
85 | payment: Payment!
86 | }
87 |
88 | type Cart {
89 | id: ID!
90 | user: User!
91 | product: [Product!]!
92 | sizeId: [Size!]!
93 | colorId: [Color!]!
94 | stockId: [Stock!]!
95 | count: [Count!]!
96 | payment: [Payment!]
97 | }
98 |
99 | type Quantity {
100 | id: ID!
101 | quantity: Int!
102 | user: User!
103 | buyList: [BuyList!]!
104 | }
105 |
106 | type BuyList {
107 | id: ID!
108 | user: User!
109 | product: [Product!]!
110 | size: [Size!]!
111 | color: [Color!]!
112 | quantity: [Quantity!]!
113 | }
114 |
115 | type Payment {
116 | id: ID!
117 | user: User!
118 | product: [Product!]!
119 | size: [Size!]!
120 | color: [Color!]!
121 | stock: [Stock!]!
122 | count: [Count!]!
123 | cart: [Cart!]
124 | }
--------------------------------------------------------------------------------
/Front-End/src/Components/Navigator.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import { useQuery } from "react-apollo-hooks";
5 | import { ME } from "./SharedQueries";
6 |
7 | const Navigator = styled.nav`
8 | width: 100%;
9 | position: fixed;
10 | top: 82px;
11 | left: 0;
12 | border-bottom: ${props => props.theme.boxBorder};
13 | @media (max-width: 600px) {
14 | top: 120px;
15 | }
16 | background-color: ${props => props.theme.whiteColor};
17 | z-index: 9999;
18 | `;
19 |
20 | const NavigatorWrapper = styled.div`
21 | width: 100%;
22 | max-width: ${props => props.theme.maxWidth};
23 | margin: 0 auto;
24 | background-color: ${props => props.theme.whiteColor};
25 | `;
26 |
27 | const UL = styled.ul`
28 | display: grid;
29 | grid-template-columns: repeat(3, 1fr);
30 | text-align: center;
31 | font-size: 20px;
32 | font-weight: 500;
33 | `;
34 |
35 | const LI = styled.li`
36 | padding: 10px 0;
37 | `;
38 |
39 | export default ({ isLoggedIn, isAdmin }) => {
40 |
41 | let Data = [];
42 | if( isLoggedIn){
43 | const { data } = useQuery(ME);
44 | Data = data;
45 | }
46 |
47 | return (
48 |
49 |
50 | {isLoggedIn && Data.me &&(
51 |
52 | - HOME
53 | - STORE
54 | {isAdmin && - Admin
}
55 | {!isAdmin && - MyPage
}
56 |
57 | )}
58 | {isLoggedIn && !Data.me &&(
59 |
60 | - HOME
61 | - STORE
62 | - MyPage
63 |
64 | )}
65 | {!isLoggedIn && (
66 |
67 | - HOME
68 | - STORE
69 | - LOGIN
70 |
71 | )}
72 |
73 |
74 | )
75 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Admin/AdminQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "apollo-boost";
2 |
3 | export const UPLOAD = gql`
4 | mutation upload(
5 | $name: String!,
6 | $price: Int!,
7 | $mainCategory: String!,
8 | $subCategory: String!,
9 | $files: [String!]!,
10 | $sizes: [String!]!,
11 | $colors: [String!]!,
12 | $stocks: [Int!]!,
13 | $productDetailFiles: [String!],
14 | $productSizeFiles: [String!]) {
15 | upload(
16 | name: $name,
17 | price: $price,
18 | mainCategory: $mainCategory,
19 | subCategory: $subCategory,
20 | files: $files,
21 | sizes: $sizes,
22 | colors: $colors,
23 | stocks: $stocks,
24 | productDetailFiles: $productDetailFiles,
25 | productSizeFiles: $productSizeFiles
26 | ) {
27 | id
28 | }
29 | }
30 | `;
31 |
32 | export const EDIT_SEE_PRODUCT = gql`
33 | mutation seeProductAll($id: String, $sort: String!, $first: Int, $skip: Int) {
34 | seeProductAll(id:$id, sort:$sort, first:$first, skip:$skip) {
35 | id
36 | name
37 | price
38 | mainCategory
39 | subCategory
40 | files {
41 | id
42 | url
43 | }
44 | colors {
45 | id
46 | color
47 | }
48 | sizes {
49 | id
50 | size
51 | }
52 | stocks {
53 | id
54 | stock
55 | }
56 | }
57 | }
58 | `;
59 |
60 | export const DELETE_PRODUCT = gql`
61 | mutation deleteProduct($id: String!) {
62 | deleteProduct(id:$id)
63 | }
64 | `;
65 |
66 | export const EDIT_PRODUCT = gql`
67 | mutation editProduct(
68 | $id: String!,
69 | $name: String,
70 | $price: Int,
71 | $mainCategory: String,
72 | $subCategory: String,
73 | $fileId:String,
74 | $file: [String],
75 | $sizeId: [String],
76 | $sizeValue: [String],
77 | $colorId: [String],
78 | $colorValue: [String],
79 | $stockId: [String],
80 | $stockValue: [Int],
81 | $productId: String)
82 | {
83 | editProduct(id:$id, name:$name, price:$price, mainCategory:$mainCategory, subCategory:$subCategory) {
84 | id,
85 | name,
86 | price
87 | },
88 | editFile(fileId:$fileId, file:$file),
89 | editSize(productId:$productId, sizeId:$sizeId, sizeValue:$sizeValue),
90 | editColor(productId:$productId, colorId:$colorId, colorValue:$colorValue),
91 | editStock(productId:$productId, stockId:$stockId, stockValue:$stockValue),
92 | }
93 | `;
--------------------------------------------------------------------------------
/Back-End/src/api/Product/Upload/upload.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | upload: async (_, args, { request, isAuthenticated}) => {
6 | isAuthenticated(request);
7 | const { name,
8 | price,
9 | mainCategory,
10 | subCategory,
11 | files,
12 | colors,
13 | sizes,
14 | stocks,
15 | productDetailFiles,
16 | productSizeFiles,
17 | } = args;
18 | // file과 option 없이 product 생성
19 | const product = await prisma.createProduct({ name, price, mainCategory, subCategory });
20 | files.forEach(async file => {
21 | await prisma.createFile({
22 | url: file,
23 | product: {
24 | connect: {
25 | id: product.id
26 | }
27 | }
28 | })
29 | });
30 | colors.forEach(async color => {
31 | await prisma.createColor({
32 | color,
33 | product: {
34 | connect: {
35 | id: product.id
36 | }
37 | }
38 | })
39 | });
40 | sizes.forEach(async size => {
41 | await prisma.createSize({
42 | size,
43 | product: {
44 | connect: {
45 | id: product.id
46 | }
47 | }
48 | })
49 | });
50 | stocks.forEach(async stock => {
51 | await prisma.createStock({
52 | stock,
53 | product: {
54 | connect: {
55 | id: product.id
56 | }
57 | }
58 | })
59 | });
60 | productDetailFiles.forEach(async productDetailFile => {
61 | await prisma.createProductDetailFile({
62 | productDetailFile,
63 | product: {
64 | connect: {
65 | id: product.id
66 | }
67 | }
68 | })
69 | });
70 | productSizeFiles.forEach(async productSizeFile => {
71 | await prisma.createProductSizeFile({
72 | productSizeFile,
73 | product: {
74 | connect: {
75 | id: product.id
76 | }
77 | }
78 | })
79 | });
80 | return product
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Main/MainContainer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MainPresenter from "./MainPresenter";
3 | import { MAIN_SEEITEM } from './MainQueries';
4 | import { useQuery } from 'react-apollo-hooks';
5 |
6 | export default () => {
7 | const settings = {
8 | dots: true,
9 | infinite: true,
10 | speed: 800,
11 | slidesToShow: 1,
12 | slidesToScroll: 1,
13 | autoplay: true,
14 | autoplaySpeed: 3500,
15 | responsive: [
16 | {
17 | breakpoint: 600,
18 | settings: {
19 | arrows: false
20 | }
21 | }
22 | ]
23 | }
24 |
25 | const itemSettings = {
26 | dots: false,
27 | infinite: false,
28 | rows: 4,
29 | slidesToShow: 1,
30 | autoplay: false,
31 | arrows: false,
32 | swipe: false,
33 | responsive: [
34 | {
35 | breakpoint: 900,
36 | settings: {
37 | dots: false,
38 | infinite: false,
39 | rows: 2,
40 | slidesToShow: 1
41 | }
42 | },
43 | {
44 | breakpoint: 600,
45 | settings: {
46 | dots: true,
47 | slidesToShow: 3,
48 | slidesToScroll: 3,
49 | rows: 1,
50 | arrows: false,
51 | swipe: true
52 | }
53 | },
54 | {
55 | breakpoint: 400,
56 | settings: {
57 | dots: true,
58 | slidesToShow: 2,
59 | slidesToScroll: 2,
60 | rows: 1,
61 | arrows: false,
62 | swipe: true
63 | }
64 | }
65 | ]
66 | }
67 |
68 | // 베스트 아이템
69 | const { data: bestData, loading: bestLoading } = useQuery(MAIN_SEEITEM, {
70 | variables: {
71 | sort: "best"
72 | }
73 | });
74 | // 최신 아이템
75 | const { data: newData, loading: newLoading } = useQuery(MAIN_SEEITEM, {
76 | variables: {
77 | sort: "new"
78 | }
79 | });
80 |
81 |
82 | const testData = [
83 | {
84 | id: 1,
85 | url: process.env.REACT_APP_SLIDE1
86 | },
87 | {
88 | id: 2,
89 | url: process.env.REACT_APP_SLIDE2
90 | }
91 | ]
92 |
93 | return (
94 |
103 | )
104 | }
--------------------------------------------------------------------------------
/Front-End/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/Front-End/public/slick/slick-theme.min.css:
--------------------------------------------------------------------------------
1 | @charset 'UTF-8';
2 | .slick-dots,.slick-next,.slick-prev{
3 | position:absolute;
4 | display:block;
5 | padding:0
6 | }
7 |
8 | .slick-dots li button:before,.slick-next:before,.slick-prev:before{
9 | font-family:slick;
10 | -webkit-font-smoothing:antialiased;
11 | -moz-osx-font-smoothing:grayscale
12 | }
13 |
14 | .slick-loading .slick-list{
15 | background:url(ajax-loader.gif) center center no-repeat #fff
16 | }
17 |
18 | .slick-next,.slick-prev{
19 | font-size:0;
20 | line-height:0;
21 | top:50%;
22 | width:40px;
23 | height:70px;
24 | -webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);
25 | cursor:pointer;
26 | border:none;
27 | outline:0;
28 | background:transparent;
29 |
30 | }
31 |
32 | .slick-next:hover,.slick-prev:hover{
33 | outline:0;
34 | background: rgba(0,0,0,0.1)
35 | }
36 |
37 | .slick-next:focus:before,.slick-next:hover:before,.slick-prev:focus:before,.slick-prev:hover:before{
38 | opacity:1
39 | }
40 |
41 | .slick-next.slick-disabled:before,.slick-prev.slick-disabled:before{
42 | opacity:.25
43 | }
44 |
45 | .slick-next:before,.slick-prev:before{
46 | font-size:40px;
47 | line-height:1;
48 | opacity:.75;
49 | font-weight: 600;
50 | }
51 |
52 | .slick-prev{
53 | left:-40px;
54 | }
55 |
56 |
57 | .slick-prev:before{
58 | content:'<'
59 | }
60 |
61 | .slick-next:before{
62 | content:'>'
63 | }
64 |
65 | .slick-next{
66 | right:-40px
67 | }
68 |
69 | [dir=rtl] .slick-next{
70 | right:auto;
71 | left:-25px
72 | }
73 |
74 | [dir=rtl] .slick-next:before{
75 | content:'●'
76 | }
77 |
78 | .slick-dotted.slick-slider{
79 | margin-bottom:30px
80 | }
81 |
82 | .slick-dots {
83 | bottom: 10px;
84 | width: 100%;
85 | margin: 0;
86 | list-style: none;
87 | text-align: center
88 | }
89 |
90 | .slick-dots li {
91 | position: relative;
92 | display: inline-block;
93 | width: 20px;
94 | height: 20px;
95 | margin: 0 2px;
96 | padding: 0;
97 | cursor: pointer
98 | }
99 |
100 | .slick-dots li button {
101 | font-size: 0;
102 | line-height: 0;
103 | display: block;
104 | width: 20px;
105 | height: 20px;
106 | padding: 5px;
107 | cursor: pointer;
108 | color: transparent;
109 | border: 0;
110 | outline: 0;
111 | background: 0 0
112 | }
113 |
114 | .slick-dots li button:focus, .slick-dots li button:hover {
115 | outline: 0
116 | }
117 |
118 | .slick-dots li button:focus:before, .slick-dots li button:hover:before {
119 | opacity: 1
120 | }
121 |
122 | .slick-dots li button:before {
123 | font-size: 6px;
124 | line-height: 20px;
125 | position: absolute;
126 | top: 0;
127 | left: 0;
128 | width: 20px;
129 | height: 20px;
130 | content: '●';
131 | text-align: center;
132 | opacity: .25;
133 | color: white;
134 | }
135 |
136 | .slick-dots li.slick-active button:before {
137 | opacity: .75;
138 | color: #000
139 | }
140 |
141 | /*# sourceMappingURL=slick-theme.min.css.map */
--------------------------------------------------------------------------------
/Front-End/src/Components/StoreContent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 | import Slider from "react-slick";
5 | import ItemBox from "./ItemBox";
6 |
7 | const ContentTitleDiv = styled.div`
8 | border-bottom: ${props => props.theme.borderBottom};
9 | padding: 20px 0 10px;
10 | `;
11 |
12 | const H2 = styled.h2`
13 | font-size: ${props => props.theme.titleFontSize};
14 | font-weight: 600;
15 | `;
16 |
17 | const H3 = styled.h3`
18 | font-size: 24px;
19 | font-weight: 600;
20 | margin-bottom: 30px;
21 | `;
22 |
23 | const Best = styled.article`
24 | padding: 15px 0 30px;
25 | `;
26 |
27 | const CustomSlider = styled(Slider)`
28 | .slick-slide {
29 | display: grid;
30 | grid-template-columns: repeat(4, 1fr);
31 | }
32 | @media(max-width: 1024px) {
33 | .slick-slide {
34 | grid-template-columns: 1fr;
35 | margin-bottom: 30px;
36 | }
37 | .slick-dots>li>button:before {
38 | color: black;
39 | }
40 | }
41 | `;
42 |
43 | const AllItem = styled.article`
44 | border-top: ${props => props.theme.borderBottom};
45 | `;
46 |
47 | const AllItemGrid = styled.div`
48 | display: grid;
49 | grid-template-columns: repeat(4, 1fr);
50 | padding: 50px 0;
51 | grid-row-gap: 50px;
52 | @media (max-width: 1024px) {
53 | grid-template-columns: repeat(3, 1fr);
54 | }
55 | @media (max-width: 768px) {
56 | grid-template-columns: repeat(2, 1fr);
57 | }
58 | `;
59 |
60 | const StoreContent = ({
61 | title,
62 | all,
63 | best,
64 | settings
65 | }) => {
66 | return (
67 | <>
68 |
69 | {title}
70 |
71 |
72 | 🔥Best Item🔥
73 |
74 | {best.length !== 0 && best.seeProductBest.map(item => (
75 |
82 | ))}
83 |
84 |
85 |
86 |
87 | {all.length !== 0 && all.map(item => (
88 |
95 | ))}
96 |
97 |
98 | >
99 | )
100 | }
101 |
102 | StoreContent.propTypes = {
103 | title: PropTypes.string.isRequired,
104 | settings: PropTypes.object
105 | }
106 |
107 | export default StoreContent;
--------------------------------------------------------------------------------
/Front-End/src/Components/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Input from "./Input";
4 | import Button from './Button';
5 | import DaumAPI from "./DaumAPI";
6 | import PropTypes from "prop-types";
7 |
8 | const ZipCodeBox = styled.div`
9 | display: flex;
10 | width: 80%;
11 | justify-content: space-between;
12 | margin: 5px 0;
13 | #zipCodeInput, #zipCodeBtn {
14 | width: 45%;
15 | margin: 0;
16 | }
17 | @media (max-width: 600px) {
18 | width: 90%;
19 | }
20 | `;
21 |
22 | export const Select = styled.select`
23 | border: 0;
24 | border: ${props => props.theme.boxBorder};
25 | border-radius: ${props => props.theme.borderRadius};
26 | padding: 10px;
27 | text-align: center;
28 | margin: 5px 0;
29 | `;
30 |
31 | const PhoneBox = styled.div`
32 | display: flex;
33 | width: 80%;
34 | justify-content: space-between;
35 | #phone1, #phone2, #phone3 {
36 | width: 30%;
37 | }
38 | @media (max-width: 600px) {
39 | width: 90%;
40 | }
41 | `;
42 |
43 | const SignUpForm = ({
44 | onSubmit,
45 | name,
46 | email,
47 | password,
48 | confirmPassword,
49 | zipCode,
50 | open,
51 | setOpen,
52 | handleAddress,
53 | address,
54 | addressDetail,
55 | phone1,
56 | phone2,
57 | phone3,
58 | setAction,
59 | ButtonText
60 | }) => {
61 | return (
62 |
99 | )
100 | }
101 |
102 | SignUpForm.propTypes = {
103 | onSubmit: PropTypes.func.isRequired,
104 | name: PropTypes.object.isRequired,
105 | email: PropTypes.object.isRequired,
106 | password: PropTypes.object,
107 | confirmPassword: PropTypes.object,
108 | zipCode: PropTypes.object.isRequired,
109 | open: PropTypes.bool.isRequired,
110 | setOpen: PropTypes.func.isRequired,
111 | handleAddress: PropTypes.func.isRequired,
112 | address: PropTypes.object.isRequired,
113 | addressDetail: PropTypes.object.isRequired,
114 | phone1: PropTypes.func,
115 | phone2: PropTypes.object,
116 | phone3: PropTypes.object,
117 | setAction: PropTypes.func,
118 | ButtonText: PropTypes.string.isRequired
119 | }
120 |
121 | export default SignUpForm;
--------------------------------------------------------------------------------
/Back-End/src/api/Payment/addPayment/addPayment.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Mutation: {
5 | addPayment : async (_, args, { request }) => {
6 | const { user } = request;
7 | const { product, size, color, stock, count, cart } = args;
8 |
9 | if(cart === undefined) {
10 | try {
11 | await count.map(async(item, index) => {
12 | const countId = await prisma.createCount({
13 | count: item
14 | });
15 | await prisma.createPayment({
16 | user: {
17 | connect: {
18 | id: user.id
19 | }
20 | },
21 | product: {
22 | connect: {
23 | id: product[index]
24 | }
25 | },
26 | size: {
27 | connect: {
28 | id: size[index]
29 | }
30 | },
31 | color: {
32 | connect: {
33 | id: color[index]
34 | }
35 | },
36 | stock: {
37 | connect: {
38 | id: stock[index]
39 | }
40 | },
41 | count: {
42 | connect: {
43 | id: countId.id
44 | }
45 | }
46 | })
47 | })
48 | return true;
49 | } catch (error) {
50 | console.log(error);
51 | return false;
52 | }
53 | } else {
54 | try {
55 | await count.map(async(item, index) => {
56 | const countId = await prisma.createCount({
57 | count: item
58 | });
59 | await prisma.createPayment({
60 | user: {
61 | connect: {
62 | id: user.id
63 | }
64 | },
65 | product: {
66 | connect: {
67 | id: product[index]
68 | }
69 | },
70 | size: {
71 | connect: {
72 | id: size[index]
73 | }
74 | },
75 | color: {
76 | connect: {
77 | id: color[index]
78 | }
79 | },
80 | stock: {
81 | connect: {
82 | id: stock[index]
83 | }
84 | },
85 | count: {
86 | connect: {
87 | id: countId.id
88 | }
89 | },
90 | cart: {
91 | connect: {
92 | id: cart[index]
93 | }
94 | }
95 | })
96 | })
97 | return true;
98 | } catch (error) {
99 | console.log(error);
100 | return false;
101 | }
102 | }
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Main/MainPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Slider from "react-slick";
3 | import styled from "styled-components";
4 | import ItemBox from "../../Components/ItemBox";
5 | import Loader from "../../Components/Loader";
6 |
7 | const Main = styled.div`
8 | max-width: ${props => props.theme.maxWidth};
9 | margin: 0 auto;
10 | width: 100%;
11 | margin-bottom: 30px;
12 | `;
13 |
14 | const MainWrapper = styled.div`
15 | padding: 0 50px;
16 | @media (max-width: 600px) {
17 | padding: 0;
18 | }
19 | `;
20 |
21 | const SliderDiv = styled.div`
22 | height: 50vh;
23 | img {
24 | width: 100%;
25 | height: 100%;
26 | }
27 | @media (max-width: 600px) {
28 | height: 30vh;
29 | }
30 | `;
31 |
32 | const MainTitle = styled.div`
33 | margin-top: 70px;
34 | border-bottom: ${props => props.theme.borderBottom};
35 | padding: 20px;
36 | margin-bottom: 40px;
37 | `;
38 |
39 | const H4 = styled.h4`
40 | font-size: 32px;
41 | font-weight: 600;
42 | text-align: center;
43 | `;
44 |
45 | const CustomSlider = styled(Slider)`
46 | @media (min-width: 601px) {
47 | .slick-slide {
48 | display: grid;
49 | grid-template-columns: repeat(4, 1fr);
50 | margin-bottom: 30px;
51 | }
52 | .slick-track {
53 | width: 0 !important;
54 | }
55 | }
56 | @media (max-width: 900px) {
57 | .slick-slide {
58 | grid-template-columns: repeat(2, 1fr);
59 | }
60 | }
61 | .slick-dots {
62 | position: relative;
63 | margin-top: 20px;
64 | }
65 | .slick-dots>li>button:before {
66 | color: black;
67 | }
68 | `;
69 |
70 | const Section = styled.section``;
71 |
72 | export default ({
73 | settings,
74 | itemSettings,
75 | testData,
76 | bestData,
77 | newData,
78 | bestLoading,
79 | newLoading
80 | }) => {
81 | return (
82 |
83 | {bestLoading && newLoading && }
84 | {!bestLoading && !newLoading &&
85 |
86 |
87 |
88 | {testData.map((data, index) => (
89 |
90 |
91 |
92 | ))}
93 |
94 |
95 |
96 |
97 | 🔥 BEST ITEM 🔥
98 |
99 |
100 | {!bestLoading && bestData.seeproduct.map(item => (
101 |
108 | ))}
109 |
110 |
111 |
112 |
113 | ⭐ NEW ITEM ⭐
114 |
115 |
116 | {!newLoading && newData.seeproduct.map(item => (
117 |
124 | ))}
125 |
126 |
127 | }
128 |
129 | )
130 | }
131 |
132 |
133 |
--------------------------------------------------------------------------------
/Front-End/src/Components/Icons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const Logo = ({ size = 24 }) => (
4 |
11 | )
12 |
13 | export const CartIcon = ({size=36}) => (
14 |
25 | )
26 |
27 | export const UpArrowIcon = () => (
28 |
36 | )
37 |
38 | export const CloseIcon = () => (
39 |
46 | )
47 |
48 | export const DownIcon = ({size=24}) => (
49 |
50 |
61 | )
62 |
63 | export const Plus = ({size=24}) => (
64 |
65 |
77 |
78 | )
79 |
80 | export const Image = ({size=24}) => (
81 |
94 | )
--------------------------------------------------------------------------------
/Front-End/src/Components/EditProduct.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 | import { addComma } from "./SharedFunction";
5 |
6 | const ProductDiv = styled.div`
7 | ${props => props.theme.whiteBox};
8 | box-shadow: 0px 0px 0px rgba(0,0,0,0), 0px 0px 10px rgba(0,0,0,0.1);
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: space-between;
12 | `;
13 |
14 | const ProductInfo = styled.div`
15 | padding: 10px;
16 | `;
17 |
18 | const Img = styled.img`
19 | width: 100%;
20 | `;
21 |
22 | const ProductBasic = styled.div`
23 | margin-top: 10px;
24 | border-bottom: ${props => props.theme.boxBorder};
25 | `;
26 |
27 | const H4 = styled.h4`
28 | font-size: 18px;
29 | font-weight: 600;
30 | padding-bottom: 5px;
31 | `;
32 |
33 | const Category = styled.p`
34 | font-size: 12px;
35 | padding-bottom: 5px;
36 | `;
37 |
38 | const Price = styled.p`
39 | font-size: 14px;
40 | padding-bottom: 5px;
41 | font-weight: 600;
42 | `;
43 |
44 | const ProductOption = styled.div`
45 | padding-top: 5px;
46 | `;
47 |
48 | const H5 = styled.h5`
49 | font-weight: 600;
50 | font-size: 14px;
51 | padding-bottom: 5px;
52 | `;
53 |
54 | const ProductOptionDiv = styled.div`
55 | display: grid;
56 | grid-template-columns: repeat(3, 1fr);
57 | text-align: center;
58 | padding-bottom: 5px;
59 | `;
60 |
61 | const ButtonDiv = styled.div`
62 | display: flex;
63 | flex-direction: column;
64 | `;
65 |
66 | const EditButton = styled.button`
67 | border: none;
68 | width: 100%;
69 | background-color: ${props => props.theme.confirmColor};
70 | color: #fff;
71 | padding: 5px 0;
72 | margin-bottom: 5px;
73 | cursor: pointer;
74 | `;
75 |
76 | const DeleteButton = styled.button`
77 | border: none;
78 | width: 100%;
79 | padding: 5px 0;
80 | background-color: firebrick;
81 | color: #fff;
82 | cursor: pointer;
83 | `;
84 |
85 | const EditProduct = ({
86 | name,
87 | mainCategory,
88 | subCategory,
89 | price,
90 | img,
91 | sizes,
92 | colors,
93 | stocks,
94 | editClick,
95 | id,
96 | deleteClick
97 | }) => {
98 | return (
99 |
100 |
101 |
102 |
103 | {name}
104 | {mainCategory} > {subCategory}
105 | ₩{addComma(price)}
106 |
107 |
108 | 옵션
109 | {sizes.map((item,index) => (
110 |
111 | {colors[index].color}
112 | {item.size}
113 | {stocks[index].stock !== 0 ? (
114 |
115 | {stocks[index].stock}
116 |
117 | ) : (
118 |
119 | {stocks[index].stock}(품절)
120 |
121 | )}
122 |
123 | ))}
124 |
125 |
126 |
127 | editClick(id)}>수정
128 | deleteClick(id)}>삭제
129 |
130 |
131 | )
132 | }
133 |
134 | EditProduct.propTypes = {
135 | name: PropTypes.string.isRequired,
136 | mainCategory: PropTypes.string.isRequired,
137 | subCategory: PropTypes.string.isRequired,
138 | price: PropTypes.number.isRequired,
139 | img: PropTypes.string.isRequired,
140 | sizes: PropTypes.array.isRequired,
141 | colors: PropTypes.array.isRequired,
142 | stocks: PropTypes.array.isRequired
143 | }
144 |
145 | export default EditProduct;
--------------------------------------------------------------------------------
/Front-End/src/Routes/Auth/AuthPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Input from "../../Components/Input";
4 | import Button from './../../Components/Button';
5 | import SignUpForm from "../../Components/SignUpForm";
6 |
7 | const Wrapper = styled.section`
8 | min-height: 76vh;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | flex-direction: column;
13 | @media (max-width: 600px) {
14 | padding: 0 50px;
15 | min-height: 73vh;
16 | }
17 | @media (max-width: 480px) {
18 | padding: 0 20px;
19 | min-height: 60vh;
20 | }
21 | `;
22 |
23 | const Box = styled.article`
24 | width: 100%;
25 | border-radius: 0;
26 | max-width: 500px;
27 | margin: 0 auto;
28 | `;
29 |
30 | export const Form = styled(Box)`
31 | form {
32 | width: 100%;
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | flex-direction: column;
37 | border: 0;
38 | border-top: ${props=> props.theme.boxBorder};
39 | border-bottom: ${props=> props.theme.boxBorder};
40 | margin-bottom: 30px;
41 | @media (max-width: 600px) {
42 | ${props => props.theme.whiteBox};
43 | }
44 | input {
45 | width: 80%;
46 | margin: 5px 0;
47 | &:first-child {
48 | margin-top: 15px;
49 | }
50 | @media (max-width: 600px) {
51 | width: 90%;
52 | }
53 | }
54 | button {
55 | width: 80%;
56 | margin: 5px 0;
57 | &:last-child {
58 | margin-bottom: 15px;
59 | }
60 | @media (max-width: 600px) {
61 | width: 90%;
62 | }
63 | }
64 | #loginBtn, #createAccountBtn {
65 | background-color: ${props => props.theme.confirmColor};
66 | color: white;
67 | margin-bottom: 30px;
68 | }
69 | }
70 | `;
71 |
72 | const H3 = styled.h3`
73 | font-size: 30px;
74 | font-weight: 600;
75 | margin-bottom: 15px;
76 | `;
77 |
78 | export default ({
79 | action,
80 | setAction,
81 | name,
82 | email,
83 | password,
84 | confirmPassword,
85 | zipCode,
86 | address,
87 | addressDetail,
88 | onSubmit,
89 | open,
90 | setOpen,
91 | handleAddress,
92 | phone1,
93 | phone2,
94 | phone3
95 | }) => {
96 | return (
97 |
98 |
109 | >) :
110 | (
111 | <>
112 | Sign Up
113 |
131 | >
132 | )}
133 |
134 |
135 | )
136 | }
--------------------------------------------------------------------------------
/Back-End/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type User {
2 | id: ID! @id
3 | name: String!
4 | email: String! @unique
5 | password: String!
6 | zipCode: String!
7 | address: String!
8 | addressDetail: String!
9 | phone: String!
10 | cart: [Cart!]! @relation(name: "UserOfCart", onDelete: CASCADE)
11 | buyList: [BuyList!]! @relation(name: "UserOfBuyList", onDelete: CASCADE)
12 | payment: [Payment!]! @relation(name: "UserOfPayment", onDelete: CASCADE)
13 | createdAt: DateTime! @createdAt
14 | updatedAt: DateTime! @updatedAt
15 | }
16 |
17 | type Product {
18 | id: ID! @id
19 | name: String!
20 | price: Int!
21 | mainCategory: String!
22 | subCategory: String!
23 | files: [File!]! @relation(name: "FileofProduct", onDelete: CASCADE)
24 | colors: [Color!]! @relation(name: "ProductOfColor", onDelete: CASCADE)
25 | sizes: [Size!]! @relation(name: "ProductOfSize", onDelete: CASCADE)
26 | stocks: [Stock!]! @relation(name: "ProductOfStock", onDelete: CASCADE)
27 | productDetailFile: [ProductDetailFile!]! @relation(name: "FileOfProductDetail", onDelete: CASCADE)
28 | productSizeFile: [ProductSizeFile!]! @relation(name: "FileOfSize", onDelete: CASCADE)
29 | numberOfSales: Int! @default(value: 0)
30 | cart: [Cart!]! @relation(name: "ProductOfCart", onDelete: CASCADE)
31 | buyList: [BuyList!]! @relation(name: "ProductOfBuyList", onDelete: CASCADE)
32 | payment: [Payment!]! @relation(name: "ProductOfPayment", onDelete: CASCADE)
33 | createdAt: DateTime! @createdAt
34 | updatedAt: DateTime! @updatedAt
35 | }
36 |
37 | type File {
38 | id: ID! @id
39 | url: String!
40 | product: Product! @relation(name: "FileofProduct")
41 | }
42 |
43 | type ProductDetailFile {
44 | id: ID! @id
45 | productDetailFile: String!
46 | product: Product! @relation(name: "FileOfProductDetail")
47 | }
48 |
49 | type ProductSizeFile {
50 | id: ID! @id
51 | productSizeFile: String @default(value: "https://mblogthumb-phinf.pstatic.net/MjAxNzExMDdfMTQ3/MDAxNTEwMDQxODYyMjY1.kAvpXchJkjzWlDqtAQgYS7MLR9PFVIIe4vcBfUR6jOQg.FHU59tAPCbw6YolyoEnnpALAKzu9-01K41e8-Nj3vlQg.JPEG.siyeonzzz/171106061107.jpg?type=w800")
52 | product: Product! @relation(name: "FileOfSize")
53 | }
54 |
55 | type Size {
56 | id: ID! @id
57 | size: String!
58 | product: Product! @relation(name: "ProductOfSize")
59 | cart: [Cart!]! @relation(name: "CartOfSize")
60 | payment: [Payment!]! @relation(name: "PaymentOfSize")
61 | buyList: [BuyList!]! @relation(name:"BuyListOfSize")
62 | }
63 |
64 | type Color {
65 | id: ID! @id
66 | color: String!
67 | product: Product! @relation(name: "ProductOfColor")
68 | cart: [Cart!]! @relation(name: "CartOfColor")
69 | payment: [Payment!]! @relation(name: "PaymentOfColor")
70 | buyList: [BuyList!]! @relation(name:"BuyListOfColor")
71 | }
72 |
73 | type Stock {
74 | id: ID! @id
75 | stock: Int!
76 | product: Product! @relation(name: "ProductOfStock")
77 | cart: [Cart!]! @relation(name: "CartOfStock")
78 | payment: [Payment!]! @relation(name: "PaymentOfStock")
79 | }
80 |
81 | type Count {
82 | id: ID! @id
83 | count: Int!
84 | cart: [Cart!] @relation(name: "CartOfCount")
85 | payment: [Payment!]! @relation(name: "PaymentOfCount")
86 | }
87 |
88 | type Cart {
89 | id: ID! @id
90 | user: User! @relation(name: "UserOfCart")
91 | product: [Product!]! @relation(name: "ProductOfCart")
92 | sizeId: [Size!]! @relation(name: "CartOfSize")
93 | colorId: [Color!]! @relation(name: "CartOfColor")
94 | stockId: [Stock!]! @relation(name: "CartOfStock")
95 | count: [Count!]! @relation(name: "CartOfCount", onDelete: CASCADE)
96 | payment: [Payment!] @relation(name: "PaymentOfCart")
97 | }
98 |
99 | type Quantity {
100 | id: ID! @id
101 | quantity : Int!
102 | buyList: [BuyList!]! @relation(name:"BuyListOfQuanitity")
103 | }
104 |
105 | type BuyList {
106 | id: ID! @id
107 | user: User! @relation(name: "UserOfBuyList")
108 | product: [Product!]! @relation(name: "ProductOfBuyList")
109 | size: [Size!]! @relation(name:"BuyListOfSize")
110 | color: [Color!]! @relation(name:"BuyListOfColor")
111 | quantity: [Quantity!]! @relation(name:"BuyListOfQuanitity")
112 | }
113 |
114 | type Payment {
115 | id: ID! @id
116 | user: User! @relation(name: "UserOfPayment")
117 | product: [Product!]! @relation(name: "ProductOfPayment")
118 | size: [Size!]! @relation(name: "PaymentOfSize")
119 | color: [Color!]! @relation(name: "PaymentOfColor")
120 | stock: [Stock!]! @relation(name: "PaymentOfStock")
121 | count: [Count!]! @relation(name: "PaymentOfCount", onDelete: CASCADE)
122 | cart: [Cart!] @relation(name: "PaymentOfCart")
123 | }
--------------------------------------------------------------------------------
/Front-End/src/Components/DaumPostCode.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const defaultErrorMessage = (현재 Daum 우편번호 서비스를 이용할 수 없습니다. 잠시 후 다시 시도해주세요.
);
5 |
6 | class DaumPostcode extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | display: 'none',
11 | width: this.props.width,
12 | height: this.props.height,
13 | error: false,
14 | };
15 | }
16 |
17 | componentDidMount() {
18 | const scriptId = 'daum_postcode_script';
19 | const isExist = !!document.getElementById(scriptId);
20 |
21 | if (!isExist) {
22 | const script = document.createElement('script');
23 | script.src = this.props.scriptUrl;
24 | script.onload = () => this.initiate(this);
25 | script.onerror = error => this.handleError(error);
26 | script.id = scriptId;
27 | document.body.appendChild(script);
28 | } else this.initiate(this);
29 | }
30 |
31 | initiate = (comp) => {
32 | window.daum.postcode.load(() => {
33 | const Postcode = new window.daum.Postcode({
34 | oncomplete: function oncomplete(data) {
35 | comp.props.onComplete(data);
36 | if (comp.props.autoClose) comp.setState({ display: 'none' });
37 | },
38 | onresize: function onresize(size) {
39 | if (comp.props.autoResize) comp.setState({ height: size.height });
40 | },
41 | alwaysShowEngAddr: comp.props.alwaysShowEngAddr,
42 | animation: comp.props.animation,
43 | autoMapping: comp.props.autoMapping,
44 | autoResize: comp.props.autoResize,
45 | height: comp.props.height,
46 | hideEngBtn: comp.props.hideEngBtn,
47 | hideMapBtn: comp.props.hideMapBtn,
48 | maxSuggestItems: comp.props.maxSuggestItems,
49 | pleaseReadGuide: comp.props.pleaseReadGuide,
50 | pleaseReadGuideTimer: comp.props.pleaseReadGuideTimer,
51 | shorthand: comp.props.shorthand,
52 | showMoreHName: comp.props.showMoreHName,
53 | submitMode: comp.props.submitMode,
54 | theme: comp.props.theme,
55 | useSuggest: comp.props.useSuggest,
56 | width: comp.props.width,
57 | zonecodeOnly: comp.props.zonecodeOnly,
58 | });
59 |
60 | Postcode.open(this.wrap, { q: this.props.defaultQuery, autoClose: this.props.autoClose });
61 | });
62 | };
63 |
64 | handleError = (error) => {
65 | error.target.remove();
66 | this.setState({ error: true });
67 | };
68 |
69 | render() {
70 | const {
71 | style,
72 | onComplete,
73 | alwaysShowEngAddr,
74 | animation,
75 | autoClose,
76 | autoMapping,
77 | autoResize,
78 | defaultQuery,
79 | errorMessage,
80 | height,
81 | hideEngBtn,
82 | hideMapBtn,
83 | maxSuggestItems,
84 | pleaseReadGuide,
85 | pleaseReadGuideTimer,
86 | scriptUrl,
87 | shorthand,
88 | showMoreHName,
89 | submitMode,
90 | theme,
91 | useSuggest,
92 | width,
93 | zonecodeOnly,
94 | ...rest
95 | } = this.props;
96 |
97 | return (
98 | { this.wrap = div; }}
100 | style={{
101 | width: this.state.width,
102 | height: this.state.height,
103 | display: this.state.display,
104 | ...style,
105 | }}
106 | {...rest}
107 | >
108 | {this.state.error && this.props.errorMessage}
109 |
110 | );
111 | }
112 | }
113 |
114 | DaumPostcode.propTypes = {
115 | onComplete: PropTypes.func.isRequired,
116 | alwaysShowEngAddr: PropTypes.bool,
117 | animation: PropTypes.bool,
118 | autoClose: PropTypes.bool,
119 | autoMapping: PropTypes.bool,
120 | autoResize: PropTypes.bool,
121 | defaultQuery: PropTypes.string,
122 | errorMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
123 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
124 | hideEngBtn: PropTypes.bool,
125 | hideMapBtn: PropTypes.bool,
126 | maxSuggestItems: PropTypes.number,
127 | pleaseReadGuide: PropTypes.number,
128 | pleaseReadGuideTimer: PropTypes.number,
129 | scriptUrl: PropTypes.string,
130 | shorthand: PropTypes.bool,
131 | showMoreHName: PropTypes.bool,
132 | style: PropTypes.object,
133 | submitMode: PropTypes.bool,
134 | theme: PropTypes.object,
135 | useSuggest: PropTypes.bool,
136 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
137 | zonecodeOnly: PropTypes.bool
138 | };
139 |
140 | DaumPostcode.defaultProps = {
141 | alwaysShowEngAddr: false,
142 | animation: false,
143 | autoClose: false,
144 | autoMapping: true,
145 | autoResize: false,
146 | defaultQuery: null,
147 | errorMessage: defaultErrorMessage,
148 | height: 400,
149 | hideEngBtn: false,
150 | hideMapBtn: false,
151 | maxSuggestItems: 10,
152 | pleaseReadGuide: 0,
153 | pleaseReadGuideTimer: 1.5,
154 | scriptUrl: 'https://ssl.daumcdn.net/dmaps/map_js_init/postcode.v2.js?autoload=false',
155 | shorthand: true,
156 | showMoreHName: false,
157 | style: null,
158 | submitMode: true,
159 | theme: null,
160 | useSuggest: true,
161 | width: '100%',
162 | zonecodeOnly: false
163 | };
164 |
165 | export default DaumPostcode;
166 |
--------------------------------------------------------------------------------
/Front-End/src/Routes/Store/StorePresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import StoreContent from './../../Components/StoreContent';
4 | import Loader from "../../Components/Loader";
5 | import { DownIcon } from "../../Components/Icons";
6 |
7 | const Store = styled.div`
8 | min-height: 79vh;
9 | position: relative;
10 | `;
11 |
12 | const Menubar = styled.aside`
13 | position: absolute;
14 | width: 250px;
15 | height: 100%;
16 | background-color:#2e353d;
17 | @media (max-width: 768px) {
18 | width: 200px;
19 | }
20 | @media (max-width: 700px) {
21 | width: 100%;
22 | position: relative;
23 | }
24 | `;
25 |
26 | const CategoryTitle = styled.div`
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | font-size: 22px;
31 | font-weight: 600;
32 | text-align: center;
33 | line-height: 50px;
34 | color: white;
35 | background-color: #23282e;
36 | div {
37 | height: 38px;
38 | @media (min-width: 600px) {
39 | display:none;
40 | }
41 | }
42 | @media (max-width: 700px) {
43 | cursor: pointer;
44 | }
45 | `;
46 |
47 | const MenuList = styled.div`
48 | @media (max-width: 700px) {
49 | display: none;
50 | }
51 | `;
52 |
53 | const MenuUl = styled.ul`
54 | text-align: center;
55 | color: white;
56 | `;
57 |
58 | const MenuLi = styled.li`
59 | padding: 15px;
60 | cursor: pointer;
61 | &:hover {
62 | background-color: #4f5b69;
63 | transition: all 1s ease;
64 | }
65 | `;
66 |
67 | const DropUl = styled.ul`
68 | display: none;
69 |
70 | `;
71 |
72 | const DropLi = styled.li`
73 | background-color: #181c20;
74 | padding: 10px 0;
75 | border-bottom: 1px solid #23282e;
76 | cursor: pointer;
77 | &:hover {
78 | background-color: #020203;
79 | }
80 | `;
81 |
82 | const ContentSection = styled.section`
83 | max-width: 1300px;
84 | margin: 0 auto;
85 | margin-left:250px;
86 | @media (max-width: 768px) {
87 | margin-left: 200px;
88 | }
89 | @media (max-width: 700px) {
90 | margin-left: 0;
91 | }
92 | `;
93 |
94 | const ContentWrapper = styled.div`
95 | margin: 0 50px;
96 | @media (max-width: 480px) {
97 | margin: 0 20px;
98 | }
99 | `;
100 |
101 | const MoreDiv = styled.div`
102 | display: flex;
103 | justify-content: center;
104 | align-items: center;
105 | height: 100px;
106 | div {
107 | min-height: 0 !important;
108 | }
109 | `;
110 |
111 | const MoreBtn = styled.button`
112 | border: none;
113 | font-size: 18px;
114 | padding: 10px 15px;
115 | border-radius: 30px;
116 | background-color: ${props => props.theme.confirmColor};
117 | color: #fff;
118 | cursor: pointer;
119 | outline-style: none;
120 | `;
121 |
122 |
123 | export default ({
124 | topToggle,
125 | bottomToggle,
126 | menuListToggle,
127 | title,
128 | menuClick,
129 | best,
130 | all,
131 | settings,
132 | clickMore,
133 | pLoading,
134 | dataTemp
135 | }) => {
136 | return (
137 |
138 |
139 | menuListToggle()}>
140 | Category
141 |
142 |
159 |
160 |
161 |
162 | {all.length === 0 && }
163 | {best.length !== 0 && all.length !== 0 && (
164 |
170 | )}
171 |
172 | {pLoading && (
173 |
174 |
175 |
176 | )}
177 | {all.length >= 4 && best.length !== 0 && all.length !== 0 && !pLoading && dataTemp.seeProductAll.length !== 0 && (
178 |
179 | clickMore()}>더 보기
180 |
181 | )}
182 |
183 |
184 | )
185 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/seeProduct/seeProduct.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | Query: {
5 | seeproduct: (_, args) => {
6 | const { sort, mainCategory, subCategory, id } = args;
7 |
8 | // 상품 하나 상세 보기
9 | if ( id !== undefined) {
10 | return prisma.products({ where: {
11 | id
12 | }});
13 | }
14 |
15 | // 전체 상품 보기 (Main 화면에서 사용될 것)
16 | else if (mainCategory === undefined && subCategory === undefined) {
17 | // 분류 x
18 | if(sort === undefined) {
19 | return prisma.products({
20 | orderBy: "createdAt_DESC"
21 | })
22 | }
23 | // 전체에서 베스트 아이템 8개
24 | else if (sort === "best") {
25 | return prisma.products({
26 | orderBy: "numberOfSales_DESC",
27 | first: 8 // 첫번째로부터 8개까지만 출력
28 | })
29 | }
30 | // 전체에서 최신 아이템 8개
31 | else if (sort === "new") {
32 | return prisma.products({
33 | orderBy: "createdAt_DESC",
34 | first: 8
35 | })
36 | }
37 | }
38 | // MainCategory별 상품 보기 (Store 화면에서 사용될 것)
39 | else if(mainCategory !== undefined && subCategory === undefined) {
40 | // MainCategory 전체 상품 보기
41 | if (sort === undefined) {
42 | return prisma.products({
43 | where: {
44 | mainCategory
45 | },
46 | orderBy: "createdAt_DESC"
47 | })
48 | }
49 | // MainCategory 전체 중 베스트 상품 8개
50 | else if (sort === "best") {
51 | return prisma.products({
52 | where: {
53 | mainCategory
54 | },
55 | orderBy: "numberOfSales_DESC"
56 | })
57 | }
58 | }
59 | // MainCategory > SubCategory별 상품 보기 (Store 화면에서 사용)
60 | else if(subCategory !== undefined) {
61 | // SubCategory 전체 상품 보기
62 | if(sort === undefined) {
63 | return prisma.products({
64 | where: {
65 | subCategory
66 | },
67 | orderBy: "createdAt_DESC"
68 | })
69 | }
70 | // SubCategory 전체 중 베스트 상품 8개
71 | else if (sort === "best") {
72 | return prisma.products({
73 | where: {
74 | subCategory
75 | },
76 | orderBy: "numberOfSales_DESC"
77 | })
78 | }
79 | }
80 | }
81 | },
82 | Mutation : {
83 | seeProductBest: (_, args) => {
84 | const { sort, mainCategory, subCategory } = args;
85 | if(sort === "all" && mainCategory === "" && subCategory === "") {
86 | return prisma.products({
87 | orderBy: "numberOfSales_DESC",
88 | first: 4
89 | })
90 | }
91 |
92 | if(sort === "all" && mainCategory !== "" && subCategory === "") {
93 | return prisma.products({
94 | where: {
95 | mainCategory
96 | },
97 | orderBy: "numberOfSales_DESC"
98 | })
99 | }
100 |
101 | if(sort === "all" && mainCategory !== "" && subCategory !== "") {
102 | return prisma.products({
103 | where: {
104 | subCategory
105 | },
106 | orderBy: "numberOfSales_DESC"
107 | })
108 | }
109 | },
110 | seeProductAll: (_, args) => {
111 | const { id, sort, mainCategory, subCategory, first, skip } = args;
112 |
113 | if(id !== undefined) {
114 | return prisma.products({ where: {
115 | id
116 | }});
117 | }
118 |
119 | // 전체상품보기
120 | if(sort === "all" && mainCategory === "") {
121 | return prisma.products({
122 | orderBy: "createdAt_DESC",
123 | first,
124 | skip
125 | })
126 | }
127 | // 대분류 상품 전체 보기
128 | if(sort === "all" && mainCategory !== "" && subCategory === "") {
129 | return prisma.products({
130 | where: {
131 | mainCategory
132 | },
133 | orderBy: "createdAt_DESC",
134 | first,
135 | skip
136 | })
137 | }
138 | // 소분류 상품 전체 보기
139 | if(sort === "all" && mainCategory !== "" && subCategory !== "") {
140 | return prisma.products({
141 | where: {
142 | subCategory
143 | },
144 | orderBy: "createdAt_DESC",
145 | first,
146 | skip
147 | })
148 | }
149 | }
150 | }
151 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Auth/AuthContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import AuthPresenter from "./AuthPresenter";
3 | import useInput from "../../Hooks/useInput";
4 | import { useMutation } from 'react-apollo-hooks';
5 | import { LOG_IN, LOCAL_LOG_IN, CREATE_ACCOUNT } from './AuthQueries';
6 | import { toast } from "react-toastify";
7 |
8 | export default () => {
9 | // 회원가입, 로그인 등의 상태를 관리하기 위한 react hook
10 | const [action, setAction] = useState("logIn");
11 | const [open, setOpen] = useState(false);
12 | // 입력값을 받아오기 위해서 useInput hook을 사용함
13 | const name = useInput("");
14 | const email = useInput("");
15 | const password = useInput("");
16 | const confirmPassword = useInput("");
17 | const zipCode = useInput("");
18 | const address = useInput("");
19 | const addressDetail = useInput("");
20 | const phone1 = useInput("010");
21 | const phone2 = useInput("");
22 | const phone3 = useInput("");
23 |
24 | const logInMutation = useMutation(LOG_IN, {
25 | variables: {
26 | email: email.value,
27 | password: password.value
28 | }
29 | });
30 |
31 | const localLogInMutation = useMutation(LOCAL_LOG_IN);
32 |
33 | const createAccountMutation = useMutation(CREATE_ACCOUNT, {
34 | variables: {
35 | name: name.value,
36 | email: email.value,
37 | password: password.value,
38 | zipCode: zipCode.value,
39 | address: address.value,
40 | addressDetail: addressDetail.value,
41 | phone: phone1.value + "-" + phone2.value + "-" + phone3.value
42 | }
43 | })
44 |
45 | // daum pstcode API의 callback 함수
46 | const handleAddress = (data) => {
47 | let fullAddress = data.address;
48 | let extraAddress = '';
49 |
50 | if (data.addressType === 'R') {
51 | if (data.bname !== '') {
52 | extraAddress += data.bname;
53 | }
54 | if (data.buildingName !== '') {
55 | extraAddress += (extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName);
56 | }
57 | fullAddress += (extraAddress !== '' ? ` (${extraAddress})` : '');
58 | }
59 | zipCode.setValue(data.zonecode);
60 | address.setValue(fullAddress);
61 | }
62 |
63 | const loginFunction = async () => {
64 | if (email !== "" && password !== "") {
65 | try {
66 | // logInMutation으로 부터 token 값을 얻어옴
67 | const { data: { login: token } } = await logInMutation();
68 | if (token !== "" && token !== undefined) {
69 | // token 값을 성공적으로 얻어오면
70 | // LOCAL_LOG_IN mutation에 의해 LocalStorage에 token이 set 된다.
71 | // LocalStorage에 token이 set되는 순간 로그인이 성공된것
72 | localLogInMutation({ variables: { token } });
73 | // 새로고침 => 새로고침을 하지 않으면 바로 mutation 값을 읽어올수 없음
74 | window.location ="/";
75 | } else {
76 | throw Error();
77 | }
78 | } catch {
79 | // back-end로 부터의 error를 catch하여 toast로 로그인 실패 메시지를 보여줌
80 | toast.error(`로그인에 실패하였습니다😢 email 또는 Password를 확인해 주세요.`);
81 | }
82 | }
83 | }
84 |
85 |
86 | const onSubmit = async e => {
87 | e.preventDefault();
88 |
89 | if (action === "logIn") {
90 | loginFunction();
91 | } else if (action === "signUp") {
92 | // 빈값이 있는지 확인
93 | if (
94 | name.value !== "" &&
95 | email.value !== "" &&
96 | password.value !== "" &&
97 | confirmPassword.value !== "" &&
98 | zipCode.value !== "" &&
99 | address.value !== "" &&
100 | addressDetail.value !== "" &&
101 | phone1.value !== "" &&
102 | phone2.value !== "" &&
103 | phone3.value !== ""
104 | ) {
105 | // 비밀번호, 비밀번호확인값 일치 검사
106 | if (password.value !== confirmPassword.value) {
107 | toast.error("비밀번호가 일치하지 않습니다.");
108 | return false;
109 | }
110 | try {
111 | const { data: { createAccount } } = await createAccountMutation();
112 | if (!createAccount) {
113 | toast.error("회원가입에 실패하였습니다. 다시시도해 주세요");
114 | } else {
115 | // 회원가입 성공시 loginFunction을 실행함으로써
116 | // token값을 얻어오고 main화면으로 이동
117 | // setTimeout을 하지 않을 경우 성공적으로 createAccountMutation된 값을
118 | // loginFunction에서 읽지 못하므로 setTimeout으로 약간의 시간을 줌
119 | setTimeout(() => loginFunction(), 1000);
120 |
121 | // 값 초기화
122 | name.setValue("");
123 | email.setValue("");
124 | password.setValue("");
125 | confirmPassword.setValue("");
126 | zipCode.setValue("");
127 | address.setValue("");
128 | addressDetail.setValue("");
129 | phone3.setValue("");
130 | phone1.setValue("");
131 | phone2.setValue("");
132 |
133 | }
134 | } catch (e) {
135 | toast.error(e.message);
136 | }
137 | }
138 | }
139 | }
140 |
141 | return (
142 |
160 | )
161 | }
--------------------------------------------------------------------------------
/Back-End/src/api/Product/editProduct/editProduct.js:
--------------------------------------------------------------------------------
1 | import { prisma } from "../../../../generated/prisma-client";
2 |
3 | export default {
4 | // product 기본 정보 수정
5 | Mutation: {
6 | editProduct: async (_, args, { request, isAuthenticated }) => {
7 | isAuthenticated(request);
8 | const {
9 | id,
10 | name,
11 | price,
12 | mainCategory,
13 | subCategory
14 | } = args;
15 | const product = await prisma.$exists.product({ id });
16 | if (product) {
17 | return prisma.updateProduct({
18 | data: {
19 | name,
20 | price,
21 | mainCategory,
22 | subCategory
23 | },
24 | where: {
25 | id
26 | }
27 | })
28 | } else {
29 | throw Error("You can't do that");
30 | }
31 | },
32 | // 판매량 수정
33 | editNumberOfSales: (_, args) => {
34 | const { id, saleCount } = args;
35 | try {
36 | id.map(async(item, index) => {
37 | await prisma.updateProduct({
38 | where: {
39 | id: item
40 | },
41 | data: {
42 | numberOfSales: saleCount[index]
43 | }
44 | })
45 | })
46 | return true;
47 | } catch {
48 | return false;
49 | }
50 | },
51 |
52 | // 파일 수정
53 | editFile: async (_, args) => {
54 | const { fileId, file } = args;
55 | file.forEach(async url => {
56 | await prisma.updateFile({
57 | where: {
58 | id: fileId
59 | },
60 | data: {
61 | url
62 | }
63 | })
64 | })
65 | return true;
66 | },
67 | // 사이즈 수정
68 | editSize: (_, args) => {
69 | const { productId, sizeId, sizeValue } = args;
70 | try {
71 | sizeValue.map(async (item,index) => {
72 | if(sizeId.length > index) {
73 | await prisma.updateSize({
74 | where: {
75 | id: sizeId[index]
76 | },
77 | data: {
78 | size: item
79 | }
80 | })
81 | } else {
82 | await prisma.createSize({
83 | size: item,
84 | product: {
85 | connect: {
86 | id: productId
87 | }
88 | }
89 | })
90 | }
91 | })
92 | return true;
93 | } catch {
94 | return false;
95 | }
96 | },
97 | // 색상 수정
98 | editColor: (_, args) => {
99 | const { productId, colorId, colorValue } = args;
100 | try {
101 | colorValue.map(async (item,index) => {
102 | if(colorId.length > index) {
103 | await prisma.updateColor({
104 | where: {
105 | id: colorId[index]
106 | },
107 | data: {
108 | color: item
109 | }
110 | })
111 | } else {
112 | await prisma.createColor({
113 | color: item,
114 | product: {
115 | connect: {
116 | id: productId
117 | }
118 | }
119 | })
120 | }
121 | })
122 | return true;
123 | } catch {
124 | return false;
125 | }
126 | },
127 | // 재고 수정
128 | editStock: (_, args) => {
129 | const { productId, stockId, stockValue } = args;
130 | try {
131 | stockValue.map(async(item, index) => {
132 | if(stockId.length > index) {
133 | await prisma.updateStock({
134 | where: {
135 | id: stockId[index]
136 | },
137 | data: {
138 | stock: item
139 | }
140 | })
141 | } else {
142 | await prisma.createStock({
143 | stock: item,
144 | product: {
145 | connect: {
146 | id: productId
147 | }
148 | }
149 | })
150 | }
151 | })
152 | return true;
153 | } catch {
154 | return false;
155 | }
156 | },
157 | // 상품디테일 파일 수정
158 | editProductDetailFile: (_, args) => {
159 | const { id, productDetailFile } = args;
160 | return prisma.updateProductDetailFile({
161 | data: {
162 | productDetailFile
163 | },
164 | where: {
165 | id
166 | }
167 | })
168 | },
169 | // 사이즈 파일 수정
170 | editProductSizeFile: async (_, args) => {
171 | const { id, productSizeFile } = args;
172 | return prisma.updateProductSizeFile({
173 | data: {
174 | productSizeFile
175 | },
176 | where: {
177 | id
178 | }
179 | })
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Payment/PaymentPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { H2, TitleDiv } from "../Product/ProductPresenter";
4 | import ProductTable from "../../Components/ProductTable";
5 | import { Article } from "../Mypage/MyPagePresenter";
6 | import Loader from "../../Components/Loader";
7 | import Button from "../../Components/Button";
8 |
9 | const Payment = styled.section`
10 | min-height: 79vh;
11 | max-width: ${props => props.theme.maxWidth};
12 | margin: 0 auto;
13 | width: 100%;
14 | `;
15 |
16 | const PaymentWrapper = styled.div`
17 | #responsiveTotalDiv {
18 | @media (min-width: 600px) {
19 | display: none;
20 | }
21 | }
22 | @media (max-width: 1024px) {
23 | padding: 0 50px;
24 | }
25 | @media (max-width: 600px) {
26 | padding: 0 20px;
27 | }
28 | @media (max-width: 350px) {
29 | padding: 0 10px;
30 | }
31 | `;
32 |
33 | const H3 = styled.h3`
34 | font-size: 24px;
35 | font-weight: 600;
36 | `;
37 |
38 | const Form = styled.form`
39 | margin-top: 15px !important;
40 | border-top: 1px solid #ccc;
41 | width: 100%;
42 | `;
43 |
44 | const FormDiv = styled.div`
45 | padding: 15px 10px;
46 | border-bottom: 1px solid #ccc;
47 | `;
48 |
49 | const InputTitleDiv = styled.div`
50 | width: 30%;
51 | text-align: center;
52 | font-size: 18px;
53 | display: inline-block;
54 | `;
55 |
56 | const InputDiv = styled.div`
57 | width: 70%;
58 | display: inline-block;
59 | `;
60 |
61 | const InputDiv2 = styled(InputDiv)`
62 | margin-left: 30%;
63 | `;
64 |
65 | const ButtonDiv = styled.div`
66 | display: flex;
67 | margin: 30px 0;
68 | justify-content: center;
69 | `;
70 |
71 | const PayBtn = styled.button`
72 | padding: 10px 0;
73 | border: none;
74 | border-radius: 5px;
75 | width: 100%;
76 | background-color: ${props => props.theme.confirmColor};
77 | cursor: pointer;
78 | color: #fff;
79 | `;
80 |
81 | const CompleteArticle = styled.article`
82 | padding-top: 200px;
83 | `;
84 |
85 | const CompleteDiv = styled.div`
86 | width: 100%;
87 | height: 100%;
88 | ${props => props.theme.whiteBox};
89 | box-shadow: 0px 0px 0px rgba(0,0,0,0), 0px 0px 10px rgba(0,0,0,0.1);
90 | padding: 100px 0 50px 0;
91 | `;
92 |
93 | const CompleteTextDiv = styled.div`
94 | display: flex;
95 | justify-content: center;
96 | h2 {
97 | font-size: 45px;
98 | font-weight: 600;
99 | @media (max-width: 600px) {
100 | font-size: 32px;
101 | }
102 | @media (max-width: 400px) {
103 | font-size: 27px;
104 | }
105 | }
106 | span {
107 | color: firebrick;
108 | }
109 | `;
110 |
111 | const CompleteButtonDiv = styled.div`
112 | display: flex;
113 | justify-content: center;
114 | padding-top: 50px;
115 | button {
116 | width: auto !important;
117 | background-color: ${props => props.theme.confirmColor} !important;
118 | color: #fff !important;
119 | }
120 | `;
121 |
122 | export default ({
123 | paymentData,
124 | paymentLoading,
125 | total,
126 | name,
127 | zipCode,
128 | address,
129 | addressDetail,
130 | email,
131 | phone,
132 | openPay,
133 | complete,
134 | goHome
135 | }) => {
136 | return (
137 |
138 |
139 | {paymentLoading === true && }
140 | {paymentLoading === false && !complete && setTimeout(() => , 2000) &&(
141 | <>
142 |
143 |
144 | Order
145 |
146 |
150 |
151 |
152 | Order Information
153 |
183 |
184 |
185 | openPay()}>결제하기
186 |
187 | >
188 | )}
189 | {complete && (
190 |
191 |
192 |
193 | 주문이 완료되었습니다.
194 |
195 |
196 |
198 |
199 |
200 | )}
201 |
202 |
203 |
204 | )
205 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Admin/AdminPresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { MyPage, MyTitleDiv, MyNavDiv } from "../Mypage/MyPagePresenter";
4 | import { H2 } from "../Product/ProductPresenter";
5 | import Button from './../../Components/Button';
6 | import EditProduct from "../../Components/EditProduct";
7 | import Loader from "../../Components/Loader";
8 | import ProductEditForm from "../../Components/ProductEditForm";
9 |
10 | const Admin = styled(MyPage)``;
11 |
12 | const AdminWrapper = styled.div`
13 | @media (max-width: 1024px) {
14 | padding: 0 50px;
15 | }
16 | @media (max-width: 700px) {
17 | padding: 0 20px;
18 | }
19 | @media (max-width: 350px) {
20 | padding: 0 10px;
21 | }
22 | `;
23 |
24 | const Article = styled.article``;
25 |
26 | const NavDiv = styled(MyNavDiv)`
27 | grid-template-columns: repeat(2, 1fr);
28 | `;
29 |
30 | const EditBox = styled.article`
31 | padding: 30px 0;
32 | `;
33 |
34 | const EditGrid = styled.div`
35 | display: grid;
36 | grid-template-columns: repeat(4, 1fr);
37 | grid-column-gap: 15px;
38 | grid-row-gap: 25px;
39 | position: relative;
40 | @media (max-width: 900px) {
41 | grid-template-columns: repeat(3, 1fr);
42 | }
43 | @media (max-width: 700px) {
44 | grid-template-columns: repeat(2, 1fr);
45 | }
46 | `;
47 |
48 | const Modal = styled.div`
49 | display: none; /* Hidden by default */
50 | position: fixed; /* Stay in place */
51 | z-index: 1; /* Sit on top */
52 | left: 0;
53 | top: 0;
54 | width: 100%; /* Full width */
55 | height: 100%; /* Full height */
56 | overflow: auto; /* Enable scroll if needed */
57 | background-color: rgb(0,0,0); /* Fallback color */
58 | background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
59 | `;
60 |
61 | const ModalContent = styled.div`
62 | background-color: #fefefe;
63 | margin: 20% auto;
64 | padding: 20px;
65 | border: 1px solid #888;
66 | width: 90%;
67 | position: relative;
68 | #close {
69 | position: absolute;
70 | right: 10px;
71 | top: 5px;
72 | color: #aaa;
73 | font-size: 28px;
74 | font-weight: bold;
75 | cursor: pointer;
76 | }
77 | @media(max-width: 768px) {
78 | margin: 30% auto;
79 | }
80 | @media(max-width: 600px) {
81 | margin: 40% auto;
82 | }
83 | @media(max-width: 486px) {
84 | margin: 60% auto;
85 | }
86 | `;
87 |
88 | export default ({
89 | logOut,
90 | customFileBtn,
91 | selectChange,
92 | subSelectChange,
93 | smallClassification,
94 | addTable,
95 | onSubmit,
96 | previewImg,
97 | tab,
98 | clickTab,
99 | editData,
100 | editClick,
101 | deleteClick,
102 | editData2,
103 | previewEditImg,
104 | customEditFileBtn
105 | }) => {
106 | return (
107 |
108 |
109 |
110 | Admin
111 |
113 |
114 |
120 |
126 |
127 | {tab === "enrollment" && (
128 |
129 |
138 |
139 | )}
140 | {tab === "edit" && editData === undefined && }
141 | {tab === "edit" && editData && (
142 |
143 |
144 | {editData.seeProductAll.map(item => (
145 |
159 | ))}
160 |
161 |
162 | )}
163 |
164 |
165 | ×
166 | {editData2 === undefined && }
167 | {editData2 !== undefined && (
168 |
178 | )}
179 |
180 |
181 |
182 |
183 | )
184 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Store/StoreContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import StorePresenter from "./StorePresenter";
3 | import { useMutation } from "react-apollo-hooks";
4 | import { SEE_ALL_BESTITEM, SEE_PRODUCT } from "./StoreQueries";
5 |
6 | export default () => {
7 | let istopToggle = false;
8 | let isBottomToggle = false;
9 | let isMenuListToggle = false;
10 |
11 | const [title, setTitle] = useState("ALL");
12 | const [best, setBest] = useState([]);
13 | const [all, setAll] = useState([]);
14 |
15 | const settings = {
16 | dots: false,
17 | infinite: false,
18 | swipe: false,
19 | arrows: false,
20 | autoplay: false,
21 | slidesToShow: 1,
22 | rows: 4,
23 | responsive: [
24 | {
25 | breakpoint: 1024,
26 | settings: {
27 | dots: true,
28 | slidesToShow: 3,
29 | sliesToScroll: 1,
30 | arrows: true,
31 | rows: 1
32 | }
33 | },
34 | {
35 | breakpoint: 768,
36 | settings: {
37 | dots: true,
38 | slidesToShow: 2,
39 | slideToScroll: 2,
40 | arrows: true,
41 | rows: 1
42 | }
43 | },
44 | {
45 | breakpoint: 600,
46 | settings: {
47 | dots: true,
48 | arrows: false,
49 | swipe: true,
50 | slidesToShow: 2,
51 | slideToScroll: 2,
52 | rows: 1
53 | }
54 | }
55 | ]
56 | }
57 |
58 | const topToggle = () => {
59 | const TopDiv = document.getElementById("top");
60 |
61 | if(istopToggle) {
62 | istopToggle = false;
63 | TopDiv.style.display = "none";
64 | } else {
65 | istopToggle = true;
66 | TopDiv.style.display = "block";
67 | }
68 | }
69 | const bottomToggle = () => {
70 | const bottomDiv = document.getElementById("bottom");
71 |
72 | if(isBottomToggle) {
73 | isBottomToggle = false;
74 | bottomDiv.style.display = "none";
75 | } else {
76 | isBottomToggle = true;
77 | bottomDiv.style.display = "block";
78 | }
79 | }
80 |
81 | const menuListToggle = () => {
82 | const menuListDiv = document.getElementById("menuList");
83 |
84 |
85 | if(matchMedia("(max-width: 600px)").matches) {
86 | if(isMenuListToggle) {
87 | isMenuListToggle = false;
88 | menuListDiv.style.display = "none";
89 | } else {
90 | isMenuListToggle = true;
91 | menuListDiv.style.display = "block";
92 | }
93 | }
94 | }
95 |
96 | // with가 600 이상이 되었을 때 menuList가 보이게끔 하기 위함
97 | // 600 이하에서 menuList를 접은상태에서 resize를 하게되면 menuList가 접혀있는상태가 되어 있으므로
98 | const onResize = () => {
99 | if(window.innerWidth >= 600) {
100 | const menuListDiv = document.getElementById("menuList");
101 | menuListDiv.style.display = "block";
102 | }
103 | }
104 |
105 | useEffect(() => {
106 | window.addEventListener("resize", onResize);
107 | return () => window.removeEventListener("resize", onResize);
108 | },[])
109 |
110 | const menuClick = (name) => {
111 | setTitle(name);
112 | }
113 |
114 | const first = 8;
115 | const [skip, setSkip] = useState(0);
116 | const [pLoading, setPloading] = useState(false);
117 | const [dataTemp, setDataTemp] = useState([]);
118 |
119 | // 페이징
120 | const clickMore = () => {
121 | setPloading(true);
122 | setSkip(skip+first);
123 | }
124 |
125 | useEffect(() => {
126 | if(skip !== 0) {
127 | seeAllItemFunction();
128 | }
129 | // eslint-disable-next-line react-hooks/exhaustive-deps
130 | },[skip])
131 |
132 |
133 |
134 |
135 | const [sort, setSort] = useState("");
136 | const [mainCategory, setMaincategory] = useState();
137 | const [subCategory, setSubCategory] = useState();
138 |
139 | // ALL
140 | const seeAllBestItemMutation = useMutation(SEE_ALL_BESTITEM, {
141 | variables: {
142 | sort,
143 | mainCategory,
144 | subCategory
145 | }
146 | })
147 |
148 | const seeAllItemMutation = useMutation(SEE_PRODUCT, {
149 | variables: {
150 | sort,
151 | mainCategory,
152 | subCategory,
153 | first,
154 | skip
155 | }
156 | })
157 |
158 | const seeAllItemFunction = async() => {
159 | const { data } = await seeAllItemMutation();
160 | if (data) {
161 | setDataTemp(data);
162 | setAll([...all, ...data.seeProductAll]);
163 | setPloading(false);
164 | }
165 | }
166 |
167 | const seeAllBestItemFunction = async() => {
168 | const { data } = await seeAllBestItemMutation();
169 | if(data) {
170 | setBest(data);
171 | seeAllItemFunction();
172 | }
173 | }
174 |
175 | useEffect(() => {
176 | setBest([]);
177 | setAll([]);
178 | if(title === "ALL") {
179 | setSort("all");
180 | setMaincategory("");
181 | setSubCategory("");
182 | setSkip(0);
183 | } else if(title === "Top ALL") {
184 | setSort("all");
185 | setMaincategory("상의");
186 | setSubCategory("");
187 | setSkip(0);
188 | } else if (title === "BOTTOM ALL") {
189 | setSort("all");
190 | setMaincategory("하의");
191 | setSubCategory("");
192 | setSkip(0);
193 | } else if (title === "T-Shirt") {
194 | setSort("all");
195 | setMaincategory("상의");
196 | setSubCategory("티셔츠");
197 | setSkip(0);
198 | } else if (title === "Shirt") {
199 | setSort("all");
200 | setMaincategory("상의");
201 | setSubCategory("셔츠");
202 | setSkip(0);
203 | } else if (title === "Jean") {
204 | setSort("all");
205 | setMaincategory("하의");
206 | setSubCategory("청바지");
207 | setSkip(0);
208 | } else if (title === "Slacks") {
209 | setSort("all");
210 | setMaincategory("하의");
211 | setSubCategory("슬랙스");
212 | setSkip(0);
213 | }
214 | // eslint-disable-next-line react-hooks/exhaustive-deps
215 | },[title])
216 |
217 | useEffect(() => {
218 | if(title !== "" && sort !== "") {
219 | seeAllBestItemFunction();
220 | }
221 | // eslint-disable-next-line react-hooks/exhaustive-deps
222 | },[mainCategory, subCategory])
223 |
224 | return (
225 |
238 | )
239 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Payment/PaymentContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import PaymentPresenter from "./PaymentPresenter";
3 | import { SEE_PAYMENT, EDIT_STOCK, EDIT_NUMBEROFSALES, ADD_BUYLIST } from './PaymentQueries';
4 | import { useQuery, useMutation } from "react-apollo-hooks";
5 | import { ME } from './../../Components/SharedQueries';
6 | import { DELETE_CART } from './../Mypage/MyPageQueries';
7 |
8 | export default ({history}) => {
9 | let IMP = window.IMP;
10 | IMP.init(process.env.REACT_APP_IMP_CODE);
11 |
12 | const { loading, data } = useQuery(SEE_PAYMENT, {fetchPolicy: "network-only"});
13 | const { loading:meLoading, data:meData } = useQuery(ME);
14 |
15 | const [total, setTotal] = useState(0);
16 | const [name, setName] = useState("");
17 | const [zipCode, setZipcode] = useState("");
18 | const [address, setAddress] = useState("");
19 | const [addressDetail, setAddressDetail] = useState("");
20 | const [email, setEmail] = useState("");
21 | const [phone, setPhone] = useState("");
22 | const [complete, setComplete] = useState(false);
23 |
24 | const totalFunc = (a, b) => a + b;
25 |
26 | const [payProductName, setPayProductName] = useState("");
27 | useEffect(() => {
28 | if (loading === false) {
29 | const countTemp = [];
30 | const totalarrTemp = [];
31 | const totalTemp = [];
32 | data.seePayment.map(item => {
33 | item.product.map(product => (
34 | totalarrTemp.push(product.price)
35 | ))
36 | item.count.map(count => (
37 | countTemp.push(count.count)
38 | ))
39 | return null;
40 | })
41 | countTemp.map((count, index) => (
42 | totalTemp.push(count * totalarrTemp[index])
43 | ))
44 | if (totalTemp.length === 0) {
45 | setTotal(0);
46 | } else {
47 | setTotal(totalTemp.reduce(totalFunc));
48 | }
49 | if(data.seePayment.length > 1) {
50 | setPayProductName(data.seePayment[0].product[0].name+ " 외 " + (data.seePayment.length-1) + "개")
51 | } else {
52 | if(loading === false && data && data.seePayment.length === 1) {
53 | setPayProductName(data.seePayment[0].product[0].name)
54 | }
55 | }
56 | }
57 | // eslint-disable-next-line react-hooks/exhaustive-deps
58 | }, [loading, data])
59 |
60 | useEffect(() => {
61 | if(meLoading === false) {
62 | setName(meData.me.name);
63 | setZipcode(meData.me.zipCode);
64 | setAddress(meData.me.address);
65 | setAddressDetail(meData.me.addressDetail);
66 | setEmail(meData.me.email);
67 | setPhone(meData.me.phone);
68 | }
69 | // eslint-disable-next-line react-hooks/exhaustive-deps
70 | },[meLoading])
71 |
72 | let stockId = [];
73 | let stockCount = [];
74 | let numberOfSalesTemp = [];
75 | let productIdTemp = [];
76 | let sizeTemp = [];
77 | let colorTemp = [];
78 | let quantityTemp = [];
79 | let cartId = [];
80 |
81 |
82 | const editStockMutation = useMutation(EDIT_STOCK, {
83 | variables: {
84 | id: stockId,
85 | stock: stockCount
86 | }
87 | })
88 |
89 | const editNumberOfSalesMutation = useMutation(EDIT_NUMBEROFSALES, {
90 | variables: {
91 | id: productIdTemp,
92 | saleCount: numberOfSalesTemp
93 | }
94 | })
95 |
96 | const addBuyListMutation = useMutation(ADD_BUYLIST, {
97 | variables: {
98 | product: productIdTemp,
99 | size: sizeTemp,
100 | color: colorTemp,
101 | quantity: quantityTemp
102 | }
103 | })
104 |
105 | const deleteCartMutation = useMutation(DELETE_CART, {
106 | variables: {
107 | id: cartId
108 | }
109 | })
110 |
111 | const deleteCartFunction = async () => {
112 | const { data } = await deleteCartMutation();
113 | if(data) {
114 | stockId = [];
115 | stockCount = [];
116 | numberOfSalesTemp = [];
117 | productIdTemp = [];
118 | sizeTemp = [];
119 | colorTemp = [];
120 | quantityTemp = [];
121 | cartId = [];
122 | setComplete(true);
123 | }
124 | }
125 |
126 | // 구매목록 추가
127 | const addBuyListFunction = async () => {
128 | const { data } = await addBuyListMutation();
129 | if (data) {
130 | // payment 제거, 장바구니 제거
131 | // => payment는 다른페이지로 이동하면 제거 됨
132 | // => 장바구니 에서 아이템 제거하고 구매완료페이지로 이동
133 | if(cartId.length !== 0) {
134 | deleteCartFunction();
135 | } else {
136 | stockId = [];
137 | stockCount = [];
138 | numberOfSalesTemp = [];
139 | productIdTemp = [];
140 | sizeTemp = [];
141 | colorTemp = [];
142 | quantityTemp = [];
143 | cartId = [];
144 | setComplete(true);
145 | }
146 |
147 | }
148 | }
149 |
150 | // 판매량 증가
151 | const editNumberFunction = async () => {
152 | const { data } = await editNumberOfSalesMutation();
153 | if (data) {
154 | addBuyListFunction();
155 | }
156 | }
157 |
158 |
159 | // 재고량 감소
160 | const editStockFunction = async () => {
161 | const { data } = await editStockMutation();
162 | if (data) {
163 | editNumberFunction();
164 | }
165 | }
166 |
167 | const openPay = async () => {
168 | IMP.request_pay({
169 | pg : 'html5_inicis',
170 | pay_method : 'card',
171 | merchant_uid : 'merchant_' + new Date().getTime(),
172 | name : payProductName,
173 | amount : 1000, // total
174 | buyer_email : email,
175 | buyer_name : name,
176 | buyer_tel : phone,
177 | buyer_addr : address + " " + addressDetail,
178 | buyer_postcode : zipCode
179 | }, function(rsp) {
180 | let msg = '결제가 완료되었습니다.';
181 | if ( rsp.success ) {
182 | // 결제가 완료되면 구매수량만큼 재고에서 감소시킨다.
183 | data.seePayment.map(async (item) => {
184 | productIdTemp.push(item.product[0].id);
185 | numberOfSalesTemp.push(item.product[0].numberOfSales + item.count[0].count);
186 | stockId.push(item.stock[0].id);
187 | stockCount.push(item.stock[0].stock - item.count[0].count);
188 | sizeTemp.push(item.size[0].id);
189 | colorTemp.push(item.color[0].id);
190 | quantityTemp.push(item.count[0].count);
191 | if(item.cart.length !== 0) {
192 | cartId.push(item.cart[0].id);
193 | }
194 | })
195 | editStockFunction();
196 | } else {
197 | msg = '결제에 실패하였습니다.';
198 | msg += '에러내용 : ' + rsp.error_msg;
199 | alert(msg);
200 | }
201 |
202 | });
203 | }
204 |
205 | const goHome = () => {
206 | history.push('/');
207 | }
208 |
209 |
210 | return (
211 |
225 | )
226 | }
--------------------------------------------------------------------------------
/Front-End/src/Routes/Mypage/MyPagePresenter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { TitleDiv, H2 } from "../Product/ProductPresenter";
4 | import Button from "../../Components/Button";
5 | import { Form } from "../Auth/AuthPresenter";
6 | import SignUpForm from "../../Components/SignUpForm";
7 | import Loader from "../../Components/Loader";
8 | import { Pagination } from "semantic-ui-react";
9 | import ProductTable from "../../Components/ProductTable";
10 | import { addComma } from "../../Components/SharedFunction";
11 |
12 | export const MyPage = styled.section`
13 | min-height: 79vh;
14 | overflow: hidden;
15 | max-width: ${props => props.theme.maxWidth};
16 | margin: 0 auto;
17 | width: 100%;
18 | `;
19 |
20 | const MyPageWrapper = styled.div`
21 | #cartOrderBtn {
22 | width: auto;
23 | padding: 10px 35px;
24 | float: right;
25 | margin: 20px 0;
26 | @media (max-width: 600px) {
27 | width: 100%;
28 | margin: 20px 0 60px 0;
29 | }
30 | }
31 | #responsiveTotalDiv {
32 | @media (min-width: 600px) {
33 | display: none;
34 | }
35 | }
36 | @media (max-width: 1024px) {
37 | padding: 0 50px;
38 | }
39 | @media (max-width: 600px) {
40 | padding: 0 20px;
41 | }
42 | @media (max-width: 350px) {
43 | padding: 0 10px;
44 | }
45 | `;
46 |
47 | const MyPageHeader = styled.header``;
48 |
49 | export const MyTitleDiv = styled(TitleDiv)`
50 | display: flex;
51 | justify-content: space-between;
52 | button {
53 | width: auto;
54 | }
55 | `;
56 |
57 | export const MyNavDiv = styled.div`
58 | display: grid;
59 | grid-template-columns: repeat(3, 1fr);
60 | padding: 30px 0 0;
61 | border-bottom: 1px solid #ccc;
62 | button {
63 | border: 1px solid transparent;
64 | border-bottom: 0;
65 | cursor: pointer;
66 | font-weight: 600;
67 | background-color: transparent;
68 | font-size: 18px;
69 | outline: none;
70 | padding: 15px;
71 | @media (max-width: 600px) {
72 | padding: 10px;
73 | }
74 | :hover {
75 | color: ${props => props.theme.confirmColor};
76 | }
77 | }
78 | `;
79 |
80 | export const Article = styled.article`
81 | margin-top: 60px;
82 | form {
83 | margin: 0 auto;
84 | }
85 | `;
86 |
87 | const BuyListTable = styled.table`
88 | width: 100%;
89 | tbody {
90 | text-align: center;
91 | tr {
92 | border-bottom: 1px solid #ccc;
93 | td {
94 | padding: 10px;
95 | }
96 | }
97 | }
98 | `;
99 |
100 | const Thead = styled.thead`
101 | background-color: ${props => props.theme.confirmColor};
102 | color: white;
103 | `;
104 |
105 | const Th = styled.th`
106 | padding: 10px;
107 | vertical-align: middle;
108 | `;
109 |
110 | const PageDiv = styled.div`
111 | display: flex;
112 | justify-content: center;
113 | align-items: center;
114 | margin-top: 60px;
115 | div {
116 | min-height: 0 !important;
117 | box-shadow: none !important;
118 | a{
119 | padding: 5px 7px !important;
120 | min-width: 0 !important;
121 | }
122 | }
123 | `;
124 |
125 | export default ({
126 | tab,
127 | clickTab,
128 | // 장바구니
129 | cartLoading,
130 | cartData,
131 | passCartId,
132 | allCheck,
133 | total,
134 | cartCountUp,
135 | cartCountDown,
136 | count,
137 | selectOrder,
138 | // 구매목록
139 | buyData,
140 | changePage,
141 | buyListLoading,
142 | pageNum,
143 | // 개인정보 수정
144 | onSubmit,
145 | name,
146 | email,
147 | password,
148 | confirmPassword,
149 | zipCode,
150 | address,
151 | addressDetail,
152 | phone1,
153 | phone2,
154 | phone3,
155 | open,
156 | setOpen,
157 | handleAddress,
158 | // 로그아웃
159 | logOut
160 | }) => {
161 | return (
162 |
163 |
164 |
165 |
166 | My Page
167 |
169 |
170 |
176 |
182 |
188 |
189 |
190 | {tab === "cart" ?
191 | <>
192 |
193 | {cartLoading === true && }
194 | {cartLoading === false && setTimeout(() => , 1500) && (
195 |
205 | )}
206 |
207 | >
208 | :
209 | tab === "buyList" ?
210 |
211 | {buyData === "" && }
212 | {buyData !== "" &&
213 |
214 |
215 |
216 | | num |
217 | Name(option) |
218 | Price |
219 | Quantity |
220 |
221 |
222 |
223 | {buyData.seeBuyList2.map((item, index) => (
224 | item.product.map(product => (
225 |
226 | | {index} |
227 |
228 | {product.name}
229 | |
230 |
231 | ₩{addComma(product.price*item.quantity[index].quantity)}
232 | |
233 |
234 | {item.quantity[index].quantity}
235 | |
236 |
237 | ))
238 | ))}
239 |
240 |
241 | }
242 | {buyListLoading === false &&
243 |
244 | changePage(e.target.attributes.value.value)}
253 | />
254 |
255 | }
256 | :
257 |
258 |
277 |
278 | }
279 |
280 |
281 | )
282 | }
--------------------------------------------------------------------------------
/Front-End/src/Components/ProductTable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from 'prop-types';
4 | import { Link } from "react-router-dom";
5 | import Button from "../Components/Button";
6 | import { SelectedCount, SelectedCountTextDiv, SelectedCountBtnDiv } from "../Routes/Product/ProductPresenter";
7 | import { addComma } from "./SharedFunction";
8 |
9 | const Table = styled.table`
10 | width: 100%;
11 | img {
12 | width: 130px;
13 | height: 150px;
14 | }
15 | td, th {
16 | padding: 10px;
17 | vertical-align: middle;
18 | }
19 | td {
20 | text-align: center;
21 | vertical-align: middle;
22 | }
23 | tbody {
24 | tr {
25 | border-bottom: 1px solid #ccc;
26 | }
27 | #emptyTd {
28 | padding: 50px 0;
29 | @media (max-width: 600px) {
30 | display: block;
31 | border-radius: 10px;
32 | background-color: transparent;
33 | border-color: #ccc;
34 | }
35 | }
36 | }
37 | tfoot {
38 | tr {
39 | border-bottom: 1px solid #ccc;
40 | }
41 | @media (max-width: 600px) {
42 | display: none;
43 | }
44 | }
45 |
46 | @media (max-width: 600px) {
47 | display: block;
48 | thead {
49 | display: none;
50 | }
51 | img {
52 | width: 100px;
53 | }
54 | tr {
55 | display: block;
56 | margin: 0.5rem 0;
57 | padding: 0;
58 | width: 100%;
59 | position:relative;
60 | border-radius: 10px;
61 | border: 1px solid #ccc;
62 | @media (max-width: 350px) {
63 | width: 95%;
64 | }
65 | }
66 | tbody {
67 | display: flex;
68 | flex-direction: column;
69 | justify-content: center;
70 | align-items: center;
71 | @media (max-width: 350px) {
72 | align-items: stretch;
73 | }
74 | }
75 | td:first-child {
76 | position: relative;
77 | top: 0;
78 | transform: translateY(0);
79 | width: 100%;
80 | background-color: ${props => props.theme.confirmColor};
81 | border-radius: 10px 10px 0 0;
82 | border-color: ${props => props.theme.confirmColor};
83 | border : 1px solid;
84 | }
85 | td {
86 | display: block;
87 | :nth-child(2) {
88 | position:absolute !important;
89 | left: 0;
90 | width: auto !important;
91 | height: 180px;
92 | ;
93 | border-right: 1px solid #ccc;
94 | }
95 | }
96 | td {
97 | :not(:first-child):not(:nth-child(2)) {
98 | position: relative;
99 | margin-left: 125px;
100 | padding: 5px 1em;
101 | text-align: left;
102 | width: auto;
103 | }
104 | }
105 | td:not(:first-child):before {
106 | content: '';
107 | display: block;
108 | left: 0;
109 | position: relative;
110 | font-size: .8em;
111 | padding-bottom: 0.3em;
112 | text-align: left;
113 | color: darkgray;
114 | }
115 | td:nth-child(3):before {
116 | content: 'Name (Option)';
117 | }
118 | td:nth-child(4):before {
119 | content: 'Price';
120 | }
121 | td:nth-child(5):before {
122 | content: 'Quantity';
123 | }
124 | }
125 | `;
126 |
127 | const Thead = styled.thead`
128 | background-color: ${props => props.theme.confirmColor};
129 | color: white;
130 | `;
131 |
132 | const Th = styled.th`
133 | padding: 10px;
134 | vertical-align: middle;
135 | `;
136 |
137 | const BoldTd = styled.td`
138 | font-weight: 600;
139 | `;
140 |
141 | const SelectedCount2 = styled(SelectedCount)`
142 | @media (max-width: 600px) {
143 | margin-left: 0;
144 | justify-content: left;
145 | }
146 | `;
147 |
148 | const SelectedCountTextDiv2 = styled(SelectedCountTextDiv)`
149 | padding: 5px 15px;
150 | @media (max-width: 600px) {
151 | width: auto;
152 | }
153 | `;
154 |
155 | const ResponsiveTotalDiv = styled.div`
156 | font-weight: 600;
157 | `;
158 |
159 | const PaymentTd = styled.td`
160 | @media (min-width: 600px) {
161 | display: none;
162 | }
163 | `;
164 |
165 | const ButtonDiv = styled.div`
166 | button {
167 | background-color: ${props => props.theme.confirmColor};
168 | color: #fff;
169 | }
170 | `;
171 |
172 | const ProductTable =({
173 | allCheck,
174 | cartData,
175 | count,
176 | cartCountUp,
177 | cartCountDown,
178 | passCartId,
179 | total,
180 | selectOrder,
181 | paymentData
182 | }) => {
183 | return (
184 | <>
185 |
277 |
278 | Total : ₩{addComma(total)}
279 |
280 | {cartData &&
281 |
282 |
284 | }
285 | >
286 | )
287 | }
288 |
289 | ProductTable.propTypes = {
290 | allCheck: PropTypes.func,
291 | cartData: PropTypes.object,
292 | count: PropTypes.array,
293 | cartCountUp: PropTypes.func,
294 | cartCountDown: PropTypes.func,
295 | passCartId: PropTypes.func,
296 | total: PropTypes.number,
297 | selectOrder: PropTypes.func
298 | }
299 |
300 | export default ProductTable;
--------------------------------------------------------------------------------
/Front-End/src/Components/ProductEditForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 | import { Image, Plus } from './Icons';
5 | import Input from "./Input";
6 | import { Select } from './SignUpForm';
7 |
8 | const Form = styled.form``;
9 |
10 | const ProductBasicDiv = styled.div`
11 | display:flex;
12 | justify-content: center;
13 | padding: 30px 0;
14 | border-bottom: 1px solid #ccc;
15 | @media(max-width: 480px) {
16 | flex-direction: column;
17 | align-items: center;
18 | }
19 | `;
20 |
21 | const ImageDiv = styled.div`
22 | width: 30%;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | flex-direction: column;
27 | @media (max-width: 480px) {
28 | margin-bottom: 30px;
29 | }
30 | `;
31 |
32 | const Preview = styled.div`
33 | width: 150px;
34 | height: 200px;
35 | background-color: white;
36 | ${props => props.theme.whiteBox};
37 | border-radius: 0;
38 | `;
39 |
40 |
41 | const PreviewImage = styled.img`
42 | width: 100%;
43 | height: 100%;
44 | `;
45 |
46 | const ImageButton = styled.div`
47 | width: 150px;
48 | background-color: #eeeeee;
49 | border: none;
50 | border-radius: 20px;
51 | cursor: pointer;
52 | padding: 10px;
53 | margin-top: 10px;
54 | display: flex;
55 | justify-content: center;
56 | align-items: center;
57 | span {
58 | &:first-child {
59 | margin-right: 10px;
60 | }
61 | }
62 | `;
63 |
64 | const BasicDiv = styled.div`
65 | width: 65%;
66 | display: flex;
67 | flex-direction: column;
68 | @media (max-width: 480px) {
69 | width: 80%;
70 | }
71 | input {
72 | margin-bottom: 15px;
73 | @media (max-width: 720px) {
74 | margin-left: 20px;
75 | }
76 | }
77 | select {
78 | @media (max-width: 720px) {
79 | margin-left: 20px;
80 | }
81 | }
82 | `;
83 |
84 | const SortDiv = styled.div`
85 | display: flex;
86 | justify-content: space-between;
87 | select {
88 | padding: 7px;
89 | width: 45%;
90 | }
91 | `;
92 |
93 | const OptionTitleDiv = styled.div`
94 | margin: 30px 0;
95 | h4 {
96 | font-size: 14px;
97 | font-weight: 600;
98 | }
99 | `;
100 |
101 | const OptionDiv = styled.div`
102 | table {
103 | width: 100%;
104 | border-collapse: collapse;
105 | border-bottom: 1px solid #ccc;
106 | tr {
107 | border-top: 1px solid #ccc;
108 | }
109 | th, td {
110 | border-right: 1px solid #ccc;
111 | text-align: center;
112 | &:last-child {
113 | border-right: 0;
114 | }
115 | }
116 | thead {
117 | tr {
118 | background-color: ${props => props.theme.confirmColor};
119 | color: #fff;
120 | }
121 | th {
122 | padding: 10px 0;
123 | }
124 | }
125 | tbody {
126 | td {
127 | margin: 5px 0;
128 | }
129 | input {
130 | width: 100%;
131 | height: 100%;
132 | border: none;
133 | outline-style: none;
134 | text-align: center;
135 |
136 | }
137 | }
138 | }
139 | `;
140 |
141 | const PlusDiv = styled.div`
142 | display: flex;
143 | justify-content: center;
144 | align-items: center;
145 | margin-top: 15px;
146 | `;
147 |
148 | const EnrollmentButton = styled.button`
149 | width: 100%;
150 | border: none;
151 | border-radius: 5px;
152 | padding: 8px 0;
153 | background-color: ${props => props.theme.confirmColor};
154 | color: #fff;
155 | cursor: pointer;
156 | margin: 50px 0 20px;
157 | `;
158 |
159 |
160 |
161 |
162 | const ProductEditForm = ({
163 | onSubmit,
164 | previewImg,
165 | customFileBtn,
166 | selectChange,
167 | subSelectChange,
168 | smallClassification,
169 | addTable,
170 | editData2
171 | }) => {
172 | return (
173 | <>
174 | {editData2 === undefined ? (
175 |
251 | ) : (
252 |
310 | )}
311 | >
312 | )
313 | }
314 |
315 | ProductEditForm.propTypes = {
316 | onSubmit: PropTypes.func,
317 | previewImg: PropTypes.object,
318 | customFileBtn: PropTypes.func,
319 | selectChange: PropTypes.func,
320 | subSelectChange: PropTypes.func,
321 | smallClassification: PropTypes.array,
322 | addTable: PropTypes.func,
323 | editData2: PropTypes.object
324 | }
325 |
326 | export default ProductEditForm;
--------------------------------------------------------------------------------