├── munetic_admin ├── src │ ├── types │ │ └── user.d.ts │ ├── vite-env.d.ts │ ├── pages │ │ ├── HomePage.tsx │ │ ├── LoginPage.tsx │ │ ├── PasswordChangePage.tsx │ │ ├── LessonInfoPage.tsx │ │ ├── UserInfoPage.tsx │ │ ├── AdminUserInfoPage.tsx │ │ ├── UserListPage.tsx │ │ ├── LessonListPage.tsx │ │ ├── CommentListPage.tsx │ │ ├── EditTermsPage.tsx │ │ ├── EditLicensePage.tsx │ │ └── AdminUserPage.tsx │ ├── components │ │ ├── Info │ │ │ ├── Common │ │ │ │ ├── Title.tsx │ │ │ │ ├── Item.tsx │ │ │ │ └── TextFields.tsx │ │ │ ├── User │ │ │ │ ├── AdminMemo.tsx │ │ │ │ ├── UserGrid.tsx │ │ │ │ ├── UserPosts.tsx │ │ │ │ └── OverView.tsx │ │ │ ├── Lesson │ │ │ │ ├── LessonContent.tsx │ │ │ │ ├── LessonInfo.tsx │ │ │ │ ├── WriterInfo.tsx │ │ │ │ └── LessonGrid.tsx │ │ │ └── InfoGrid.tsx │ │ ├── Button.tsx │ │ ├── Menu │ │ │ └── menuLists.ts │ │ ├── Table │ │ │ ├── Lesson │ │ │ │ ├── LessonTableCell.tsx │ │ │ │ └── LessonHeadCells.ts │ │ │ ├── AdminUser │ │ │ │ ├── AdminUserTableCell.tsx │ │ │ │ └── adminUserHeadCells.ts │ │ │ ├── Comment │ │ │ │ ├── CommentHeadCell.tsx │ │ │ │ └── CommentHeadCells.ts │ │ │ ├── User │ │ │ │ ├── UserTableCell.tsx │ │ │ │ └── userHeadCells.ts │ │ │ ├── MUITableToolbar.tsx │ │ │ └── MUITableRow.tsx │ │ ├── Inputs │ │ │ ├── CustomSelect.tsx │ │ │ ├── CustomInput.tsx │ │ │ └── CustomPasswordInput.tsx │ │ └── Routing.tsx │ ├── main.tsx │ ├── style │ │ └── GlobalStyle.tsx │ ├── contexts │ │ ├── info.tsx │ │ └── login.tsx │ └── App.tsx ├── .dockerignore ├── .gitignore ├── public │ └── img │ │ └── testImg.png ├── Dockerfile ├── .prettierrc ├── vite.config.ts ├── index.html ├── tsconfig.json ├── .eslintrc.js └── package.json ├── munetic_app ├── .dockerignore ├── jest.setup.js ├── src │ ├── vite-env.d.ts │ ├── lib │ │ ├── api │ │ │ ├── category.ts │ │ │ ├── etc.ts │ │ │ ├── client.ts │ │ │ ├── bookmark.ts │ │ │ ├── like.ts │ │ │ ├── search.ts │ │ │ ├── auth.ts │ │ │ ├── lesson.ts │ │ │ ├── profile.ts │ │ │ └── comment.ts │ │ ├── auth │ │ │ ├── loginCheck.ts │ │ │ ├── logout.ts │ │ │ └── vaildCheck.ts │ │ ├── getYoutubeId.ts │ │ └── getCategoriesByMap.ts │ ├── components │ │ ├── Bar.tsx │ │ ├── Wrapper.tsx │ │ ├── setting │ │ │ ├── Terms.tsx │ │ │ └── License.tsx │ │ ├── common │ │ │ ├── ToggleBtn.tsx │ │ │ ├── Button.tsx │ │ │ ├── Pagination.tsx │ │ │ ├── Select.tsx │ │ │ └── BottomMenu.tsx │ │ ├── ui │ │ │ ├── SnsButtons.tsx │ │ │ └── SwitchWithLabel.tsx │ │ ├── like │ │ │ ├── MyLikeLessons.tsx │ │ │ └── LikeButton.tsx │ │ ├── media │ │ │ └── VideoEmbed.tsx │ │ ├── lesson │ │ │ └── CategoryContainer.tsx │ │ ├── bookmark │ │ │ └── BookmarkButton.tsx │ │ └── comment │ │ │ └── CommentTop.tsx │ ├── pages │ │ ├── SearchPage.tsx │ │ ├── HomePage.tsx │ │ ├── SettingPage.tsx │ │ ├── setting │ │ │ ├── PolicyPage.tsx │ │ │ ├── LicensePage.tsx │ │ │ ├── HelpPage.tsx │ │ │ ├── AboutusPage.tsx │ │ │ └── ContactPage.tsx │ │ ├── profile │ │ │ ├── ViewMyLikesPage.tsx │ │ │ ├── EditProfilePage.tsx │ │ │ ├── ViewProfilePage.tsx │ │ │ ├── ManageProfilePage.tsx │ │ │ ├── EditTutorProfilePage.tsx │ │ │ └── ViewTutorProfilePage.tsx │ │ ├── lesson │ │ │ ├── ClassPage.tsx │ │ │ ├── WriteClassPage.tsx │ │ │ ├── ManageClassPage.tsx │ │ │ ├── CategoryPage.tsx │ │ │ └── ClassListPage.tsx │ │ ├── bookmarks │ │ │ └── ViewMyBookmarksPage.tsx │ │ └── auth │ │ │ ├── RegisterPage.tsx │ │ │ └── LoginPage.tsx │ ├── types │ │ ├── enums.ts │ │ ├── categoryData.d.ts │ │ ├── commentWriteData.d.ts │ │ ├── commentTopData.d.ts │ │ ├── userSignupData.d.ts │ │ ├── lessonLikeData.d.ts │ │ ├── userData.d.ts │ │ ├── tutorInfoData.d.ts │ │ ├── lessonData.d.ts │ │ └── commentData.d.ts │ ├── tests │ │ ├── components │ │ │ ├── pages │ │ │ │ └── lesson │ │ │ │ │ └── CategoryPage.test.tsx │ │ │ ├── common │ │ │ │ └── Button.test.tsx │ │ │ └── Bar.test.tsx │ │ ├── App.test.tsx │ │ └── Home.test.tsx │ ├── style │ │ ├── palette.ts │ │ └── GlobalStyle.ts │ ├── context │ │ └── Contexts.tsx │ ├── main.tsx │ └── App.tsx ├── .gitignore ├── public │ └── img │ │ ├── testImg.png │ │ └── basicProfileImg.png ├── Dockerfile ├── .prettierrc ├── index.html ├── vite.config.ts ├── jest.config.js ├── tsconfig.json ├── .eslintrc.js ├── README.md └── package.json ├── munetic_express ├── .dockerignore ├── .gitignore ├── src │ ├── tests │ │ ├── dummy │ │ │ ├── errResponse.json │ │ │ ├── userInfo.json │ │ │ ├── userInstance.ts │ │ │ └── userProfileInstance.ts │ │ ├── unit │ │ │ ├── modules.unit.test.ts │ │ │ └── user.service.unit.test.ts │ │ ├── 20211217154104-Category.js │ │ ├── db │ │ │ └── lessonseeds.ts │ │ └── integration │ │ │ └── auth.int.test.ts │ ├── server.ts │ ├── modules │ │ ├── types.ts │ │ ├── errorResponse.ts │ │ ├── errorHandler.ts │ │ ├── imgCreateMiddleware.ts │ │ ├── jwt.admin.strategy.ts │ │ ├── admin.strategy.ts │ │ ├── local.strategy.ts │ │ ├── reshape.ts │ │ ├── jwt.local.strategy.ts │ │ └── jwt.ts │ ├── routes │ │ ├── category.routes.ts │ │ ├── etc.routes.ts │ │ ├── admin │ │ │ ├── etc.routes.ts │ │ │ ├── comment.routes.ts │ │ │ ├── admin.routes.ts │ │ │ ├── auth.routes.ts │ │ │ ├── lesson.routes.ts │ │ │ └── user.routes.ts │ │ ├── search.routes.ts │ │ ├── bookmark.routes.ts │ │ ├── user.routes.ts │ │ ├── lessonLike.routes.ts │ │ ├── comment.routes.ts │ │ ├── auth.routes.ts │ │ ├── lesson.routes.ts │ │ └── index.ts │ ├── @types │ │ └── express.d.ts │ ├── swagger │ │ ├── swagger.ts │ │ └── apis │ │ │ └── category.yml │ ├── util │ │ └── addProperty.ts │ ├── models │ │ ├── initdata │ │ │ ├── etc.init.ts │ │ │ ├── category.init.ts │ │ │ └── user.init.ts │ │ ├── category.ts │ │ ├── etc.ts │ │ ├── bookmark.ts │ │ ├── lessonLike.ts │ │ └── comment.ts │ ├── types │ │ ├── controller │ │ │ └── tutorInfoData.d.ts │ │ └── service │ │ │ └── lesson.service.d.ts │ ├── config │ │ └── config.ts │ ├── controllers │ │ ├── category.controller.ts │ │ └── admin │ │ │ └── comment.controller.ts │ ├── service │ │ ├── category.service.ts │ │ ├── etc.service.ts │ │ └── tutorInfo.service.ts │ ├── mapping │ │ ├── LessonMapper.ts │ │ └── TutorInfoMapper.ts │ └── app.ts ├── Dockerfile ├── .prettierrc ├── jest.config.ts ├── .sequelizerc ├── .eslintrc.js ├── README.md ├── package.json └── seeders │ └── 3-Comment.js ├── munetic_proxy ├── Dockerfile ├── envsubst.sh └── templates │ ├── localhost.conf.template │ └── default.conf.template ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── deployment.yml ├── .env_template ├── munetic_database ├── Dockerfile └── my.cnf ├── network.yaml ├── network-main.yaml ├── network-develop.yaml ├── LICENSE ├── docs └── scenario.md └── .gitignore /munetic_admin/src/types/user.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /munetic_app/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | dist -------------------------------------------------------------------------------- /munetic_admin/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | dist 4 | -------------------------------------------------------------------------------- /munetic_app/jest.setup.js: -------------------------------------------------------------------------------- 1 | require('@testing-library/jest-dom'); 2 | -------------------------------------------------------------------------------- /munetic_express/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | dist -------------------------------------------------------------------------------- /munetic_admin/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /munetic_app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /munetic_proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.4 2 | COPY ./envsubst.sh /docker-entrypoint.d -------------------------------------------------------------------------------- /munetic_admin/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /munetic_app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .env 7 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | export default function HomePage() { 2 | return
HOME
; 3 | } 4 | -------------------------------------------------------------------------------- /munetic_express/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .env* 7 | !.env.sample 8 | -------------------------------------------------------------------------------- /munetic_express/src/tests/dummy/errResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": "서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", 3 | "data": {} 4 | } 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 개요 2 | 3 | ### 작업 사항 4 | 5 | ### 변경점 6 | 7 | ### 목적 8 | 9 | ### 스크린샷 (optional) 10 | -------------------------------------------------------------------------------- /munetic_app/public/img/testImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/innovationacademy-kr/slabs-munetic/HEAD/munetic_app/public/img/testImg.png -------------------------------------------------------------------------------- /munetic_admin/public/img/testImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/innovationacademy-kr/slabs-munetic/HEAD/munetic_admin/public/img/testImg.png -------------------------------------------------------------------------------- /munetic_app/src/lib/api/category.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | export const getCategories = () => client.get('/category'); 4 | -------------------------------------------------------------------------------- /munetic_express/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | RUN mkdir munetic_express 3 | WORKDIR /munetic_express 4 | COPY . . 5 | RUN npm i esbuild 6 | RUN npm i 7 | -------------------------------------------------------------------------------- /munetic_app/public/img/basicProfileImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/innovationacademy-kr/slabs-munetic/HEAD/munetic_app/public/img/basicProfileImg.png -------------------------------------------------------------------------------- /munetic_app/src/components/Bar.tsx: -------------------------------------------------------------------------------- 1 | export default function Bar() { 2 | return ( 3 |
4 | 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import Login from '../components/Auth/Login'; 2 | 3 | export default function LoginPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /munetic_app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | RUN mkdir munetic_app 3 | WORKDIR /munetic_app 4 | COPY . . 5 | RUN npm i esbuild 6 | RUN npm i 7 | CMD [ "npm", "run", "dev" ] 8 | -------------------------------------------------------------------------------- /munetic_admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | RUN mkdir munetic_admin 3 | WORKDIR /munetic_admin 4 | COPY . . 5 | RUN npm i esbuild 6 | RUN npm i 7 | CMD [ "npm", "run", "dev" ] 8 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/etc.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | export const getTerms = () => client.get(`/etc/terms`); 4 | export const getLicense = () => client.get(`/etc/license`); -------------------------------------------------------------------------------- /munetic_app/src/pages/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | import Search from '../components/Search'; 2 | 3 | export default function SearchPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /munetic_admin/src/pages/PasswordChangePage.tsx: -------------------------------------------------------------------------------- 1 | import PasswordChange from '../components/Auth/PasswordChange'; 2 | 3 | export default function PasswordChangePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /munetic_app/src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import Home from '../components/Home'; 2 | 3 | export default function HomePage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /munetic_express/src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | app.listen(3030, () => 4 | console.log(`============= 5 | 🚀 App listening on the port 3030 6 | =============`), 7 | ); 8 | -------------------------------------------------------------------------------- /munetic_admin/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": true, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "printWidth": 80, 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /munetic_app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": true, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "printWidth": 80, 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /munetic_app/src/pages/SettingPage.tsx: -------------------------------------------------------------------------------- 1 | import Setting from '../components/Setting'; 2 | 3 | export default function SettingPage() { 4 | return ( 5 | <> 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /munetic_app/src/types/enums.ts: -------------------------------------------------------------------------------- 1 | export enum Account { 2 | Student = 'Student', 3 | Tutor = 'Tutor', 4 | } 5 | 6 | export enum Gender { 7 | Male = 'Male', 8 | Female = 'Female', 9 | Other = 'Other', 10 | } 11 | -------------------------------------------------------------------------------- /munetic_express/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": true, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "printWidth": 80, 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /munetic_express/src/modules/types.ts: -------------------------------------------------------------------------------- 1 | export interface ResponseData {} 2 | 3 | export class ResJSON { 4 | constructor( 5 | public readonly message: string, 6 | public readonly data: ResponseData = {}, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /munetic_express/src/modules/errorResponse.ts: -------------------------------------------------------------------------------- 1 | export default class ErrorResponse extends Error { 2 | status: number; 3 | 4 | constructor(status: number, message: string) { 5 | super(message); 6 | this.status = status; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.env_template: -------------------------------------------------------------------------------- 1 | # set environment variables below and set the file name as .env 2 | MARIADB_USER= 3 | MARIADB_PASSWORD= 4 | MARIADB_ROOT_PASSWORD= 5 | EXPRESS_USER= 6 | EXPRESS_PASSWORD= 7 | ACCESS_SECRET= 8 | REFRESH_SECRET= 9 | SERVER_HOST= 10 | MODE= 11 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | const { VITE_BASE_URL } = import.meta.env; 3 | 4 | const client = axios.create({ 5 | baseURL: `${VITE_BASE_URL}/api`, 6 | withCredentials: true, 7 | }); 8 | 9 | export default client; 10 | -------------------------------------------------------------------------------- /munetic_app/src/tests/components/pages/lesson/CategoryPage.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | }); 7 | -------------------------------------------------------------------------------- /munetic_express/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | verbose: true, 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.ts?$': 'ts-jest', 8 | }, 9 | }; 10 | export default config; 11 | -------------------------------------------------------------------------------- /munetic_express/src/routes/category.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as CategoryAPI from '../controllers/category.controller'; 3 | 4 | export const path = '/category'; 5 | export const router = Router(); 6 | 7 | router.get('/', CategoryAPI.getAllCategory); 8 | -------------------------------------------------------------------------------- /munetic_app/src/types/categoryData.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 카테고리 테이블의 데이터 타입을 정의합니다. 기본키나 외래키 제외 모두 optional로 설정합니다. 4 | */ 5 | export interface ICategoryTable { 6 | id: number; 7 | name?: string; 8 | createdAt?: Date; 9 | updatedAt?: Date; 10 | deletedAt?: Date; 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/pages/setting/PolicyPage.tsx: -------------------------------------------------------------------------------- 1 | import Terms from "../../components/setting/Terms"; 2 | import Wrapper from '../../components/Wrapper'; 3 | 4 | export default function PolicyPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /munetic_express/src/@types/express.d.ts: -------------------------------------------------------------------------------- 1 | declare module Express { 2 | export interface Request { 3 | user?: { 4 | login_id: string; 5 | id: number; 6 | login_password?: string; 7 | type: string; 8 | TutorInfo?: ITutorInfoData; 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /munetic_express/src/routes/etc.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as EtcAPI from '../controllers/etc.controller'; 3 | 4 | export const path = '/etc'; 5 | export const router = Router(); 6 | 7 | router.get('/terms', EtcAPI.getTerms); 8 | router.get('/license', EtcAPI.getLicense); 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 둘 중 하나를 참고하여 작성할 것. bug, feat 외에 commit 타입을 선택해서 작성 가능합니다. 2 | 3 | ## [bug] 버그 제목 4 | 5 | ### 발견 일시 6 | 7 | ### 증상 8 | 9 | ### 발생 위치 (optional) 10 | 11 | ### 발생 커밋 12 | 13 | ### 재현 방법 14 | 15 | ### 스크린샷 16 | 17 | --- 18 | 19 | ## [feat] 기능 제목 20 | 21 | ## 기능 내용 22 | -------------------------------------------------------------------------------- /munetic_app/src/pages/setting/LicensePage.tsx: -------------------------------------------------------------------------------- 1 | import License from "../../components/setting/License"; 2 | import Wrapper from '../../components/Wrapper'; 3 | 4 | export default function LicensePage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Common/Title.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Title = styled.p` 4 | margin: 1rem; 5 | padding-left: 1rem; 6 | font-size: 1.5rem; 7 | font-weight: 700; 8 | border-left: 4px solid rgb(82, 111, 255); 9 | `; 10 | 11 | export default Title; 12 | -------------------------------------------------------------------------------- /munetic_admin/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: '/admin/', 7 | plugins: [react()], 8 | server: { 9 | port: 4242, 10 | host: true, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /munetic_app/src/pages/profile/ViewMyLikesPage.tsx: -------------------------------------------------------------------------------- 1 | import MyLikesPage from "../../components/like/MyLikeLessons"; 2 | import Wrapper from "../../components/Wrapper"; 3 | 4 | export default function ViewMyLikesPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /munetic_app/src/tests/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import App from '../App'; 4 | 5 | test('renders learn react link', () => { 6 | render( 7 | 8 | 9 | , 10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /munetic_app/src/pages/lesson/ClassPage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import LessonInfo from '../../components/lesson/LessonInfo'; 3 | 4 | export default function ClassPage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/pages/bookmarks/ViewMyBookmarksPage.tsx: -------------------------------------------------------------------------------- 1 | import MyBookmarkLessons from "../../components/bookmark/MyBookmarkLessons"; 2 | import Wrapper from "../../components/Wrapper"; 3 | 4 | export default function ViewMyBookmarksPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /munetic_app/src/pages/lesson/WriteClassPage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import ClassWrite from '../../components/lesson/ClassWrite'; 3 | 4 | export default function WriteClassPage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/pages/lesson/ManageClassPage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import ClassManage from '../../components/lesson/ClassManage'; 3 | 4 | export default function ManageClassPage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/style/palette.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | darkBlue: '#1d3557', 3 | grayBlue: '#457b9d', 4 | lightBlue: '#a8dadc', 5 | green: '#f1faee', 6 | ivory: '#fbfefb', 7 | red: '#d22227', 8 | orange: '#fc5c12', 9 | lightgray: '#e7e7e7', 10 | darkgray: '#555555', 11 | banana: '#fcba03', 12 | white: '#ffffff', 13 | }; 14 | -------------------------------------------------------------------------------- /munetic_app/src/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | /** 4 | * 페이지의 컨텐츠를 감싸기 위한 래퍼 컴포넌트입니다. 5 | * styled-components를 이용해 리액트 컴포넌트로 만들어 스타일을 적용합니다. 6 | * 7 | * @author joohongpark 8 | */ 9 | const Wrapper = styled.div` 10 | margin-bottom: 30px; 11 | padding: 30px; 12 | `; 13 | 14 | export default Wrapper; -------------------------------------------------------------------------------- /munetic_app/src/lib/auth/loginCheck.ts: -------------------------------------------------------------------------------- 1 | import * as Auth from '../api/auth'; 2 | 3 | export default async function loginCheck (): Promise { 4 | let rtn = false; 5 | try { 6 | const res = await Auth.loginCheck(); 7 | rtn = res.data.data as boolean; 8 | } catch (e) { 9 | console.log(e, 'err'); 10 | } 11 | return rtn; 12 | }; -------------------------------------------------------------------------------- /munetic_app/src/pages/profile/EditProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import EditProfile from '../../components/profile/EditProfile'; 3 | 4 | export default function EditProfilePage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/pages/profile/ViewProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import ViewProfile from '../../components/profile/ViewProfile'; 3 | 4 | export default function ViewProfilePage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Common/Item.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Item = styled.div` 4 | background-color: white; 5 | border: 0.1rem solid white; 6 | border-radius: 0.5rem; 7 | box-shadow: 0px 0px 0.7rem lightgrey; 8 | height: 100%; 9 | padding: 1rem 1rem 0.5rem 1rem; 10 | `; 11 | 12 | export default Item; 13 | -------------------------------------------------------------------------------- /munetic_app/src/pages/profile/ManageProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import ManageProfile from '../../components/profile/ManageProfile'; 3 | 4 | export default function ManageProfilePage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/types/commentWriteData.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 댓글 작성 컴포넌트의 프로퍼티를 정의합니다. 3 | * 댓글 작성 버튼을 누를 때 호출되는 콜백함수, 초기 별 개수, 초기 댓글 데이터를 받습니다. 4 | * 5 | * @author joohongpark 6 | */ 7 | export interface CommentWritePropsType { 8 | submit: (stars: number | null, comment: string) => void; 9 | initStars: number | null; 10 | initComment: string; 11 | } 12 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/User/AdminMemo.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Title from '../Common/Title'; 3 | 4 | export default function AdminMemo() { 5 | return ( 6 | <> 7 | 관리자 메모 8 | 9 | 10 | ); 11 | } 12 | 13 | const EmptyBox = styled.div` 14 | height: 10rem; 15 | `; 16 | -------------------------------------------------------------------------------- /munetic_app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Munetic 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /munetic_app/src/style/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import reset from 'styled-reset'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | ${reset}; 6 | * { 7 | box-sizing: border-box; 8 | } 9 | a { 10 | text-decoration: none; 11 | color: black; 12 | } 13 | `; 14 | 15 | export default GlobalStyle; 16 | -------------------------------------------------------------------------------- /munetic_app/src/pages/profile/EditTutorProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import EditTutorProfile from '../../components/profile/EditTutorProfile'; 3 | 4 | export default function EditTutorProfilePage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/src/pages/profile/ViewTutorProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import ViewTutorProfile from '../../components/profile/ViewTutorProfile'; 3 | 4 | export default function ViewTutorProfilePage() { 5 | return ( 6 | <> 7 | 8 | {/* */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /munetic_app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | react({ 8 | fastRefresh: process.env.NODE_ENV !== 'test', 9 | }), 10 | ], 11 | server: { 12 | port: 2424, 13 | host: true, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /munetic_proxy/envsubst.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -eu 3 | 4 | LOCALHOST=localhost 5 | 6 | if [ ${SERVER_HOST} != "$LOCALHOST" ]; then 7 | envsubst '${SERVER_HOST}' < /templates/default.conf.template > /etc/nginx/conf.d/default.conf 8 | else 9 | envsubst '${SERVER_HOST}' < /templates/localhost.conf.template > /etc/nginx/conf.d/default.conf 10 | fi 11 | 12 | exec "$@" -------------------------------------------------------------------------------- /munetic_express/.sequelizerc: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ 2 | /* options */ 3 | }); 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | config: path.resolve('./dist/config', 'config.js'), 8 | 'migrations-path': path.resolve('.', 'migrations'), 9 | 'models-path': path.resolve('.', 'models'), 10 | 'seeders-path': path.resolve('.', 'seeders'), 11 | }; 12 | -------------------------------------------------------------------------------- /munetic_express/src/tests/dummy/userInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "login_id": "pca0046", 4 | "login_password": "1234", 5 | "name": "박채인", 6 | "birth": "1995-11-05T00:00:00.000Z", 7 | "nickname": "chaepark", 8 | "email": "pca0046@gmail.com", 9 | "type": "STUDENT", 10 | "updatedAt": "2021-12-27T15:25:26.113Z", 11 | "createdAt": "2021-12-27T15:25:26.113Z" 12 | } 13 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Button = styled.button` 4 | border: none; 5 | outline: none; 6 | padding: 10px 5px 10px 5px; 7 | border-radius: 30px; 8 | margin-bottom: 10px; 9 | color: #fff; 10 | box-shadow: 3px 3px 8px #b1b1b1, -3px -3px 8px #fff; 11 | cursor: pointer; 12 | `; 13 | 14 | export default Button; 15 | -------------------------------------------------------------------------------- /munetic_database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mariadb:10.6.5-focal 2 | ARG EXPRESS_USER 3 | ARG EXPRESS_PASSWORD 4 | RUN echo "CREATE USER IF NOT EXISTS '${EXPRESS_USER}'@'%' IDENTIFIED BY '${EXPRESS_PASSWORD}'; \ 5 | CREATE DATABASE munetic; \ 6 | GRANT ALL PRIVILEGES ON munetic.* TO ${EXPRESS_USER};" \ 7 | > /docker-entrypoint-initdb.d/init.sql 8 | RUN chmod 755 /docker-entrypoint-initdb.d/init.sql 9 | -------------------------------------------------------------------------------- /munetic_app/src/types/commentTopData.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CommentTop 컴포넌트가 프로퍼티로 받아야 하는 타입을 지정합니다. 3 | * 댓글 새로고침 함수, 시간순 정렬 함수, 별 개수순 정렬 함수, 댓글 개수를 받습니다. 4 | * 5 | * @author joohongpark 6 | */ 7 | export interface FunctionPropsType { 8 | refrash: () => void; 9 | sortByTime: () => void; 10 | incSortByStar: () => void; 11 | decSortByStar: () => void; 12 | commentCount: number; 13 | } 14 | -------------------------------------------------------------------------------- /munetic_express/src/swagger/swagger.ts: -------------------------------------------------------------------------------- 1 | import { development } from '../config/config'; 2 | 3 | export const options = { 4 | definition: { 5 | swagger: '2.0', 6 | info: { 7 | title: 'MUNETIC API', 8 | version: '1.0.0', 9 | }, 10 | host: development.domain, 11 | basePath: '/api', 12 | }, 13 | apis: ['./src/swagger/apis/*.yml', './src/swagger/definitions.yml'], 14 | }; 15 | -------------------------------------------------------------------------------- /munetic_app/src/lib/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import * as Auth from '../api/auth'; 2 | import client from '../api/client'; 3 | 4 | export default async function Logout () { 5 | try { 6 | localStorage.removeItem('user'); 7 | await Auth.logout(); 8 | client.defaults.headers.common['Authorization'] = ''; 9 | } catch (e) { 10 | alert("Authorization Error!"); 11 | console.log(e, '로그아웃 실패'); 12 | } 13 | }; -------------------------------------------------------------------------------- /munetic_app/src/pages/lesson/CategoryPage.tsx: -------------------------------------------------------------------------------- 1 | import BottomMenu from '../../components/common/BottomMenu'; 2 | import CategoryContainer from '../../components/lesson/CategoryContainer'; 3 | import ClassListAll from '../../components/lesson/ClassListAll'; 4 | 5 | export default function CategoryPage() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /munetic_app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'vite-jest', 3 | 4 | setupFilesAfterEnv: ['/jest.setup.js'], 5 | //셋업 파일 위치 6 | testMatch: [ 7 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 8 | '/src/**/*.{spec,test}.{js,jsx,ts,tsx}', 9 | ], //src폴더 내부에 test나 spec 이름을 포함한 파일을 test 대상으로 한다. 10 | testEnvironment: 'jest-environment-jsdom', 11 | //test가 진행되는 환경 12 | }; 13 | -------------------------------------------------------------------------------- /munetic_app/src/lib/getYoutubeId.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 어떤 종류의 유튜브 영상 링크가 입력되도 유튜브 영상 고유 ID를 파싱하는 함수 3 | * 4 | * @param url 유튜브 링크 5 | * @returns 파싱 성공시 ID, 실패시 null 6 | */ 7 | export default function getYoutubeId(url: string) : string | null { 8 | var p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; 9 | return (url.match(p)) ? RegExp.$1 : null; 10 | } -------------------------------------------------------------------------------- /munetic_app/src/lib/api/bookmark.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | export const getLessonBookmarks = () => client.get(`/bookmark`); 4 | export const getLessonBookmark = (lesson_id: number) => client.get(`/bookmark/${lesson_id}`); 5 | export const putLessonBookmark = (lesson_id: number) => client.put(`/bookmark/${lesson_id}`); 6 | export const delLessonBookmark = (lesson_id: number) => client.delete(`/bookmark/${lesson_id}`); 7 | -------------------------------------------------------------------------------- /munetic_express/src/tests/dummy/userInstance.ts: -------------------------------------------------------------------------------- 1 | import { User, Account, Gender } from '../../models/user'; 2 | 3 | const UserInstance = new User({ 4 | login_id: 'pca0046', 5 | login_password: '1234', 6 | type: Account['Student'], 7 | birth: new Date('1992-10-05'), 8 | gender: Gender['Other'], 9 | name: '박채인', 10 | nickname: 'chaepark', 11 | email: 'sdasdasd@gmail.com', 12 | }); 13 | 14 | export default UserInstance; 15 | -------------------------------------------------------------------------------- /munetic_app/src/tests/Home.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import Home from '../components/Home'; 4 | 5 | describe('Home test', () => { 6 | it('has buttons', () => { 7 | const { getByText } = render( 8 | 9 | 10 | , 11 | ); 12 | getByText('로그인'); 13 | getByText('회원가입'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /munetic_express/src/util/addProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 객체에 새로운 프로퍼티를 삽입해주는 함수입니다. 3 | * 프로퍼티로 모든 타입을 받을 수 있게 제네릭 함수로 구현하였습니다. 4 | * 5 | * @param obj 프로퍼티를 삽입하고자 하는 객체 6 | * @param propName 삽입하고자 하는 프로퍼티명 7 | * @param prop 삽입하고자 하는 프로퍼티 8 | */ 9 | export default function addProperty(obj: Object, propName: string, prop: T) { 10 | Object.defineProperty(obj, propName, { 11 | value: prop, 12 | enumerable: true, 13 | configurable: true, 14 | }); 15 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Lesson/LessonContent.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Title from '../Common/Title'; 3 | import { useInfo } from '../../../contexts/info'; 4 | 5 | export default function LessonContent() { 6 | const info = useInfo() as any; 7 | 8 | return ( 9 | <> 10 | 작성 내용 11 | {info.content} 12 | 13 | ); 14 | } 15 | 16 | const Content = styled.div` 17 | padding: 1rem; 18 | `; 19 | -------------------------------------------------------------------------------- /munetic_express/src/models/initdata/etc.init.ts: -------------------------------------------------------------------------------- 1 | import { Etc, Key } from '../etc'; 2 | 3 | function createEtcData() { 4 | const dataLists = [ 5 | {id: Key.Terms, content: "약관 데이터 샘플"}, 6 | {id: Key.License, content: "오픈소스 라이센스 데이터 샘플"}, 7 | ]; 8 | dataLists.map(data => { 9 | Etc.create(data as Etc).catch(e => console.log(e)); 10 | }); 11 | console.log('🎺 App:EtcLists created'); 12 | } 13 | 14 | export default function initialize() { 15 | createEtcData(); 16 | } -------------------------------------------------------------------------------- /munetic_admin/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import LoginProvider, { useLogin } from './contexts/login'; 5 | import App from './App'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root'), 16 | ); 17 | -------------------------------------------------------------------------------- /munetic_app/src/lib/auth/vaildCheck.ts: -------------------------------------------------------------------------------- 1 | import * as AuthAPI from '../api/auth'; 2 | 3 | export default async function vaildCheck(param_name: string, param_value: T, callback: React.Dispatch>) { 4 | if (param_value) { 5 | try { 6 | await AuthAPI.isValidInfo(`${param_name}=${param_value}`); 7 | alert(`사용 가능합니다.`); 8 | callback(true); 9 | } catch (e) { 10 | alert('중복입니다!'); 11 | } 12 | } else { 13 | alert(`값을 입력해 주세요!`); 14 | } 15 | } -------------------------------------------------------------------------------- /munetic_express/src/routes/admin/etc.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { jwtAdminAuth } from '../../modules/jwt.admin.strategy'; 3 | import * as EtcAPI from '../../controllers/etc.controller'; 4 | 5 | export const path = '/etc'; 6 | export const router = Router(); 7 | 8 | router.get('/terms', EtcAPI.getTerms); 9 | router.put('/terms', jwtAdminAuth(), EtcAPI.editTerms); 10 | router.get('/license', EtcAPI.getLicense); 11 | router.put('/license', jwtAdminAuth(), EtcAPI.editLicense); 12 | -------------------------------------------------------------------------------- /munetic_app/src/lib/getCategoriesByMap.ts: -------------------------------------------------------------------------------- 1 | import * as CatrgoryAPI from '../lib/api/category'; 2 | import { ICategoryTable } from '../types/categoryData'; 3 | 4 | export default async function getCategoriesByMap(): Promise> { 5 | const categoriesMap = new Map(); 6 | const categoriesRes = await CatrgoryAPI.getCategories(); 7 | categoriesRes.data.data.forEach((e: ICategoryTable) => { 8 | categoriesMap.set(e.id, e.name || ""); 9 | }); 10 | return categoriesMap; 11 | } -------------------------------------------------------------------------------- /munetic_app/src/types/userSignupData.d.ts: -------------------------------------------------------------------------------- 1 | export interface userSignupData { 2 | login_id: string; 3 | login_password: string; 4 | type: string; 5 | nickname: string; 6 | name: string; 7 | birth: string; 8 | email: string; 9 | phone_number: string; 10 | gender: string | undefined; 11 | } 12 | 13 | export interface ITutorInfoType { 14 | spec: string; 15 | career: string; 16 | youtube: string; 17 | instagram: string; 18 | soundcloud: string; 19 | tutor_introduction: string; 20 | } 21 | -------------------------------------------------------------------------------- /munetic_express/src/routes/search.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as SearchAPI from '../controllers/search.controller'; 3 | 4 | export const path = '/search'; 5 | export const router = Router(); 6 | 7 | router.get('/', SearchAPI.getLessonsAll); 8 | 9 | router.get('/instrument', SearchAPI.getLessonsByInstrument); 10 | router.get('/tutor', SearchAPI.getLessonsByTutor); 11 | router.get('/location', SearchAPI.getLessonsByLocation); 12 | router.get('/mix-filter', SearchAPI.getLessonsMix); -------------------------------------------------------------------------------- /munetic_admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | Munetic Admin 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /munetic_admin/src/style/GlobalStyle.tsx: -------------------------------------------------------------------------------- 1 | import reset from 'styled-reset'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | ${reset}; 6 | html { 7 | font-size: 62.5%; 8 | line-height: 1.285; 9 | } 10 | body { 11 | box-sizing: border-box; 12 | font-family: 'Noto Sans KR', sans-serif; 13 | background-color: #ecf0f3; 14 | a { 15 | text-decoration:none; 16 | } 17 | } 18 | `; 19 | 20 | export default GlobalStyle; 21 | -------------------------------------------------------------------------------- /munetic_express/src/routes/admin/comment.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as CommentAPI from '../../controllers/comment.controller'; 3 | import * as CommentAdminAPI from '../../controllers/admin/comment.controller'; 4 | import { jwtAdminAuth } from '../../modules/jwt.admin.strategy'; 5 | 6 | export const path = '/comment'; 7 | export const router = Router(); 8 | 9 | router.get('/', jwtAdminAuth(), CommentAdminAPI.getAllComments); 10 | router.post('/del', jwtAdminAuth(), CommentAdminAPI.delComments); -------------------------------------------------------------------------------- /munetic_app/src/types/lessonLikeData.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 댓글 테이블의 데이터 타입을 정의합니다. 4 | * 5 | * @author sungkim 6 | */ 7 | export interface ILessonLikeTable { 8 | id: number; 9 | user_id: number; 10 | lesson_id: number; 11 | lesson_like: boolean; 12 | createdAt: Date; 13 | updatedAt: Date; 14 | deletedAt: Date; 15 | } 16 | 17 | /** 18 | * 좋아요 많은 강사의 타입을 지정합니다. 19 | * 20 | * @author joohongpark 21 | */ 22 | export interface ILikesPerTutorTable { 23 | tutor_id: number, 24 | like_count: number, 25 | } 26 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/like.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | export const getStarTutors = () => client.get(`/like/startutor`); 4 | export const getLessonLikes = () => client.get(`/like`); 5 | export const getLessonLike = (lesson_id: number) => client.get(`/like/${lesson_id}`); 6 | export const getLikedPeoples = (lesson_id: number) => client.get(`/like/${lesson_id}/all`); 7 | export const putLessonLike = (lesson_id: number) => client.put(`/like/${lesson_id}`); 8 | export const delLessonLike = (lesson_id: number) => client.delete(`/like/${lesson_id}`); 9 | -------------------------------------------------------------------------------- /munetic_express/src/routes/bookmark.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as BookmarkAPI from '../controllers/bookmark.controller'; 3 | import { jwtAuth } from '../modules/jwt.local.strategy'; 4 | 5 | export const path = '/bookmark'; 6 | export const router = Router(); 7 | 8 | router.get('/', jwtAuth(), BookmarkAPI.getBookmarks); 9 | router.get('/:lesson_id', jwtAuth(), BookmarkAPI.getBookmark); 10 | router.put('/:lesson_id', jwtAuth(), BookmarkAPI.putBookmark); 11 | router.delete('/:lesson_id', jwtAuth(), BookmarkAPI.delBookmark); 12 | -------------------------------------------------------------------------------- /munetic_proxy/templates/localhost.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name ${SERVER_HOST}; 4 | 5 | resolver 127.0.0.11; 6 | 7 | # domain name을 ip 주소로 치환하기 위한 설정 8 | set $admin munetic_admin; 9 | set $app munetic_app; 10 | set $express munetic_express; 11 | 12 | location /admin { 13 | rewrite /admin(.*) /$1 break; 14 | 15 | proxy_pass http://$admin:4242; 16 | } 17 | 18 | location / { 19 | proxy_pass http://$app:2424; 20 | } 21 | 22 | location /api { 23 | proxy_pass http://$express:3030; 24 | } 25 | } -------------------------------------------------------------------------------- /munetic_app/src/pages/auth/RegisterPage.tsx: -------------------------------------------------------------------------------- 1 | import Register from '../../components/auth/Register'; 2 | import TutorRegister from '../../components/auth/TutorRegister'; 3 | import { useSearchParams } from 'react-router-dom'; 4 | import BottomMenu from '../../components/common/BottomMenu'; 5 | 6 | export default function RegisterPage() { 7 | const [getParams] = useSearchParams(); 8 | const tutorParam = getParams.get('tutor'); 9 | return ( 10 | <> 11 | {tutorParam ? : } 12 | {/* */} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /munetic_express/src/routes/admin/admin.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as auth from './auth.routes'; 3 | import * as user from './user.routes'; 4 | import * as lesson from './lesson.routes'; 5 | import * as comment from './comment.routes'; 6 | import * as etc from './etc.routes'; 7 | 8 | export const path = '/admin'; 9 | export const router = Router(); 10 | 11 | router.use(auth.path, auth.router); 12 | router.use(user.path, user.router); 13 | router.use(lesson.path, lesson.router); 14 | router.use(comment.path, comment.router); 15 | router.use(etc.path, etc.router); 16 | -------------------------------------------------------------------------------- /munetic_express/src/routes/admin/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as Auth from '../../controllers/admin/auth.controller'; 3 | import { jwtAdminAuth, jwtReAdminAuth } from '../../modules/jwt.admin.strategy'; 4 | 5 | export const path = '/auth'; 6 | export const router = Router(); 7 | 8 | router.post('/signup', jwtAdminAuth(), Auth.signup); 9 | router.post('/login', Auth.login); 10 | router.get('/logout', jwtAdminAuth(), Auth.logout); 11 | router.get('/refresh', jwtReAdminAuth(), Auth.refresh); 12 | router.patch('/password', jwtAdminAuth(), Auth.updatePassword); 13 | -------------------------------------------------------------------------------- /network.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | munetic_admin: 5 | networks: 6 | - munetic 7 | munetic_app: 8 | networks: 9 | - munetic 10 | munetic_database: 11 | networks: 12 | - munetic 13 | munetic_express: 14 | networks: 15 | - munetic 16 | munetic_proxy: 17 | networks: 18 | - munetic 19 | ports: 20 | - '80:80' 21 | - '443:443' 22 | 23 | networks: # 네트워크를 사용하겠다는 선언입니다. 선언하지 않을 경우 기본 네트워크가 사용됩니다. 24 | munetic: {} # 기본 설정을 이용하여 munetic이라는 네트워크를 선언합니다. munetic network를 사용하는 컨테이너들은 서로 간에 서비스의 이름으로 접근할 수 있습니다. 25 | -------------------------------------------------------------------------------- /network-main.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | munetic_admin: 5 | networks: 6 | - munetic 7 | munetic_app: 8 | networks: 9 | - munetic 10 | munetic_database: 11 | networks: 12 | - munetic 13 | munetic_express: 14 | networks: 15 | - munetic 16 | munetic_proxy: 17 | networks: 18 | - munetic 19 | ports: 20 | - '8888:80' 21 | - '3443:443' 22 | 23 | networks: # 네트워크를 사용하겠다는 선언입니다. 선언하지 않을 경우 기본 네트워크가 사용됩니다. 24 | munetic: {} # 기본 설정을 이용하여 munetic이라는 네트워크를 선언합니다. munetic network를 사용하는 컨테이너들은 서로 간에 서비스의 이름으로 접근할 수 있습니다. 25 | -------------------------------------------------------------------------------- /munetic_app/src/pages/lesson/ClassListPage.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from 'react-router-dom'; 2 | import BottomMenu from '../../components/common/BottomMenu'; 3 | import ClassList from '../../components/lesson/ClassList'; 4 | import SearchTarget from '../../components/SearchTarget'; 5 | 6 | export default function SearchPageTarget() { 7 | const [getParams] = useSearchParams(); 8 | const categoryParam = getParams.get('category') as string; 9 | return ( 10 |
11 | 12 | {/* */} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /munetic_express/src/types/controller/tutorInfoData.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 클라이언트로부터 튜터 정보 데이터를 받거나 전송하는 타입을 정의합니다. 3 | */ 4 | export interface ITutorInfoType { 5 | spec?: string; 6 | career?: string; 7 | youtube?: string; 8 | instagram?: string; 9 | soundcloud?: string; 10 | tutor_introduction?: string; 11 | } 12 | 13 | /** 14 | * 테이블에 컬럼을 삽입할 때 사용되는 인터페이스입니다. 15 | */ 16 | export interface ITutorInfoInsertType { 17 | user_id: number; 18 | spec: string; 19 | career: string; 20 | youtube: string; 21 | instagram: string; 22 | soundcloud: string; 23 | tutor_introduction: string; 24 | } 25 | -------------------------------------------------------------------------------- /munetic_admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /munetic_app/src/tests/components/common/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import Button from '../../../components/common/Button'; 4 | 5 | describe('Button test', () => { 6 | it('has link url', () => { 7 | const children = '레슨 찾기'; 8 | const to = '/lesson/category'; 9 | const { getByText } = render( 10 | 11 | 12 | , 13 | ); 14 | const link = getByText('레슨 찾기'); 15 | expect(link).toHaveTextContent(children);; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /network-develop.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | munetic_admin: 5 | networks: 6 | - dev_munetic 7 | munetic_app: 8 | networks: 9 | - dev_munetic 10 | munetic_database: 11 | networks: 12 | - dev_munetic 13 | munetic_express: 14 | networks: 15 | - dev_munetic 16 | munetic_proxy: 17 | networks: 18 | - dev_munetic 19 | ports: 20 | - '8080:80' 21 | - '4443:443' 22 | 23 | networks: # 네트워크를 사용하겠다는 선언입니다. 선언하지 않을 경우 기본 네트워크가 사용됩니다. 24 | dev_munetic: {} # 기본 설정을 이용하여 munetic이라는 네트워크를 선언합니다. munetic network를 사용하는 컨테이너들은 서로 간에 서비스의 이름으로 접근할 수 있습니다. 25 | -------------------------------------------------------------------------------- /munetic_express/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { Dialect } from 'sequelize'; 3 | dotenv.config(); 4 | 5 | export const development = { 6 | host: process.env.DB_HOST as string, 7 | port: parseInt(process.env.DB_PORT!, 10), 8 | username: process.env.DB_USERNAME, 9 | password: process.env.DB_PASSWORD, 10 | database: process.env.DB_NAME, 11 | dialect: 'mariadb', 12 | access_secret: process.env.ACCESS_SECRET, 13 | refresh_secret: process.env.REFRESH_SECRET, 14 | domain: process.env.SERVER_HOST, 15 | // test: {}, 16 | // production: {}, 17 | }; 18 | 19 | // module.exports = { development }; 20 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/LessonInfoPage.tsx: -------------------------------------------------------------------------------- 1 | import InfoGrid from '../components/Info/InfoGrid'; 2 | import { useInfoUpdate } from '../contexts/info'; 3 | import { useLocation } from 'react-router-dom'; 4 | import { useEffect } from 'react'; 5 | import * as Api from '../lib/api'; 6 | 7 | export default function LessonInfoPage() { 8 | const path = useLocation().pathname; 9 | const setInfo = useInfoUpdate(); 10 | 11 | useEffect(() => { 12 | const lessonId = parseInt(path.slice(9)); 13 | Api.getLesson(lessonId).then(res => { 14 | if (setInfo) setInfo(res.data.data); 15 | }); 16 | }, []); 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/UserInfoPage.tsx: -------------------------------------------------------------------------------- 1 | import InfoGrid from '../components/Info/InfoGrid'; 2 | import { useInfoUpdate } from '../contexts/info'; 3 | import { useLocation } from 'react-router-dom'; 4 | import { useEffect } from 'react'; 5 | import * as Api from '../lib/api'; 6 | 7 | export default function UserInfoPage() { 8 | const path = useLocation().pathname; 9 | const setInfo = useInfoUpdate(); 10 | 11 | useEffect(() => { 12 | const userId = parseInt(path.slice(7), 10); 13 | Api.getUserInfo(userId).then(res => { 14 | if (setInfo) setInfo(res.data.data); 15 | }); 16 | }, []); 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /munetic_express/src/controllers/category.controller.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import * as Status from 'http-status'; 3 | import { ResJSON } from '../modules/types'; 4 | import * as CategoryService from '../service/category.service'; 5 | 6 | export const getAllCategory: RequestHandler = async (req, res, next) => { 7 | try { 8 | let result: ResJSON; 9 | const categories = await CategoryService.findAllCategory(); 10 | result = new ResJSON( 11 | '모든 카테고리를 불러오는데 성공하였습니다.', 12 | categories, 13 | ); 14 | res.status(Status.OK).json(result); 15 | } catch (err) { 16 | next(err); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/AdminUserInfoPage.tsx: -------------------------------------------------------------------------------- 1 | import InfoGrid from '../components/Info/InfoGrid'; 2 | import { useInfoUpdate } from '../contexts/info'; 3 | import { useLocation } from 'react-router-dom'; 4 | import { useEffect } from 'react'; 5 | import * as Api from '../lib/api'; 6 | 7 | export default function AdminUserInfoPage() { 8 | const path = useLocation().pathname; 9 | const setInfo = useInfoUpdate(); 10 | 11 | useEffect(() => { 12 | const userId = parseInt(path.slice(13)); 13 | Api.getUserInfo(userId).then(res => { 14 | if (setInfo) setInfo(res.data.data); 15 | }); 16 | }, []); 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /munetic_express/src/routes/admin/lesson.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as lesson from '../../controllers/admin/lesson.controller'; 3 | import passport from 'passport'; 4 | import { jwtAdminAuth } from '../../modules/jwt.admin.strategy'; 5 | 6 | export const path = '/lesson'; 7 | export const router = Router(); 8 | 9 | router.get('/', jwtAdminAuth(), lesson.getAllLessons); 10 | router.get('/:id', jwtAdminAuth(), lesson.getLessonById); 11 | router.delete('/:id', jwtAdminAuth(), lesson.deleteLesson); 12 | router.get('/user/:id', jwtAdminAuth(), lesson.getUserLessons); 13 | router.post('/del', jwtAdminAuth(), lesson.delLessons); 14 | -------------------------------------------------------------------------------- /munetic_app/src/pages/setting/HelpPage.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function HelpPage() { 3 | return ( 4 |
5 |

6 | HelpPage 7 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquam tempore, minima repellendus vel amet sed iste rem reprehenderit quod magnam incidunt qui ex corrupti eos nobis optio suscipit excepturi similique! 8 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Maiores, odio possimus tenetur nisi sequi doloribus eveniet est provident architecto recusandae quidem rerum quos qui totam debitis illum repudiandae vero inventore! 9 |

10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /munetic_app/src/pages/setting/AboutusPage.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function AboutusPage() { 3 | return ( 4 |
5 |

6 | AboutusPage 7 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquam tempore, minima repellendus vel amet sed iste rem reprehenderit quod magnam incidunt qui ex corrupti eos nobis optio suscipit excepturi similique! 8 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Maiores, odio possimus tenetur nisi sequi doloribus eveniet est provident architecto recusandae quidem rerum quos qui totam debitis illum repudiandae vero inventore! 9 |

10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /munetic_app/src/pages/setting/ContactPage.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function ContactPage() { 3 | return ( 4 |
5 |

6 | ContactPage 7 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquam tempore, minima repellendus vel amet sed iste rem reprehenderit quod magnam incidunt qui ex corrupti eos nobis optio suscipit excepturi similique! 8 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Maiores, odio possimus tenetur nisi sequi doloribus eveniet est provident architecto recusandae quidem rerum quos qui totam debitis illum repudiandae vero inventore! 9 |

10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /munetic_express/src/modules/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import * as Status from 'http-status'; 3 | import ErrorResponse from './errorResponse'; 4 | 5 | export default function errorHandler( 6 | err: Error | ErrorResponse, 7 | req: Request, 8 | res: Response, 9 | next: NextFunction, 10 | ) { 11 | let error: ErrorResponse; 12 | if (!(err instanceof ErrorResponse)) { 13 | console.log(err); 14 | error = new ErrorResponse( 15 | Status.INTERNAL_SERVER_ERROR, 16 | '서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', 17 | ); 18 | } else error = err as ErrorResponse; 19 | res.status(error.status).json(error.message); 20 | return; 21 | } 22 | -------------------------------------------------------------------------------- /munetic_express/src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as UserAPI from '../controllers/user.controller'; 3 | import * as storage from '../modules/imgCreateMiddleware'; 4 | import { jwtAuth } from '../modules/jwt.local.strategy'; 5 | 6 | export const path = '/user'; 7 | export const router = Router(); 8 | 9 | router.get('/', jwtAuth(), UserAPI.getMyProfile); 10 | router.patch('/', jwtAuth(), UserAPI.editUserProfile); 11 | router.get('/:id', UserAPI.getUserProfile); 12 | router.get('/tutor/:id', UserAPI.getTutorProfile); 13 | router.patch('/tutor', jwtAuth(), UserAPI.editTutorProfile); 14 | router.post('/image', jwtAuth(), storage.imgUpload, UserAPI.createProfileImg); 15 | -------------------------------------------------------------------------------- /munetic_express/src/routes/lessonLike.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as LessonLikeAPI from '../controllers/lessonLike.controller'; 3 | import { jwtAuth } from '../modules/jwt.local.strategy'; 4 | 5 | export const path = '/like'; 6 | export const router = Router(); 7 | 8 | router.get('/startutor', LessonLikeAPI.getAllLessonLikePerTutor); 9 | router.get('/', jwtAuth(), LessonLikeAPI.getLessonLikes); 10 | router.get('/:lesson_id', jwtAuth(), LessonLikeAPI.getLessonLike); 11 | router.put('/:lesson_id', jwtAuth(), LessonLikeAPI.putLessonLike); 12 | router.delete('/:lesson_id', jwtAuth(), LessonLikeAPI.delLessonLike); 13 | router.get('/:lesson_id/all', jwtAuth(), LessonLikeAPI.getLikedPeoples); -------------------------------------------------------------------------------- /munetic_express/src/models/initdata/category.init.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '../category'; 2 | 3 | function createCategories() { 4 | const categoryLists = [ 5 | { name: '기타' }, 6 | { name: '바이올린' }, 7 | { name: '드럼' }, 8 | { name: '피아노' }, 9 | { name: '하프' }, 10 | { name: '첼로' }, 11 | { name: '하모니카' }, 12 | { name: '베이스' }, 13 | { name: '오카리나' }, 14 | { name: '디제잉' }, 15 | { name: '랩레슨' }, 16 | ]; 17 | categoryLists.forEach(category => { 18 | Category.create(category as Category) 19 | .catch(e => console.log(e)); 20 | }); 21 | console.log('🎺 App:DefaultCategoryLists created'); 22 | } 23 | 24 | export default function initialize() { 25 | createCategories(); 26 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Menu/menuLists.ts: -------------------------------------------------------------------------------- 1 | export const menuLists = [ 2 | '회원 관리', 3 | '게시물 관리', 4 | '서비스 관리', 5 | '버전 관리', 6 | '결제 관리', 7 | '푸쉬 관리', 8 | ]; 9 | 10 | export const subMenuLists = [ 11 | ['회원 조회 및 관리', '관리자 조회 및 관리'], 12 | ['게시물 조회 및 관리', '댓글 조회 및 관리'], 13 | ['앱 가입 정책 관리', '회원 가입 양식', '오픈 소스 라이센스', '팝업 관리'], 14 | ['스토어 앱 버전 관리'], 15 | ['결제 내역 조회', '환불/취소 관리', '서비스 센터'], 16 | ['푸쉬 보내기', '푸쉬 히스토리'], 17 | ]; 18 | 19 | export const menuLinks = ['/users', '/lessons', '', '', '', '']; 20 | 21 | export const subMenuLinks = [ 22 | ['/users', '/admin_users'], 23 | ['/lessons', '/comments'], 24 | ['/terms', '', '/license', ''], 25 | [''], 26 | ['', '', ''], 27 | ['', ''], 28 | ]; 29 | -------------------------------------------------------------------------------- /munetic_express/src/routes/comment.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as CommentAPI from '../controllers/comment.controller'; 3 | import { jwtAuth } from '../modules/jwt.local.strategy'; 4 | 5 | export const path = '/comment'; 6 | export const router = Router(); 7 | 8 | router.get('/startutor', CommentAPI.getAllCommentsCountPerTutor); 9 | router.get('/user/:user_id', jwtAuth(), CommentAPI.getCommentsByUserId); 10 | router.get('/lesson/:lesson_id', jwtAuth(), CommentAPI.getCommentsByLessonId); 11 | router.post('/lesson/:lesson_id', jwtAuth(), CommentAPI.putComment); 12 | router.put('/:comment_id', jwtAuth(), CommentAPI.updateComment); 13 | router.delete('/:comment_id', jwtAuth(), CommentAPI.delComment); 14 | -------------------------------------------------------------------------------- /munetic_express/src/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as Auth from '../controllers/auth.controller'; 3 | import passport from 'passport'; 4 | import { jwtAuth, jwtReAuth } from '../modules/jwt.local.strategy'; 5 | 6 | export const path = '/auth'; 7 | export const router = Router(); 8 | 9 | router.post('/login', Auth.login); 10 | router.get('/logout', jwtAuth(), Auth.logout); 11 | router.post('/signup', Auth.signup); 12 | router.post('/tutorsignup', jwtAuth(), Auth.tutorsignup); 13 | router.get('/refresh', jwtReAuth(), Auth.refresh); 14 | router.get('/signup/user', Auth.isValidInfo); 15 | router.put('/changeaccount', jwtAuth(), Auth.changeAccount); 16 | router.get('/logincheck', jwtAuth(), Auth.loginCheck); 17 | -------------------------------------------------------------------------------- /munetic_app/src/types/userData.d.ts: -------------------------------------------------------------------------------- 1 | import { Account, Gender } from './enums'; 2 | import { ITutorInfoData } from './tutorInfoData'; 3 | 4 | /** 5 | * 유저 테이블의 데이터 타입을 정의합니다. 기본키나 외래키 제외 모두 optional로 설정합니다. 6 | */ 7 | export interface IUserTable { 8 | id: number; 9 | type?: Account; 10 | login_id?: string | null; 11 | login_password?: string | null; 12 | nickname?: string; 13 | name?: string; 14 | name_public?: boolean; 15 | birth?: Date; 16 | gender?: Gender; 17 | email?: string | null; 18 | phone_number?: string | null; 19 | phone_public?: boolean; 20 | image_url?: string; 21 | introduction?: string | null; 22 | createdAt?: Date; 23 | updatedAt?: Date; 24 | deletedAt?: Date; 25 | TutorInfo?: ITutorInfoData; 26 | } 27 | -------------------------------------------------------------------------------- /munetic_express/src/routes/admin/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as UserApi from '../../controllers/admin/user.controller'; 3 | import { jwtAdminAuth } from '../../modules/jwt.admin.strategy'; 4 | 5 | export const path = '/user'; 6 | export const router = Router(); 7 | 8 | router.get('/app', jwtAdminAuth(), UserApi.getAppUserList); 9 | router.get('/admin', jwtAdminAuth(), UserApi.getAdminUserList); 10 | router.get('/check', jwtAdminAuth(), UserApi.doubleCheck); 11 | router.get('/:id', jwtAdminAuth(), UserApi.getUserInfo); 12 | router.patch('/:id', jwtAdminAuth(), UserApi.patchUserByAdmin); 13 | router.delete('/:id', jwtAdminAuth(), UserApi.deleteUserByAdmin); 14 | router.post('/del', jwtAdminAuth(), UserApi.delLessons); 15 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/Lesson/LessonTableCell.tsx: -------------------------------------------------------------------------------- 1 | import { TableCell } from '@mui/material'; 2 | 3 | export default function LessonTableCell({ row }: any) { 4 | return ( 5 | <> 6 | {row['User.login_id']} 7 | 8 | {row['Category.name']} 9 | 10 | 11 | {row.title} 12 | 13 | 14 | {row.createdAt} 15 | 16 | 17 | {row.deletedAt} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/AdminUser/AdminUserTableCell.tsx: -------------------------------------------------------------------------------- 1 | import { TableCell } from '@mui/material'; 2 | 3 | export default function AdminUserTableCell({ row }: any) { 4 | return ( 5 | <> 6 | 7 | {row.login_id} 8 | 9 | {row.name} 10 | 11 | {row.type} 12 | 13 | 14 | {row.createdAt} 15 | 16 | 17 | {row.deletedAt} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/search.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | 4 | export const searchLessonsByInstrument = (instrument: string | undefined) => 5 | client.get(`/search/instrument/?instrument=${instrument}`); 6 | 7 | export const searchLessonsByTutor = (tutor: string | undefined) => 8 | client.get(`/search/tutor/?tutor=${tutor}`); 9 | 10 | export const searchLessonsByLocation = (location: string | undefined) => 11 | client.get(`/search/location/?location=${location}`); 12 | 13 | export const searchLessonsMix = ( 14 | instrument: string | undefined, 15 | tutor: string | undefined, 16 | location: string | undefined, 17 | ) => 18 | client.get(`/search/mix-filter/?instrument=${instrument}&tutor=${tutor}&location=${location}`); 19 | 20 | -------------------------------------------------------------------------------- /munetic_express/src/service/category.service.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '../models/category'; 2 | 3 | export const createUser = async (category: Category) => { 4 | const data = await category.save(); 5 | const dataJSON = data.toJSON() as any; 6 | return dataJSON; 7 | }; 8 | 9 | export const findAllCategory = async () => { 10 | const categories = await Category.findAll(); 11 | if (categories === null) return '카테고리가 없습니다.'; 12 | return categories; 13 | }; 14 | 15 | export const findCategoryIdByName = async (category_name: string): Promise<{id: number}> => { 16 | const data = await Category.findOne({ 17 | where: { 18 | name: category_name 19 | }, 20 | attributes: ['id'] 21 | }) 22 | if (data === null) return {id: 0}; 23 | return data; 24 | }; -------------------------------------------------------------------------------- /munetic_app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | // "types": [ 19 | // "vite/client", 20 | // "jest", 21 | // "@types/testing-library__jest-dom", 22 | // "@types/react" 23 | // ] 24 | }, 25 | "include": ["./src", "jest.setup.js"] 26 | } 27 | -------------------------------------------------------------------------------- /munetic_express/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | 'airbnb', 12 | 'eslint:recommended', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | ignorePatterns: ['.eslintrc.js'], 20 | rules: { 21 | '@typescript-eslint/interface-name-prefix': 'on', 22 | '@typescript-eslint/explicit-function-return-type': 'on', 23 | '@typescript-eslint/explicit-module-boundary-types': 'on', 24 | '@typescript-eslint/no-explicit-any': 'on', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /munetic_app/src/components/setting/Terms.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useState, useEffect } from 'react'; 3 | import * as EtcAPI from '../../lib/api/etc'; 4 | 5 | const InnerWrapper = styled.div` 6 | font-family: "Roboto","Arial",sans-serif; 7 | font-size: 0.9rem; 8 | white-space : pre-line; 9 | `; 10 | 11 | export default function Terms() { 12 | const [text, setText] = useState(""); 13 | 14 | const getTerms = () => { 15 | EtcAPI.getTerms() 16 | .then(({ data }: any) => { 17 | setText(data.data); 18 | }) 19 | .catch(err => { 20 | console.log(err); 21 | }); 22 | } 23 | 24 | useEffect(() => { 25 | getTerms(); 26 | }, []); 27 | 28 | return ( 29 | {text} 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/InfoGrid.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useInfo } from '../../contexts/info'; 3 | import { useLocation } from 'react-router-dom'; 4 | import UserGrid from './User/UserGrid'; 5 | import LessonGrid from './Lesson/LessonGrid'; 6 | 7 | export default function InfoGrid() { 8 | const path = useLocation().pathname; 9 | const info = useInfo() as any; 10 | 11 | return ( 12 | 13 | {path === `/users/${info!.id}` && } 14 | {path === `/admin_users/${info!.id}` && } 15 | {path === `/lessons/${info!.id}` && } 16 | 17 | ); 18 | } 19 | 20 | const InfoContainer = styled.div` 21 | position: relative; 22 | background-color: #ecf0f3; 23 | border-radius: 0.5rem; 24 | `; 25 | -------------------------------------------------------------------------------- /munetic_app/src/components/setting/License.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useState, useEffect } from 'react'; 3 | import * as EtcAPI from '../../lib/api/etc'; 4 | 5 | const InnerWrapper = styled.div` 6 | font-family: "Roboto","Arial",sans-serif; 7 | font-size: 0.9rem; 8 | white-space : pre-line; 9 | `; 10 | 11 | export default function License() { 12 | const [text, setText] = useState(""); 13 | 14 | const getLicense = () => { 15 | EtcAPI.getLicense() 16 | .then(({ data }: any) => { 17 | setText(data.data); 18 | }) 19 | .catch(err => { 20 | console.log(err); 21 | }); 22 | } 23 | 24 | useEffect(() => { 25 | getLicense(); 26 | }, []); 27 | 28 | return ( 29 | {text} 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /munetic_app/src/tests/components/Bar.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import Bar from '../../components/Bar'; 3 | 4 | describe('test example: 전체 test이름이 들어감', () => { 5 | //describe는 여러 테스트를 묶어줌 6 | it('has 등록 버튼: test 이름', () => { 7 | const { getByText } = render(); 8 | //render함수는 렌더링할 리액트 컴포넌트를 인자로 받아 9 | //React Testing Library가 제공하는 모든 쿼리 함수와 10 | //기타 유틸리티 함수를 담고 있는 객체를 리턴한다. 11 | getByText('등록'); 12 | //'등록'이라는 텍스트를 가진 element가 있는지 확인하고 없으면 fail 13 | }); 14 | 15 | //위 테스트랑 같은 테스트 다른 예시 16 | it('has 등록 버튼: test 이름', () => { 17 | const { getByText } = render(); 18 | const button = getByText('등록'); 19 | expect(button).toHaveTextContent('등록'); 20 | //button이 toHaveTextContent('등록')을 가지고있는지 expect하고 맞으면 성공 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /munetic_admin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | ecmaVersion: 13, 8 | sourceType: 'module', 9 | }, 10 | plugins: ['react', '@typescript-eslint'], 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:react/recommended', 14 | 'airbnb', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'plugin:prettier/recommended', 17 | ], 18 | env: { 19 | browser: true, 20 | es2021: true, 21 | }, 22 | rules: { 23 | '@typescript-eslint/interface-name-prefix': 'on', 24 | '@typescript-eslint/explicit-function-return-type': 'on', 25 | '@typescript-eslint/explicit-module-boundary-types': 'on', 26 | '@typescript-eslint/no-explicit-any': 'on', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /munetic_app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | ecmaVersion: 13, 8 | sourceType: 'module', 9 | }, 10 | plugins: ['react', '@typescript-eslint'], 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:react/recommended', 14 | 'airbnb', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'plugin:prettier/recommended', 17 | ], 18 | env: { 19 | browser: true, 20 | es2021: true, 21 | }, 22 | rules: { 23 | '@typescript-eslint/interface-name-prefix': 'on', 24 | '@typescript-eslint/explicit-function-return-type': 'on', 25 | '@typescript-eslint/explicit-module-boundary-types': 'on', 26 | '@typescript-eslint/no-explicit-any': 'on', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /munetic_express/src/models/initdata/user.init.ts: -------------------------------------------------------------------------------- 1 | import { User, Gender, Account } from '../user'; 2 | import * as UserService from '../../service/user.service'; 3 | 4 | function createFirstOwnerAccount() { 5 | const userlist = [{ 6 | login_id: 'munetic@gmail.com', 7 | login_password: 8 | '$2b$10$9ZgatOfeQp5Di8QLo21ODuOFjrm1/zKwgOkJIPD7Yu0Ws.opQTeqK', 9 | name: '대표님', 10 | nickname: '운영자', 11 | birth: new Date(), 12 | gender: Gender.Other, 13 | type: Account.Owner, 14 | email: 'munetic@gmail.com', 15 | }]; 16 | 17 | userlist.forEach(user => { 18 | UserService.createUser(new User({ ...user })).then(() => 19 | console.log('👑 Admin:First Owner account created'), 20 | ); 21 | }); 22 | } 23 | 24 | export default function initialize() { 25 | createFirstOwnerAccount(); 26 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/Comment/CommentHeadCell.tsx: -------------------------------------------------------------------------------- 1 | import { TableCell, Rating } from '@mui/material'; 2 | 3 | export default function CommentTableCell({ row }: any) { 4 | return ( 5 | <> 6 | 7 | {row.content} 8 | 9 | 10 | 11 | 12 | 13 | {row['Lesson.title']} 14 | 15 | 16 | {row.createdAt} 17 | 18 | 19 | {row.deletedAt} 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /munetic_app/src/pages/auth/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Login from '../../components/auth/Login'; 3 | import Button from '../../components/common/Button'; 4 | 5 | const Container = styled.div` 6 | width: 60%; 7 | margin: 0px auto; 8 | `; 9 | 10 | const RegisterButton = styled(Button)` 11 | height: 40px; 12 | border-radius: 5px; 13 | margin-top: 30px; 14 | font-size: 18px; 15 | transition: all 0.7s ease; 16 | :hover { 17 | opacity: 0.8; 18 | } 19 | ::before { 20 | padding-top: 0%; 21 | } 22 | `; 23 | const CustomP = styled.p` 24 | text-align: center; 25 | `; 26 | 27 | export default function LoginPage() { 28 | return ( 29 | 30 | 31 | OR 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { userSignupData, ITutorInfoType } from '../../types/userSignupData'; 2 | import client from './client'; 3 | 4 | export const signup = (userSignupData: userSignupData) => 5 | client.post('/auth/signup', userSignupData); 6 | export const login = (body: { login_id: string; login_password: string }) => 7 | client.post('/auth/login', body); 8 | export const logout = () => client.get('/auth/logout'); 9 | export const isValidInfo = (body: string) => 10 | client.get(`/auth/signup/user?${body}`); 11 | 12 | export const refresh = () => client.get('/auth/refresh'); 13 | export const changeAccount = (to: string) => client.put(`/auth/changeaccount?to=${to}`); 14 | export const tutorsignup = (tutorInfoType: ITutorInfoType) => client.post('/auth/tutorsignup', tutorInfoType); 15 | export const loginCheck = () => client.get('/auth/logincheck'); -------------------------------------------------------------------------------- /munetic_app/src/types/tutorInfoData.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 튜터의 추가 정보를 정의합니다. 기본키나 외래키 제외 모두 optional로 설정합니다. 3 | */ 4 | export interface ITutorInfoTable { 5 | id: number; 6 | user_id: number; 7 | spec?: string; 8 | career?: string; 9 | youtube?: string; 10 | instagram?: string; 11 | soundcloud?: string; 12 | createdAt?: Date; 13 | updatedAt?: Date; 14 | deletedAt?: Date; 15 | } 16 | 17 | /** 18 | * 튜터의 추가 정보 중 필요한 정보만 정의합니다. 19 | */ 20 | export interface ITutorInfoData { 21 | spec?: string; 22 | career?: string; 23 | youtube?: string; 24 | instagram?: string; 25 | soundcloud?: string; 26 | tutor_introduction?: string; 27 | } 28 | 29 | export interface ITutorProfileData { 30 | spec?: string; 31 | career?: string[]; 32 | youtube?: string; 33 | instagram?: string; 34 | soundcloud?: string; 35 | tutor_introduction?: string; 36 | } 37 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/User/UserTableCell.tsx: -------------------------------------------------------------------------------- 1 | import { TableCell } from '@mui/material'; 2 | 3 | export default function UserTableCell({ row }: any) { 4 | return ( 5 | <> 6 | 7 | {row.login_id} 8 | 9 | {row.name} 10 | 11 | {row.type} 12 | 13 | 14 | {row.lastLogin} 15 | 16 | 17 | {row.createdAt} 18 | 19 | 20 | {row.deletedAt} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /munetic_express/src/routes/lesson.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | deleteLesson, 4 | getLesson, 5 | getLessons, 6 | getUserLessons, 7 | patchLesson, 8 | postLesson, 9 | updateLessonOrder, 10 | getLessonsAll, 11 | } from '../controllers/lesson.controller'; 12 | 13 | export const path = '/lesson'; 14 | export const router = Router(); 15 | 16 | router 17 | .post('/', postLesson) // createLesson 18 | .get('/', getLessons) // findLessons 19 | .get('/lesson/:category_id', getLessons) // findLessons 20 | .get('/:id', getLesson) // findLesson 21 | .patch('/:id', patchLesson) // editLesson 22 | .delete('/:id', deleteLesson) // removeLesson 23 | .get('/user/:id', getUserLessons) // findLessonsByUserId 24 | 25 | .patch('/update/:id', updateLessonOrder); // updateLessonOrderByButton 26 | 27 | router.get('/all/:dummy_param', getLessonsAll); // findLessons -------------------------------------------------------------------------------- /munetic_admin/src/contexts/info.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, Dispatch, useContext } from 'react'; 2 | 3 | type Info = object; 4 | type SetInfo = Dispatch>; 5 | 6 | const InfoContext = createContext(null); 7 | const InfoUpdateContext = createContext(null); 8 | 9 | interface Props { 10 | children: JSX.Element | JSX.Element[]; 11 | } 12 | 13 | export default function InfoProvider({ children }: Props) { 14 | const [info, setInfo] = useState({}); 15 | 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | ); 23 | } 24 | 25 | export function useInfo() { 26 | return useContext(InfoContext); 27 | } 28 | 29 | export function useInfoUpdate() { 30 | return useContext(InfoUpdateContext); 31 | } 32 | -------------------------------------------------------------------------------- /munetic_admin/src/contexts/login.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, Dispatch, useContext } from 'react'; 2 | 3 | type Login = boolean; 4 | type SetLogin = Dispatch>; 5 | 6 | const LoginContext = createContext(null); 7 | const LoginUpdateContext = createContext(null); 8 | 9 | interface Props { 10 | children: JSX.Element | JSX.Element[]; 11 | } 12 | 13 | export default function LoginProvider({ children }: Props) { 14 | const [login, setLogin] = useState(false); 15 | 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | ); 23 | } 24 | 25 | export function useLogin() { 26 | return useContext(LoginContext); 27 | } 28 | 29 | export function useLoginUpdate() { 30 | return useContext(LoginUpdateContext); 31 | } 32 | -------------------------------------------------------------------------------- /munetic_app/src/context/Contexts.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from 'react'; 2 | 3 | const Contexts = createContext({ 4 | state: { write: false, validationMode: false, loggedin: false }, 5 | actions: { 6 | setWrite: (bool: boolean) => {}, 7 | setValidationMode: (bool: boolean) => {}, 8 | setLoggedin: (bool: boolean) => {}, 9 | }, 10 | }); 11 | 12 | interface IProps { 13 | children: React.ReactNode; 14 | } 15 | 16 | const ContextProvider = ({ children }: IProps) => { 17 | const [write, setWrite] = useState(false); 18 | const [validationMode, setValidationMode] = useState(false); 19 | const [loggedin, setLoggedin] = useState(false); 20 | const value = { 21 | state: { write, validationMode, loggedin }, 22 | actions: { setWrite, setValidationMode, setLoggedin }, 23 | }; 24 | 25 | return {children}; 26 | }; 27 | 28 | export { ContextProvider }; 29 | 30 | export default Contexts; 31 | -------------------------------------------------------------------------------- /munetic_app/README.md: -------------------------------------------------------------------------------- 1 | ## 프론트앤드 (munetic_app) 2 | 3 | ### build & run 4 | - 빌드 도구는 vite를 사용하며 TypeSctipt + React 기반입니다. 5 | - tsc / vite build 명령으로 컴파일 합니다. 6 | - serve ./dist 를 이용해 정적 파일 서버 형태로 배포합니다. 7 | ### 디렉토리 구조 8 | - dist (빌드 시 생성) → vite가 빌드한 결과물이 생성됩니다. 9 | - /src 10 | - /components → React 컴포넌트들이 정의되어 있습니다. 11 | - /context → 전역적인 상태 관리를 위한 모듈입니다. 12 | - 보통 redux를 사용하지만 상태 관리 객체가 얼마 없어 리액트의 Context를 이용합니다. 13 | - /lib → API 접근을 위한 객체가 정의되어 있는 모듈입니다. 14 | - axios와 HTTP URI를 이용해 HTTP Request를 보내기 위한 객체들을 정의합니다. 15 | - /pages → 페이지 컴포넌트가 정의되어 있습니다. 16 | - react-router에 의해 라우팅되는 타겟의 컴포넌트들이 정의되어 있습니다. 17 | - /style → CSS 태그로 정의되는 스타일 컴포넌트들이 정의되어 있습니다. 18 | - 스타일 컴포넌트들은 styled-components를 이용해 정의합니다. 19 | - /tests → jest 테스트 파일들이 있습니다. 20 | - 단위 테스트, 통합 테스트가 있습니다. 21 | - /types → 타입들이 정의되어 있습니다. 22 | - App.tsx → 리액트의 메인 앱(페이지)입니다. 23 | - main.tsx → 메인 페이지 입니다. 24 | - Routing.tsx → react-router를 이용해 라우팅 경로들을 정의합니다. -------------------------------------------------------------------------------- /munetic_express/src/mapping/LessonMapper.ts: -------------------------------------------------------------------------------- 1 | import { Category } from "../models/category"; 2 | import { Lesson } from "../models/lesson"; 3 | import { User } from "../models/user"; 4 | import { LessonAllInfo } from "../types/service/lesson.service"; 5 | 6 | 7 | /** 8 | * Lesson 엔티티를 LessonAllInfo로 매핑합니다. 9 | * 10 | * @param lesson Lesson 11 | * @returns LessonAllInfo 12 | * @author joohongpark 13 | */ 14 | export function toLessonAllInfo(lesson: Lesson, CommentsCount?: number, LessonLikesCount?: number) : LessonAllInfo { 15 | return { 16 | id: lesson.id, 17 | tutor_id: lesson.tutor_id, 18 | title: lesson.title, 19 | price: lesson.price || undefined, 20 | location: lesson.location || undefined, 21 | minute_per_lesson: lesson.minute_per_lesson || undefined, 22 | content: lesson.content || undefined, 23 | Category: lesson.get('Category') as Category, 24 | User: lesson.get('User') as User, 25 | CommentsCount, 26 | LessonLikesCount, 27 | }; 28 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Inputs/CustomSelect.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | InputLabel, 4 | Select, 5 | MenuItem, 6 | SelectChangeEvent, 7 | } from '@mui/material'; 8 | 9 | interface SelectProps { 10 | width: string; 11 | onChangeEvent: (event: SelectChangeEvent) => void; 12 | fontSize: string; 13 | value: string; 14 | items: string[]; 15 | } 16 | 17 | export default function CustomSelect({ 18 | width, 19 | onChangeEvent, 20 | fontSize, 21 | value, 22 | items, 23 | }: SelectProps) { 24 | return ( 25 | 26 | Auth 27 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /munetic_express/src/swagger/apis/category.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: API Title 4 | version: '1.0' 5 | tags: 6 | - name: Category 7 | description: Category Lists 8 | paths: 9 | # Category 관련 API 10 | /category: 11 | get: 12 | tags: 13 | - Category 14 | summary: Get Category Lists 15 | consumes: 16 | - application/json 17 | produces: 18 | - application/json 19 | responses: 20 | '200': 21 | description: Successfully got category Lists 22 | schema: 23 | type: object 24 | properties: 25 | message: 26 | type: string 27 | example: 'Success' 28 | data: 29 | $ref: '#definitions/Category' 30 | '400': 31 | description: 'Bad Request' 32 | schema: 33 | type: object 34 | properties: 35 | message: 36 | type: string 37 | example: 'Failed' 38 | -------------------------------------------------------------------------------- /munetic_app/src/components/common/ToggleBtn.tsx: -------------------------------------------------------------------------------- 1 | import ToggleButton from '@mui/material/ToggleButton'; 2 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; 3 | 4 | interface ToggleBtnProps { 5 | first: string; 6 | second: string; 7 | value: boolean | undefined; 8 | handleChange: ( 9 | event: React.MouseEvent, 10 | newAlignment: boolean, 11 | ) => void; 12 | } 13 | 14 | export default function ToggleBtn({ 15 | first, 16 | second, 17 | value, 18 | handleChange, 19 | }: ToggleBtnProps) { 20 | return ( 21 | 29 | 30 | {first} 31 | 32 | 33 | {second} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /munetic_app/src/types/lessonData.d.ts: -------------------------------------------------------------------------------- 1 | import { ICategoryTable } from './categoryData'; 2 | import { IUserTable } from './userData'; 3 | import { ICommentTable } from './commentData'; 4 | import { ILessonLikeTable } from './lessonLikeData'; 5 | 6 | /** 7 | * 레슨 테이블의 데이터 타입을 정의합니다. 기본키나 외래키 제외 모두 optional로 설정합니다. 8 | */ 9 | export interface ILessonTable { 10 | id: number; 11 | tutor_id: number; 12 | category_id: number; 13 | title?: string | null; 14 | price?: number | null; 15 | location?: string | null; 16 | minute_per_lesson?: number | null; 17 | content?: string | null; 18 | youtube?: string | null; 19 | createdAt?: Date; 20 | updatedAt?: Date; 21 | deletedAt?: Date; 22 | } 23 | 24 | /** 25 | * 레슨 테이블 외 카테고리, 강사 정보를 연관하여 가져올 때의 데이터 타입입니다. 26 | */ 27 | export interface ILessonData extends ILessonTable { 28 | Category: ICategoryTable; 29 | User: IUserTable; 30 | Comments: ICommentTable[]; 31 | LessonLikes: ILessonLikeTable[]; 32 | CommentsCount?: number; 33 | LessonLikesCount?: number; 34 | } 35 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Common/TextFields.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const TextFields = styled.div` 4 | display: flex; 5 | width: 100%; 6 | `; 7 | 8 | export const TextField = styled.div` 9 | margin: auto 0; 10 | flex: 1; 11 | padding-top: 1rem; 12 | padding-left: 1rem; 13 | display: flex; 14 | width: 100%; 15 | & p { 16 | font-size: 1.3rem; 17 | font-weight: 700; 18 | margin-bottom: 0.7rem; 19 | width: 8rem; 20 | } 21 | & div { 22 | margin: auto 0; 23 | padding-bottom: 0.3rem; 24 | font-size: 1.3rem; 25 | min-width: 10rem; 26 | max-width: 30rem; 27 | } 28 | `; 29 | 30 | export const TextField_ = styled.div` 31 | flex: 1; 32 | padding-top: 1rem; 33 | display: flex; 34 | width: 100%; 35 | & p { 36 | border-left: 0.1rem solid grey; 37 | padding-left: 3rem; 38 | font-size: 1.3rem; 39 | font-weight: 700; 40 | margin-bottom: 0.7rem; 41 | width: 8rem; 42 | } 43 | & div { 44 | padding-bottom: 0.3rem; 45 | font-size: 1.3rem; 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /munetic_proxy/templates/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name ${SERVER_HOST}; 4 | 5 | location /.well-known/acme-challenge/ { 6 | root /var/www/certbot; 7 | } 8 | location / { 9 | return 301 https://$host$request_uri; 10 | } 11 | } 12 | 13 | server { 14 | listen 443 ssl; 15 | server_name ${SERVER_HOST}; 16 | resolver 127.0.0.11 valid=5s; # 도커 네트워크 내부의 기본 DNS 서버 주소 17 | 18 | ssl_certificate /etc/letsencrypt/live/${SERVER_HOST}/fullchain.pem; 19 | ssl_certificate_key /etc/letsencrypt/live/${SERVER_HOST}/privkey.pem; 20 | 21 | include /etc/letsencrypt/options-ssl-nginx.conf; 22 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 23 | 24 | # domain name을 ip 주소로 치환하기 위한 설정 25 | set $admin munetic_admin; 26 | set $app munetic_app; 27 | set $express munetic_express; 28 | 29 | location /admin { 30 | rewrite /admin(.*) /$1 break; 31 | 32 | proxy_pass http://$admin:4242; 33 | } 34 | 35 | location / { 36 | proxy_pass http://$app:2424; 37 | } 38 | 39 | location /api { 40 | proxy_pass http://$express:3030; 41 | } 42 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/User/UserGrid.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Grid } from '@mui/material'; 3 | import OverView from './OverView'; 4 | import UserInfo from './UserInfo'; 5 | import AdminMemo from './AdminMemo'; 6 | import UserPosts from './UserPosts'; 7 | import { useInfo } from '../../../contexts/info'; 8 | import Item from '../Common/Item'; 9 | 10 | export default function UserGrid() { 11 | const info = useInfo() as any; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {info.type === 'Tutor' && ( 31 | 32 | 33 | 34 | )} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /munetic_app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | import { ContextProvider } from './context/Contexts'; 6 | import * as Auth from './lib/api/auth'; 7 | import client from './lib/api/client'; 8 | 9 | async function loadUser() { 10 | try { 11 | const loggedUser = localStorage.getItem('user'); 12 | if (!loggedUser) { 13 | return; 14 | } 15 | try { 16 | const res = await Auth.refresh(); 17 | const { data: accessToken } = res.data; 18 | client.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; 19 | } catch (e) { 20 | console.log(e, '로그인을 유지하지 못했습니다.'); 21 | } 22 | } catch (e) { 23 | console.log(e, 'localStorage is not working'); 24 | } 25 | } 26 | 27 | loadUser(); 28 | 29 | ReactDOM.render( 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | document.getElementById('root'), 38 | ); 39 | -------------------------------------------------------------------------------- /munetic_express/src/modules/imgCreateMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import multer from 'multer'; 3 | import path from 'path'; 4 | 5 | const storage = multer.diskStorage({ 6 | destination: function (req, file, cb) { 7 | cb(null, '../munetic_app/public/img'); 8 | }, 9 | filename: function (req, file, cb) { 10 | const ext = path.extname(file.originalname); 11 | cb(null, path.basename(file.originalname, ext) + '-' + Date.now() + ext); 12 | }, 13 | }); 14 | const storageConfig = multer({ 15 | storage: storage, 16 | limits: { fileSize: 1000000 }, 17 | fileFilter: function (req, file, callback) { 18 | const ext = path.extname(file.originalname); 19 | if ( 20 | ext !== '.png' && 21 | ext !== '.jpg' && 22 | ext !== '.jpeg' && 23 | ext !== '.gif' && 24 | ext !== '.svg' 25 | ) { 26 | return callback(new Error('이미지 형식이 잘못됐습니다.')); 27 | } 28 | callback(null, true); 29 | }, 30 | }); 31 | 32 | export const imgUpload: RequestHandler = (req, res, next) => { 33 | return storageConfig.single('img')(req, res, next); 34 | }; 35 | -------------------------------------------------------------------------------- /munetic_express/src/modules/jwt.admin.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import passport from 'passport'; 3 | import { Strategy } from 'passport-jwt'; 4 | 5 | import * as UserService from '../service/user.service'; 6 | import { accessOpts, refreshOpts } from './jwt.local.strategy'; 7 | 8 | const JwtAdminStrategyCallback = async ( 9 | jwt_payload: { sub: any; login_id: any }, 10 | done: any, 11 | ) => { 12 | const [user] = await UserService.searchActiveUser({ 13 | login_id: jwt_payload.login_id, 14 | type: { [Op.or]: ['Admin', 'Owner'] }, 15 | }); 16 | if (user) { 17 | return done(null, user.toJSON()); 18 | } else { 19 | return done(null, false); 20 | } 21 | }; 22 | 23 | export const JwtAdminAccessStrategy = () => 24 | new Strategy(accessOpts, JwtAdminStrategyCallback); 25 | 26 | export const JwtAdminRefreshStrategy = () => 27 | new Strategy(refreshOpts, JwtAdminStrategyCallback); 28 | 29 | export const jwtAdminAuth = () => 30 | passport.authenticate('jwt-admin', { session: false }); 31 | 32 | export const jwtReAdminAuth = () => 33 | passport.authenticate('jwtRefresh-admin', { session: false }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Innovation Academy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/AdminUser/adminUserHeadCells.ts: -------------------------------------------------------------------------------- 1 | export interface AdminUserData { 2 | id: number; 3 | name: string; 4 | login_id: string; 5 | type: string; 6 | createdAt: string; 7 | deletedAt: string; 8 | } 9 | 10 | export interface AdminUserHeadCell { 11 | disablePadding: boolean; 12 | id: keyof AdminUserData; 13 | label: string; 14 | numeric: boolean; 15 | } 16 | 17 | export const adminUserHeadCells: readonly AdminUserHeadCell[] = [ 18 | { 19 | id: 'id', 20 | numeric: false, 21 | disablePadding: true, 22 | label: 'No.', 23 | }, 24 | { 25 | id: 'login_id', 26 | numeric: false, 27 | disablePadding: false, 28 | label: '이메일', 29 | }, 30 | { 31 | id: 'name', 32 | numeric: false, 33 | disablePadding: false, 34 | label: '이름', 35 | }, 36 | { 37 | id: 'type', 38 | numeric: false, 39 | disablePadding: false, 40 | label: '유형', 41 | }, 42 | { 43 | id: 'createdAt', 44 | numeric: false, 45 | disablePadding: false, 46 | label: '생성일', 47 | }, 48 | { 49 | id: 'deletedAt', 50 | numeric: false, 51 | disablePadding: false, 52 | label: '삭제일', 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /munetic_app/src/components/ui/SnsButtons.tsx: -------------------------------------------------------------------------------- 1 | import { Instagram, YouTube, CloudQueue } from '@mui/icons-material'; 2 | import { IconButton } from '@mui/material'; 3 | 4 | 5 | 6 | /** 7 | * TutorSnsButtons 컴포넌트의 프로퍼티 정의 8 | */ 9 | export interface LessonItemIProps { 10 | instagramId?: string, 11 | youtubeChannel?: string, 12 | soundcloudId?: string 13 | } 14 | 15 | 16 | export default function SnsButtons(props: LessonItemIProps) { 17 | return ( 18 | <> 19 | 24 | 25 | 26 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | ); 42 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/Comment/CommentHeadCells.ts: -------------------------------------------------------------------------------- 1 | export interface CommentData { 2 | id: number; 3 | content: string; 4 | stars: number; 5 | 'Lesson.title': string; 6 | createdAt: string; 7 | deletedAt: string; 8 | } 9 | 10 | export interface CommentHeadCell { 11 | disablePadding: boolean; 12 | id: keyof CommentData; 13 | label: string; 14 | numeric: boolean; 15 | } 16 | 17 | export const CommentHeadCells: readonly CommentHeadCell[] = [ 18 | { 19 | id: 'id', 20 | numeric: false, 21 | disablePadding: true, 22 | label: 'No.', 23 | }, 24 | { 25 | id: 'content', 26 | numeric: false, 27 | disablePadding: false, 28 | label: '댓글', 29 | }, 30 | { 31 | id: 'stars', 32 | numeric: false, 33 | disablePadding: false, 34 | label: '별점', 35 | }, 36 | { 37 | id: 'Lesson.title', 38 | numeric: false, 39 | disablePadding: false, 40 | label: '레슨 제목', 41 | }, 42 | { 43 | id: 'createdAt', 44 | numeric: false, 45 | disablePadding: false, 46 | label: '생성일', 47 | }, 48 | { 49 | id: 'deletedAt', 50 | numeric: false, 51 | disablePadding: false, 52 | label: '삭제일', 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: remote ssh command for deploy 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - main 7 | - test/github_actions 8 | paths-ignore: 9 | - 'README.md' 10 | 11 | jobs: 12 | build-dev: 13 | name: deploy-dev 14 | runs-on: ubuntu-latest 15 | if: ${{ github.ref == 'refs/heads/develop' }} 16 | steps: 17 | - name: executing remote ssh commands using key 18 | uses: appleboy/ssh-action@master 19 | with: 20 | host: ${{ secrets.HOST }} 21 | username: ${{ secrets.USERNAME }} 22 | key: ${{ secrets.KEY }} 23 | port: ${{ secrets.PORT }} 24 | script: | 25 | ./build-dev.sh 26 | 27 | build: 28 | name: deploy 29 | runs-on: ubuntu-latest 30 | if: ${{ github.ref == 'refs/heads/main' }} 31 | steps: 32 | - name: executing remote ssh commands using key 33 | uses: appleboy/ssh-action@master 34 | with: 35 | host: ${{ secrets.HOST }} 36 | username: ${{ secrets.USERNAME }} 37 | key: ${{ secrets.KEY }} 38 | port: ${{ secrets.PORT }} 39 | script: | 40 | ./build.sh 41 | -------------------------------------------------------------------------------- /munetic_express/src/modules/admin.strategy.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import passportLocal from 'passport-local'; 3 | import * as UserService from '../service/user.service'; 4 | 5 | const Strategy = passportLocal.Strategy; 6 | 7 | function verifyPassword(password: string, encryptedPassword: string): boolean { 8 | return bcrypt.compareSync(password, encryptedPassword); 9 | } 10 | 11 | const adminStrategyCallback = async ( 12 | login_id: string, 13 | login_password: string, 14 | done: any, 15 | ) => { 16 | const [user] = await UserService.searchActiveUser({ login_id }); 17 | if (!user || (user.type !== 'Admin' && user.type !== 'Owner')) 18 | return done(null, false, { 19 | message: '입력하신 id에 해당하는 계정이 없습니다.', 20 | }); 21 | const encryptedPassword = (await user?.toJSON().login_password) as string; 22 | if (!(await verifyPassword(login_password, encryptedPassword))) 23 | return done(null, false, { message: '잘못된 비밀번호 입니다.' }); 24 | 25 | return done(null, user.toJSON()); 26 | }; 27 | 28 | const AdminStrategy = () => 29 | new Strategy( 30 | { usernameField: 'login_id', passwordField: 'login_password' }, 31 | adminStrategyCallback, 32 | ); 33 | 34 | export default AdminStrategy; 35 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/Lesson/LessonHeadCells.ts: -------------------------------------------------------------------------------- 1 | export interface LessonData { 2 | id: number; 3 | 'User.login_id': string; 4 | 'Category.name': string; 5 | title: string; 6 | location: string; 7 | createdAt: string; 8 | deletedAt: string; 9 | } 10 | 11 | export interface LessonHeadCell { 12 | disablePadding: boolean; 13 | id: keyof LessonData; 14 | label: string; 15 | numeric: boolean; 16 | } 17 | 18 | export const lessonHeadCells: readonly LessonHeadCell[] = [ 19 | { 20 | id: 'id', 21 | numeric: false, 22 | disablePadding: true, 23 | label: 'No.', 24 | }, 25 | { 26 | id: 'User.login_id', 27 | numeric: false, 28 | disablePadding: false, 29 | label: '아이디', 30 | }, 31 | { 32 | id: 'Category.name', 33 | numeric: false, 34 | disablePadding: false, 35 | label: '카테고리', 36 | }, 37 | { 38 | id: 'title', 39 | numeric: false, 40 | disablePadding: false, 41 | label: '제목', 42 | }, 43 | { 44 | id: 'createdAt', 45 | numeric: false, 46 | disablePadding: false, 47 | label: '생성일', 48 | }, 49 | { 50 | id: 'deletedAt', 51 | numeric: false, 52 | disablePadding: false, 53 | label: '삭제일', 54 | }, 55 | ]; 56 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/lesson.ts: -------------------------------------------------------------------------------- 1 | import { ILessonTable } from '../../types/lessonData'; 2 | import client from './client'; 3 | 4 | export const postLesson = async (id: number, body: ILessonTable) => { 5 | return await client.post(`/lesson?tutor_id=${id}`, body); 6 | }; 7 | export const getLesson = (id: number) => client.get(`/lesson/${id}`); 8 | export const getLessonByUserId = (id: number, limit: number, offset: number) => 9 | client.get(`/lesson/user/${id}?limit=${limit}&offset=${offset}`); 10 | export const getLessons = (limit: number, offset: number) => 11 | client.get(`/lesson/?limit=${limit}&offset=${offset}`); 12 | export const getLessonsByCategoryId = (category: number, limit: number, offset: number) => 13 | client.get(`/lesson/lesson/${category}?limit=${limit}&offset=${offset}`); 14 | export const editLessonById = async (id: number, body: ILessonTable) => { 15 | return await client.patch(`/lesson/${id}`, body); 16 | }; 17 | export const deleteLessonById = (id: number) => client.delete(`/lesson/${id}`); 18 | export const updateLessonOrder = async (id: number) => await client.patch(`/lesson/update/${id}`); 19 | 20 | export const getLessonsAll = (limit: number, offset: number) => 21 | client.get(`/lesson/all/dummy?limit=${limit}&offset=${offset}`); -------------------------------------------------------------------------------- /munetic_express/src/tests/unit/modules.unit.test.ts: -------------------------------------------------------------------------------- 1 | import '@types/jest'; 2 | import * as Status from 'http-status'; 3 | import * as httpMocks from 'node-mocks-http'; 4 | import ErrorResponse from '../../modules/errorResponse'; 5 | import errorHandler from '../../modules/errorHandler'; 6 | 7 | describe('module : errorHandler unit test', () => { 8 | let req: any, res: any, next: any; 9 | const err = new Error('error'); 10 | const errResponse = new ErrorResponse( 11 | Status.BAD_REQUEST, 12 | '잘못된 요청입니다.', 13 | ); 14 | beforeEach(() => { 15 | req = httpMocks.createRequest(); 16 | res = httpMocks.createResponse(); 17 | next = jest.fn(); 18 | }); 19 | 20 | it('err가 ErrorResponse 인스턴스가 아니면 새로운 ErrorResponse 객체를 생성해 응답한다.', async () => { 21 | await errorHandler(err, req, res, next); 22 | expect(res.statusCode).toBe(Status.INTERNAL_SERVER_ERROR); 23 | expect(res._getJSONData()).toBe( 24 | '서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', 25 | ); 26 | }); 27 | it('err가 ErrorResponse 인스턴스이면 인스턴스 정보를 사용해 응답한다.', async () => { 28 | await errorHandler(errResponse, req, res, next); 29 | expect(res.statusCode).toBe(Status.BAD_REQUEST); 30 | expect(res._getJSONData()).toStrictEqual(errResponse.message); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /munetic_express/src/tests/20211217154104-Category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.bulkInsert('Category', [ 6 | // { 7 | // name: '기타', 8 | // createdAt: Sequelize.fn('now'), 9 | // updatedAt: Sequelize.fn('now'), 10 | // }, 11 | // { 12 | // name: '바이올린', 13 | // createdAt: Sequelize.fn('now'), 14 | // updatedAt: Sequelize.fn('now'), 15 | // }, 16 | // { 17 | // name: '드럼', 18 | // createdAt: Sequelize.fn('now'), 19 | // updatedAt: Sequelize.fn('now'), 20 | // }, 21 | // { 22 | // name: '피아노', 23 | // createdAt: Sequelize.fn('now'), 24 | // updatedAt: Sequelize.fn('now'), 25 | // }, 26 | // { 27 | // name: '하프', 28 | // createdAt: Sequelize.fn('now'), 29 | // updatedAt: Sequelize.fn('now'), 30 | // }, 31 | // { 32 | // name: '첼로', 33 | // createdAt: Sequelize.fn('now'), 34 | // updatedAt: Sequelize.fn('now'), 35 | // }, 36 | ]); 37 | }, 38 | 39 | down: async (queryInterface, Sequelize) => { 40 | await queryInterface.bulkDelete('Category', null, {}); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /munetic_express/src/types/service/lesson.service.d.ts: -------------------------------------------------------------------------------- 1 | import { categoryAttributes } from '../../models/category' 2 | import { userAttributes } from '../../models/user' 3 | 4 | /** 5 | * 레슨이 카테고리, 유저 정보를 포함하고 있는 타입을 정의합니다. 6 | * 이 타입은 `Lesson.findOne` or `Lesson.findAll` 에서 사용됩니다. 7 | * 8 | * @author Jonghyun Lim 9 | * @version 1 10 | */ 11 | export interface LessonAllInfo { 12 | id: number; 13 | tutor_id: number; 14 | title: string; 15 | price?: number; 16 | location?: string; 17 | minute_per_lesson?: number; 18 | content?: string; 19 | Category: categoryAttributes; 20 | User: userAttributes; 21 | CommentsCount?: number; 22 | LessonLikesCount?: number; 23 | }; 24 | 25 | /** 26 | * findAndCountAll의 리턴 타입입니다. 27 | * 28 | * @author Jonghyun Lim 29 | */ 30 | export interface CountRows { 31 | count: number; 32 | rows: T[]; 33 | } 34 | 35 | /** 36 | * HTTP 요청의 Req Body입니다. 레슨의 수정 가능한 항목들을 나타냅니다. 37 | * # TODO: 레슨의 수정 뿐만 아니라 추가시에도 동일한 인터페이스가 사용되므로 인터페이스 이름 변경 필요 38 | * 39 | * @author Jonghyun Lim 40 | * @version 1 41 | */ 42 | export interface LessonEditable { 43 | tutor_id?: number; 44 | category_id?: number; 45 | title?: string; 46 | price?: number; 47 | location?: string; 48 | minute_per_lesson?: number; 49 | content?: string; 50 | } -------------------------------------------------------------------------------- /munetic_app/src/lib/api/profile.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | export interface NewProfileInfoType { 4 | type?: string; 5 | nickname?: string; 6 | name_public?: boolean; 7 | phone_public?: boolean; 8 | image_url?: string | null; 9 | introduction?: string | null; 10 | } 11 | 12 | export interface NewTutorProfileInfoType { 13 | spec?: string; 14 | career?: string; 15 | youtube?: string; 16 | instagram?: string; 17 | soundcloud?: string; 18 | tutor_introduction?: string; 19 | } 20 | 21 | export interface forEditTutorProfileInfoType { 22 | spec?: string; 23 | career?: string[]; 24 | youtube?: string; 25 | instagram?: string; 26 | soundcloud?: string; 27 | tutor_introduction?: string; 28 | } 29 | 30 | export const updateProfile = (body: NewProfileInfoType) => 31 | client.patch('/user', body); 32 | export const getProfileById = (id: number) => client.get(`/user/${id}`); 33 | export const getMyProfile = () => client.get('/user'); 34 | export const createProfileImg = (body: FormData) => 35 | client.post('/user/image', body); 36 | export const getTutorProfileById = (id: number) => 37 | client.get(`/user/tutor/${id}`); 38 | export const updateTutorProfile = (body: NewTutorProfileInfoType, id: number) => 39 | client.patch(`/user/tutor`, body); 40 | -------------------------------------------------------------------------------- /munetic_express/src/modules/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import passportLocal from 'passport-local'; 3 | import * as UserService from '../service/user.service'; 4 | 5 | const Strategy = passportLocal.Strategy; 6 | 7 | function verifyPassword(password: string, encryptedPassword: string): boolean { 8 | return bcrypt.compareSync(password, encryptedPassword); 9 | } 10 | 11 | const localStrategyCallback = async ( 12 | login_id: string, 13 | login_password: string, 14 | done: any, 15 | ) => { 16 | const [user] = await UserService.searchActiveUser({ login_id }); 17 | if ( 18 | !user || 19 | (user.type !== 'Tutor' && user.type !== 'Student') || 20 | user.deletedAt !== null 21 | ) 22 | return done(null, false, { 23 | message: '입력하신 id에 해당하는 계정이 없습니다.', 24 | }); 25 | const encryptedPassword = (await user?.toJSON().login_password) as string; 26 | if (!(await verifyPassword(login_password, encryptedPassword))) 27 | return done(null, false, { message: '잘못된 비밀번호 입니다.' }); 28 | 29 | return done(null, user.toJSON()); 30 | }; 31 | 32 | const LocalStrategy = () => 33 | new Strategy( 34 | { usernameField: 'login_id', passwordField: 'login_password' }, 35 | localStrategyCallback, 36 | ); 37 | 38 | export default LocalStrategy; 39 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/User/userHeadCells.ts: -------------------------------------------------------------------------------- 1 | export interface UserData { 2 | id: number; 3 | name: string; 4 | login_id: string; 5 | type: string; 6 | lastLogin: string; 7 | createdAt: string; 8 | deletedAt: string; 9 | } 10 | 11 | export interface UserHeadCell { 12 | disablePadding: boolean; 13 | id: keyof UserData; 14 | label: string; 15 | numeric: boolean; 16 | } 17 | 18 | export const userHeadCells: readonly UserHeadCell[] = [ 19 | { 20 | id: 'id', 21 | numeric: false, 22 | disablePadding: true, 23 | label: 'No.', 24 | }, 25 | { 26 | id: 'login_id', 27 | numeric: false, 28 | disablePadding: false, 29 | label: '아이디', 30 | }, 31 | { 32 | id: 'name', 33 | numeric: false, 34 | disablePadding: false, 35 | label: '이름', 36 | }, 37 | { 38 | id: 'type', 39 | numeric: false, 40 | disablePadding: false, 41 | label: '유형', 42 | }, 43 | { 44 | id: 'lastLogin', 45 | numeric: false, 46 | disablePadding: false, 47 | label: '마지막 로그인', 48 | }, 49 | { 50 | id: 'createdAt', 51 | numeric: false, 52 | disablePadding: false, 53 | label: '생성일', 54 | }, 55 | { 56 | id: 'deletedAt', 57 | numeric: false, 58 | disablePadding: false, 59 | label: '삭제일', 60 | }, 61 | ]; 62 | -------------------------------------------------------------------------------- /munetic_admin/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Menu from './components/Menu/Menu'; 2 | import Routing from './components/Routing'; 3 | import GlobalStyle from './style/GlobalStyle'; 4 | import LoginPage from './pages/LoginPage'; 5 | import { useLogin, useLoginUpdate } from './contexts/login'; 6 | import { useEffect, useState } from 'react'; 7 | import { CircularProgress } from '@mui/material'; 8 | import * as Api from './lib/api'; 9 | 10 | function App() { 11 | const [isLoading, setIsLoading] = useState(true); 12 | 13 | const login = useLogin(); 14 | const setLogin = useLoginUpdate(); 15 | 16 | useEffect(() => { 17 | Api.refresh() 18 | .then(res => { 19 | Api.instance.defaults.headers.common[ 20 | 'Authorization' 21 | ] = `Bearer ${res.data.data}`; 22 | if (setLogin) setLogin(true); 23 | setIsLoading(false); 24 | }) 25 | .catch(err => { 26 | if (err.respose) alert(err.response.data); 27 | setIsLoading(false); 28 | }); 29 | }, []); 30 | 31 | return ( 32 | <> 33 | 34 | {isLoading ? ( 35 | 36 | ) : login ? ( 37 | <> 38 | 39 | 40 | 41 | ) : ( 42 | 43 | )} 44 | 45 | ); 46 | } 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /munetic_app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import TopBar from './components/common/TopBar'; 3 | import Routing from './Routing'; 4 | import GlobalStyle from './style/GlobalStyle'; 5 | import BottomMenu from './components/common/BottomMenu'; 6 | import Contexts from './context/Contexts'; 7 | import loginCheck from './lib/auth/loginCheck'; 8 | import Logout from './lib/auth/logout'; 9 | 10 | function App() { 11 | const { actions } = useContext(Contexts); 12 | const isLoggedIn: boolean = Boolean(localStorage.getItem('user')); 13 | 14 | useEffect(() => { 15 | async function checkLogin() { 16 | const result = await loginCheck(); 17 | if (!result && isLoggedIn) { 18 | Logout(); //임의로 로컬스토리지에 user 정보 기입하는 행위 방지 19 | } 20 | actions.setLoggedin(isLoggedIn); 21 | } 22 | checkLogin(); 23 | }, []); 24 | 25 | return ( 26 | // FIXME: 임시로 글꼴 설정했는데 추후에 바꾸어야 할 듯 합니다. by joohongpark 27 |
33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /munetic_app/src/components/ui/SwitchWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import FormControlLabel from '@mui/material/FormControlLabel'; 3 | import Switch from '@mui/material/Switch'; 4 | 5 | /** 6 | * SwitchWithLabel 컴포넌트의 프로퍼티 정의 7 | */ 8 | export interface SwitchWithLabelIProps { 9 | init?: boolean; // 초기값 10 | label?: string; // 카테고리명 11 | change?: (changeTo: boolean) => Promise; // 삭제 콜백함수 (optional) 12 | } 13 | 14 | /** 15 | * mui 라이브러리를 사용한 라벨을 포함한 스위치 컴포넌트 16 | * 17 | * @param props init, label, change 18 | * @returns 리액트 앨리먼트 19 | * @author joohongpark 20 | */ 21 | export default function SwitchWithLabel(props: SwitchWithLabelIProps) { 22 | const [checked, setChecked] = useState(props.init || true); 23 | 24 | const onChange = async (e: React.ChangeEvent, c: boolean): Promise => { 25 | console.log(c); 26 | if (props.change === undefined) { 27 | setChecked(c); 28 | } else { 29 | if (await props.change(c)) { 30 | setChecked(c); 31 | } 32 | } 33 | }; 34 | 35 | return ( 36 | props.label ? 37 | } label={props.label} /> 38 | : 39 | 40 | ); 41 | } -------------------------------------------------------------------------------- /munetic_admin/src/components/Inputs/CustomInput.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, InputLabel, Input } from '@mui/material'; 2 | 3 | interface InputProps { 4 | width: string; 5 | text: string; 6 | fontSize: string; 7 | name: string; 8 | value: string; 9 | onChangeEvent: (event: React.ChangeEvent) => void; 10 | error?: any; 11 | } 12 | 13 | export default function CustomInput({ 14 | width, 15 | text, 16 | fontSize, 17 | name, 18 | value, 19 | error, 20 | onChangeEvent, 21 | }: InputProps) { 22 | return ( 23 | <> 24 | {error ? ( 25 | 26 | 27 | {text} 28 | 29 | 35 | 36 | ) : ( 37 | 38 | 39 | {text} 40 | 41 | 47 | 48 | )} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /munetic_app/src/components/like/MyLikeLessons.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { LessonItem, LessonItemIProps } from '../lesson/lessonlist/LessonItem'; 3 | import * as LikeAPI from '../../lib/api/like'; 4 | import getCategoriesByMap from '../../lib/getCategoriesByMap'; 5 | 6 | export default function MyLikesPage() { 7 | const [lessons, setLessons] = useState>([]) 8 | 9 | useEffect(() => { 10 | async function getMyProfile() { 11 | try { 12 | const categoriesMap = await getCategoriesByMap(); 13 | const lessonLikeRes = await LikeAPI.getLessonLikes(); 14 | const categories = lessonLikeRes.data.data.map((c: any) => ({ 15 | lesson_id: c.lesson_id, 16 | category: categoriesMap.get(c.Lesson.category_id), 17 | title: c.Lesson.content, 18 | })); 19 | setLessons(categories); 20 | } catch (e) { 21 | console.log(e, '좋아요한 강의 목록을 불러오지 못했습니다.'); 22 | } 23 | } 24 | getMyProfile(); 25 | }, []); 26 | 27 | return ( 28 | <> 29 | { 30 | lessons.map((lessons) => 31 | 37 | ) 38 | } 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /munetic_admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "munetic_admin", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "npx vite", 6 | "dev": "tsc && npx vite build && vite serve", 7 | "serve": "tsc && npx vite build && serve dist -s -l 4242" 8 | }, 9 | "dependencies": { 10 | "@emotion/react": "^11.7.1", 11 | "@emotion/styled": "^11.6.0", 12 | "@mui/icons-material": "^5.2.5", 13 | "@mui/material": "^5.2.3", 14 | "@types/styled-components": "^5.1.18", 15 | "axios": "^0.24.0", 16 | "react": "^17.0.0", 17 | "react-dom": "^17.0.0", 18 | "react-router-dom": "^6.0.2", 19 | "serve": "^13.0.2", 20 | "styled-components": "^5.3.3", 21 | "styled-reset": "^4.3.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^17.0.2", 25 | "@types/react": "^17.0.0", 26 | "@types/react-dom": "^17.0.0", 27 | "@typescript-eslint/eslint-plugin": "^5.6.0", 28 | "@typescript-eslint/parser": "^5.6.0", 29 | "@vitejs/plugin-react": "^1.0.0", 30 | "eslint": "^8.4.1", 31 | "eslint-config-airbnb": "^19.0.2", 32 | "eslint-config-prettier": "^8.3.0", 33 | "eslint-plugin-import": "^2.25.3", 34 | "eslint-plugin-jsx-a11y": "^6.5.1", 35 | "eslint-plugin-prettier": "^4.0.0", 36 | "eslint-plugin-react": "^7.27.1", 37 | "eslint-plugin-react-hooks": "^4.3.0", 38 | "typescript": "^4.3.2", 39 | "vite": "^2.6.14" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /munetic_express/src/mapping/TutorInfoMapper.ts: -------------------------------------------------------------------------------- 1 | import { TutorInfo } from '../models/tutorInfo'; 2 | import { 3 | ITutorInfoType, 4 | ITutorInfoInsertType, 5 | } from '../types/controller/tutorInfoData'; 6 | 7 | /** 8 | * tutorInfo 엔티티에서 필요한 정보만 가져오는 맵퍼입니다. 9 | * 10 | * @param tutorInfo tutorInfo 엔티티 11 | * @returns ITutorInfoType 12 | * @author joohongpark 13 | */ 14 | export function toTutorInfoType(tutorInfo: TutorInfo): ITutorInfoType { 15 | return { 16 | spec: tutorInfo.spec, 17 | career: tutorInfo.career, 18 | youtube: tutorInfo.youtube, 19 | instagram: tutorInfo.instagram, 20 | soundcloud: tutorInfo.soundcloud, 21 | tutor_introduction: tutorInfo.tutor_introduction, 22 | }; 23 | } 24 | 25 | /** 26 | * 튜터 정보(tutorInfo) 를 변경 혹은 추가할 때 Sequelize가 받아들일 수 있는 타입으로 변경하는 맵퍼입니다. 27 | * 28 | * @param tutorInfoType ITutorInfoType 튜터 정보 29 | * @returns ITutorInfoType tutorInfo 엔티티 30 | * @author joohongpark 31 | */ 32 | export function toTutorInfoEntity( 33 | user_id: number, 34 | tutorInfoType: ITutorInfoType, 35 | ): ITutorInfoInsertType { 36 | return { 37 | user_id, 38 | spec: tutorInfoType.spec || '', 39 | career: tutorInfoType.career || '', 40 | youtube: tutorInfoType.youtube || '', 41 | instagram: tutorInfoType.instagram || '', 42 | soundcloud: tutorInfoType.soundcloud || '', 43 | tutor_introduction: tutorInfoType.soundcloud || '', 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /munetic_app/src/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import styled from 'styled-components'; 3 | import palette from '../../style/palette'; 4 | 5 | const Container = styled.button` 6 | width: 100%; 7 | border: 0; 8 | border-radius: 20px; 9 | background-color: ${palette.grayBlue}; 10 | color: ${palette.green}; 11 | font-size: 20px; 12 | font-weight: bold; 13 | position: relative; 14 | box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px; 15 | cursor: pointer; 16 | ::before { 17 | content: ''; 18 | display: block; 19 | padding-top: 100%; 20 | } 21 | .buttonText { 22 | width: 100%; 23 | margin: 0; 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | } 29 | `; 30 | 31 | interface IProps extends React.ButtonHTMLAttributes { 32 | children: React.ReactNode; 33 | to?: string; 34 | } 35 | 36 | const Button = ({ children, to, ...props }: IProps) => { 37 | const navigate = useNavigate(); 38 | return ( 39 | { 41 | if (to) { 42 | e.preventDefault(); 43 | e.stopPropagation(); 44 | navigate(to); 45 | } 46 | }} 47 | {...props} 48 | > 49 | {children} 50 | 51 | ); 52 | }; 53 | 54 | export default Button; 55 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Lesson/LessonInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | import Title from '../Common/Title'; 3 | import { useInfo } from '../../../contexts/info'; 4 | import { TextFields, TextField, TextField_ } from '../Common/TextFields'; 5 | 6 | export default function LessonInfo() { 7 | const info = useInfo() as any; 8 | const path = useLocation().pathname; 9 | 10 | return ( 11 | <> 12 | 레슨 정보 13 | 14 |

제목

15 |
{info.title}
16 |
17 | 18 | 19 |

카테고리

20 |
{info['Category.name']}
21 |
22 | 23 |

위치

24 |
{info.location}
25 |
26 |
27 | 28 |

가격

29 |
{info.price}
30 |
31 | 32 |

시간(분)

33 |
{info.minute_per_lesson}
34 |
35 | 36 |

생성일

37 |
{info.createdAt}
38 |
39 | 40 |

최근 수정일

41 |
{info.updatedAt}
42 |
43 | 44 |

삭제일

45 |
{info.deletedAt || '없음'}
46 |
47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /munetic_app/src/types/commentData.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 프로퍼티로 전달되는 댓글 배열의 원소 타입을 지정합니다. 3 | * 4 | * @author joohongpark 5 | */ 6 | export interface CommentDataType { 7 | commentListId: number; 8 | nickname: string; 9 | user_id: number; 10 | text: string; 11 | date: string; 12 | stars: number; 13 | accessible: boolean; 14 | modified: boolean; 15 | } 16 | 17 | /** 18 | * 하나의 댓글을 구성하는 컴포넌트의 프로퍼티를 지정합니다. 19 | * 20 | * @author joohongpark 21 | */ 22 | export interface OneCommentPropsType { 23 | comment: CommentDataType; 24 | edit: (commentId: number, stars: number, comment: string) => void; 25 | del: (commentId: number) => void; 26 | } 27 | 28 | /** 29 | * 프로퍼티로 받아야 하는 댓글 배열의 타입을 지정합니다. 30 | * 31 | * @author joohongpark 32 | */ 33 | export interface CommentPropsType { 34 | comments_arr: ReadonlyArray; 35 | edit: (commentId: number, stars: number, comment: string) => void; 36 | del: (commentId: number) => void; 37 | } 38 | 39 | /** 40 | * 댓글 테이블의 데이터 타입을 정의합니다. 41 | * 42 | * @author sungkim 43 | */ 44 | export interface ICommentTable { 45 | id: number; 46 | user_id: number; 47 | lesson_id: number; 48 | content: string; 49 | stars: number; 50 | createdAt: Date; 51 | updatedAt: Date; 52 | deletedAt: Date; 53 | } 54 | 55 | /** 56 | * 댓글 많은 강사의 타입을 지정합니다. 57 | * 58 | * @author joohongpark 59 | */ 60 | export interface ICommentPerTutorTable { 61 | tutor_id: number; 62 | comment_count: number; 63 | } 64 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Inputs/CustomPasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | InputLabel, 4 | Input, 5 | InputAdornment, 6 | IconButton, 7 | } from '@mui/material'; 8 | 9 | import { Visibility, VisibilityOff } from '@mui/icons-material'; 10 | 11 | interface PasswordInput { 12 | width: string; 13 | fontSize: string; 14 | showPassword: boolean; 15 | clickEvent: () => void; 16 | value: string; 17 | onChangeEvent: (event: React.ChangeEvent) => void; 18 | placeholder?: string; 19 | } 20 | 21 | export default function CustomPasswordInput({ 22 | width, 23 | fontSize, 24 | showPassword, 25 | clickEvent, 26 | value, 27 | onChangeEvent, 28 | placeholder, 29 | }: PasswordInput) { 30 | return ( 31 | 32 | 33 | Password 34 | 35 | 44 | 45 | {showPassword ? : } 46 | 47 | 48 | } 49 | /> 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /munetic_app/src/components/media/VideoEmbed.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const VideoWrapper = styled.div` 4 | border-radius: 2px; 5 | padding: 10px; 6 | width: 100%; 7 | height: 100%; 8 | border: 1px solid #ccc; 9 | `; 10 | 11 | const VideoContainer = styled.div` 12 | position: relative; 13 | padding-bottom: 56.25%; 14 | padding-top: 30px; 15 | height: 0; 16 | overflow: hidden; 17 | `; 18 | 19 | const Video = styled.iframe` 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | width: 100%; 24 | height: 100%; 25 | `; 26 | 27 | const Label = styled.div` 28 | font-family: "Roboto","Arial",sans-serif; 29 | font-size: 1.6rem; 30 | line-height: 2.2rem; 31 | font-weight: 400; 32 | margin: 0; 33 | padding: 5px; 34 | border: 0; 35 | background: transparent; 36 | `; 37 | 38 | /** 39 | * VideoEmbed 컴포넌트의 프로퍼티 정의 40 | */ 41 | export interface VideoEmbedIProps { 42 | title?: string; 43 | id: string; 44 | } 45 | 46 | /** 47 | * 유튜브 영상의 고유 ID를 받아 유튜브 임베드를 하는 컴포넌트 48 | * 49 | * @param props.title optional string 비디오 라벨 50 | * @param props.id string 비디오 고유 ID 51 | * @returns 리액트 컴포넌트 52 | */ 53 | export default function VideoEmbed(props: VideoEmbedIProps) { 54 | return ( 55 | 56 | {props.title && } 57 | 58 | 60 | 61 | ); 62 | } -------------------------------------------------------------------------------- /munetic_database/my.cnf: -------------------------------------------------------------------------------- 1 | # The MariaDB configuration file 2 | # 3 | # The MariaDB/MySQL tools read configuration files in the following order: 4 | # 0. "/etc/mysql/my.cnf" symlinks to this file, reason why all the rest is read. 5 | # 1. "/etc/mysql/mariadb.cnf" (this file) to set global defaults, 6 | # 2. "/etc/mysql/conf.d/*.cnf" to set global options. 7 | # 3. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options. 8 | # 4. "~/.my.cnf" to set user-specific options. 9 | # 10 | # If the same option is defined multiple times, the last one will apply. 11 | # 12 | # One can use all long options that the program supports. 13 | # Run program with --help to get a list of available options and with 14 | # --print-defaults to see which it would actually understand and use. 15 | # 16 | # If you are new to MariaDB, check out https://mariadb.com/kb/en/basic-mariadb-articles/ 17 | 18 | # 19 | # This group is read both by the client and the server 20 | # use it for options that affect everything 21 | # 22 | [client-server] 23 | # Port or socket location where to connect 24 | # port = 3306 25 | socket = /run/mysqld/mysqld.sock 26 | 27 | # Import all .cnf files from configuration directory 28 | [mariadbd] 29 | skip-host-cache 30 | skip-name-resolve 31 | 32 | !includedir /etc/mysql/mariadb.conf.d/ 33 | !includedir /etc/mysql/conf.d/ 34 | lower_case_table_names = 1 35 | # For not to discern lower cases and upper cases 36 | 37 | [client] 38 | default-character-set = utf8 39 | 40 | [mysql] 41 | default-character-set = utf8 42 | -------------------------------------------------------------------------------- /munetic_express/src/modules/reshape.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { Account, Gender } from '../models/user'; 3 | 4 | export const userObject = (req: Request) => { 5 | const { 6 | login_id, 7 | login_password, 8 | name, 9 | nickname, 10 | birth, 11 | gender, 12 | type, 13 | email, 14 | phone_number, 15 | } = req.body as { 16 | login_id: string; 17 | login_password: string; 18 | name: string; 19 | nickname: string; 20 | birth: string; 21 | gender: string; 22 | type: string; 23 | email?: string; 24 | phone_number?: string; 25 | }; 26 | const user = { 27 | login_id, 28 | login_password, 29 | name, 30 | birth: new Date(birth), 31 | gender: 32 | gender === 'Male' 33 | ? Gender.Male 34 | : gender === 'Female' 35 | ? Gender.Female 36 | : Gender.Other, 37 | nickname, 38 | type: type === 'Student' ? Account.Student : Account.Tutor, 39 | email, 40 | phone_number, 41 | }; 42 | return user; 43 | }; 44 | 45 | export const adminObject = (req: Request) => { 46 | const { email, name, login_password, type } = req.body as { 47 | email: string; 48 | login_password: string; 49 | name: string; 50 | type: string; 51 | }; 52 | 53 | const admin = { 54 | login_id: email, 55 | login_password, 56 | name, 57 | nickname: email, 58 | birth: new Date(), 59 | gender: Gender.Other, 60 | type: type === 'Admin' ? Account.Admin : Account.Owner, 61 | email, 62 | }; 63 | 64 | return admin; 65 | }; 66 | -------------------------------------------------------------------------------- /munetic_express/src/service/etc.service.ts: -------------------------------------------------------------------------------- 1 | import { Etc, Key } from '../models/etc'; 2 | 3 | /** 4 | * 약관 조회 5 | * 6 | * @returns Promise 7 | * @author joohongpark 8 | */ 9 | export const getTerms = async (): Promise< string > => { 10 | const rtn = await Etc.findOne({ 11 | where: { 12 | id: Key.Terms, 13 | } 14 | }); 15 | return rtn?.content || ''; 16 | }; 17 | 18 | /** 19 | * 오픈소스 라이센스 조회 20 | * 21 | * @returns Promise 22 | * @author joohongpark 23 | */ 24 | export const getLicense = async (): Promise< string > => { 25 | const rtn = await Etc.findOne({ 26 | where: { 27 | id: Key.License, 28 | } 29 | }); 30 | return rtn?.content || ''; 31 | }; 32 | 33 | 34 | /** 35 | * 약관 수정 36 | * 37 | * @param content 수정하고자 하는 정보 38 | * @returns Promise 39 | * @author joohongpark 40 | */ 41 | export const editTerms = async ( 42 | content: string, 43 | ): Promise => { 44 | const [updatedNum] = await Etc.update( 45 | { content }, 46 | { 47 | where: { 48 | id: Key.Terms, 49 | }, 50 | }, 51 | ); 52 | return updatedNum > 0; 53 | }; 54 | 55 | 56 | 57 | /** 58 | * 오픈소스 라이센스 수정 59 | * 60 | * @param content 수정하고자 하는 정보 61 | * @returns Promise 62 | * @author joohongpark 63 | */ 64 | export const editLicense = async ( 65 | content: string, 66 | ): Promise => { 67 | const [updatedNum] = await Etc.update( 68 | { content }, 69 | { 70 | where: { 71 | id: Key.License, 72 | }, 73 | }, 74 | ); 75 | return updatedNum > 0; 76 | }; 77 | -------------------------------------------------------------------------------- /munetic_express/src/tests/db/lessonseeds.ts: -------------------------------------------------------------------------------- 1 | import { Gender } from '../../models/user'; 2 | 3 | export const user1 = { 4 | nickname: 'kunlee', 5 | name: '쿠운리', 6 | gender: Gender.Male, 7 | name_public: true, 8 | phone_number: '010-1234-1234', 9 | birth: '1994-03-02', 10 | image_url: '../../munetic_app/public/img/testImg.png', 11 | }; 12 | 13 | export const user2 = { 14 | nickname: 'jolim', 15 | name: '조올림', 16 | gender: Gender.Male, 17 | name_public: true, 18 | phone_number: '010-5678-5678', 19 | birth: '1998-02-20', 20 | image_url: '../../munetic_app/public/img/testImg.png', 21 | }; 22 | 23 | export const lesson1 = { 24 | lesson_id: 1, 25 | tutor_id: 2, 26 | title: '지금까지 이런 기타 레슨은 없었다.', 27 | price: 100000, 28 | location: '서울시', 29 | minute_per_lesson: 60, 30 | content: '더 이상 설명이 필요 없습니다. 믿고 따라오세요.', 31 | Category: { 32 | name: '기타', 33 | }, 34 | User: user2, 35 | }; 36 | 37 | export const lesson2 = { 38 | lesson_id: 2, 39 | tutor_id: 2, 40 | title: '기타만 잘 치는 줄 아셨죠? 바이올린도 합니다.', 41 | price: 200000, 42 | location: '서울시', 43 | minute_per_lesson: 80, 44 | content: 45 | '헨리도 저한테 바이올린 배웠습니다. 더 이상 설명이 필요 없습니다. 믿고 따라오세요.', 46 | Category: { 47 | name: '바이올린', 48 | }, 49 | User: user2, 50 | }; 51 | 52 | export const lesson3 = { 53 | lesson_id: 3, 54 | tutor_id: 2, 55 | title: '죄송합니다. 드럼도 가르쳐드립니다.', 56 | price: 50000, 57 | location: '경기도', 58 | minute_per_lesson: 40, 59 | content: 60 | '드럼드럼드럼드럼드럼드럼 더 이상 설명이 필요 없습니다. 믿고 따라오세요.', 61 | Category: { 62 | name: '드럼', 63 | }, 64 | User: user2, 65 | }; 66 | -------------------------------------------------------------------------------- /munetic_express/src/models/category.ts: -------------------------------------------------------------------------------- 1 | import { sequelize } from './index'; 2 | import { Sequelize, DataTypes, Model, Optional } from 'sequelize'; 3 | 4 | export interface categoryAttributes { 5 | id: number; 6 | name: string; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | deletedAt: Date; 10 | } 11 | 12 | type categoryCreationAttributes = Optional< 13 | categoryAttributes, 14 | 'id' | 'createdAt' | 'updatedAt' | 'deletedAt' 15 | >; 16 | 17 | export class Category 18 | extends Model 19 | implements categoryAttributes 20 | { 21 | public id!: number; 22 | public name!: string; 23 | public readonly createdAt!: Date; 24 | public readonly updatedAt!: Date; 25 | public readonly deletedAt!: Date; 26 | 27 | static initModel(sequelize: Sequelize): typeof Category { 28 | return Category.init( 29 | { 30 | id: { 31 | allowNull: false, 32 | autoIncrement: true, 33 | type: DataTypes.INTEGER, 34 | primaryKey: true, 35 | }, 36 | name: { 37 | allowNull: false, 38 | type: DataTypes.STRING, 39 | unique: true, 40 | }, 41 | createdAt: { 42 | field: 'createdAt', 43 | type: DataTypes.DATE, 44 | }, 45 | updatedAt: { 46 | field: 'updatedAt', 47 | type: DataTypes.DATE, 48 | }, 49 | deletedAt: { 50 | field: 'deletedAt', 51 | type: DataTypes.DATE, 52 | }, 53 | }, 54 | { tableName: 'Category', sequelize }, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/User/UserPosts.tsx: -------------------------------------------------------------------------------- 1 | import MUITable from '../../Table/MUITable'; 2 | import { useEffect, useState } from 'react'; 3 | import { useInfo } from '../../../contexts/info'; 4 | import * as Api from '../../../lib/api'; 5 | 6 | export default function UserPosts() { 7 | const info = useInfo() as any; 8 | const [page, setPage] = useState(0); 9 | const [rowsPerPage, setRowsPerPage] = useState(5); 10 | const [rows, setRows] = useState<[]>([]); 11 | const [count, setCount] = useState(0); 12 | 13 | /** 14 | * Page 전환 15 | */ 16 | const handleChangePage = ( 17 | event: React.MouseEvent | null, 18 | newPage: number, 19 | ) => { 20 | setPage(newPage); 21 | }; 22 | 23 | /** 24 | * 한 페이지에 노출하는 row수 25 | */ 26 | const handleChangeRowsPerPage = ( 27 | event: React.ChangeEvent, 28 | ) => { 29 | setRowsPerPage(parseInt(event.target.value, 10)); 30 | setPage(0); 31 | }; 32 | 33 | useEffect(() => { 34 | const limit = 5; 35 | const offset = page * limit; 36 | Api.getUserLessons(info!.id, offset, limit).then(({ data }: any) => { 37 | setRows(data.data.rows); 38 | setCount(parseInt(data.data.count, 10)); 39 | }); 40 | }, [page]); 41 | 42 | return ( 43 | <> 44 | {}} 50 | handleChangePage={handleChangePage} 51 | handleChangeRowsPerPage={handleChangeRowsPerPage} 52 | /> 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /munetic_app/src/components/common/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import ReactPaginate from 'react-paginate'; 2 | import styled from 'styled-components'; 3 | import palette from '../../style/palette'; 4 | 5 | const PaginationContainer = styled.div` 6 | .container { 7 | padding: 0; 8 | margin: 0; 9 | margin-bottom: 20px; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .container li + li { 15 | margin-left: 3px; 16 | } 17 | 18 | .container li { 19 | line-height: normal; 20 | } 21 | 22 | .container a { 23 | padding: 5px 10px; 24 | display: flex; 25 | line-height: 1; 26 | } 27 | 28 | .currentPage { 29 | background-color: ${palette.lightBlue}; 30 | } 31 | .disabledLink { 32 | color: #bfbfbf; 33 | } 34 | `; 35 | 36 | interface IProps { 37 | itemsPerPage: number; 38 | classCount: number; 39 | handlePageClick: (e: any) => void; 40 | } 41 | 42 | const Pagination = ({ itemsPerPage, classCount, handlePageClick }: IProps) => { 43 | const pageCount = Math.ceil(classCount / itemsPerPage); 44 | 45 | return ( 46 | 47 | {pageCount > 1 && ( 48 | 59 | )} 60 | 61 | ); 62 | }; 63 | 64 | export default Pagination; 65 | -------------------------------------------------------------------------------- /munetic_express/README.md: -------------------------------------------------------------------------------- 1 | ## 백앤드 (munetic_express) 2 | 3 | ### build & run 4 | - TypeSctipt 컴파일러로 빌드를 한 후 node.js로 빌드된 결과물을 실행합니다. 5 | - 관련 설정은 tsconfig.json에 정의되어 있습니다. 6 | - tsc 명령으로 빌드가 되어 dist 폴더에 생성됩니다. 7 | - node server.js 형태로 구동됩니다. 8 | ### 디렉토리 구조 9 | - /dist (빌드 시 생성) → 타입스크립트 컴파일러가 빌드한 결과물이 생성됩니다. 10 | - /src 11 | - @types → express의 미들웨어에 한해 사용됩니다. 12 | - /config → 환경 변수로부터 백앤드 앱이 사용하는 설정을 가져옵니다. 13 | - /controllers → MVC 패턴의 Controller이며 비즈니스 로직의 연결을 담당합니다. 14 | - 컨트롤러는 express의 미들웨어 함수로 정의합니다. 15 | - 컨트롤러를 담당하는 파일들은 [기능명].controller.ts로 명명됩니다. 16 | - /data (현재 삭제) → 앱 초기 실행 시 데이터베이스에 들어가 있어야 할 항목들을 정의합니다. 17 | - /models → MVC 패턴의 Model이자 sequelize의 RDB 객체를 정의합니다. 18 | - 내부 파일들은 [테이블명].ts 들과 index.ts가 있습니다. 19 | - [테이블명].ts들은 테이블을 객체로 정의합니다. 20 | - index.ts는 RDB의 기본 설정과 테이블 간 관계 설정 등을 정의합니다. 21 | - /modules → 여러 모듈들이 위치합니다. 22 | - 현재는 jwt strategy와 에러 핸들러가 정의되어 있습니다. 23 | - /routes → express의 라우팅을 정의합니다. 24 | - 라우팅은 URI 및 HTTP 메소드로 구성되는 특정 엔드포인트에 대한 요청에 응답하는 방법을 정의하는 것을 의미합니다. 25 | - 내부 파일들은 [라우트 상위 경로명].routes.ts 들과 index.ts가 있습니다. 26 | - [라우트 상위 경로명].routes.ts 파일들은 요청에 따른 컨트롤러를 연결합니다. 27 | - index.ts는 jwt 미들웨어 및 라우트를 express에 적용합니다. 28 | - /seeders → sequelize cli로 RDB에 시드 데이터를 삽입할 때 사용합니다. 29 | - 시드 데이터를 저장하고 있으며 테스트 시에만 사용됩니다. 30 | - express 구동과 직접적인 연관은 없습니다. 31 | - /service → 데이터 유효성, 트랜잭션 처리 등 RDB와 직접 상호작용하는 로직입니다. 32 | - 컨트롤러 모듈에서 서비스를 호출하는 식으로 사용됩니다. 33 | - /swagger → Swagger API 관련 yaml 파일, 설정 파일이 있습니다. 34 | - /tests → jest 테스트 파일들이 있습니다. 35 | - /mapping → 엔티티와 DTO 간 매핑을 해주는 함수들이 있습니다. 36 | - /types → 매핑 관련 타입들을 정의합니다. -------------------------------------------------------------------------------- /munetic_express/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | export const router = Router(); 3 | 4 | import * as auth from './auth.routes'; 5 | import * as user from './user.routes'; 6 | import * as lesson from './lesson.routes'; 7 | import * as category from './category.routes'; 8 | import * as admin from './admin/admin.routes'; 9 | import * as bookmark from './bookmark.routes'; 10 | import * as comment from './comment.routes'; 11 | import * as search from './search.routes'; 12 | import * as lessonLike from './lessonLike.routes'; 13 | import * as etc from './etc.routes'; 14 | 15 | import passport from 'passport'; 16 | import AdminStrategy from '../modules/admin.strategy'; 17 | import { 18 | JwtAdminAccessStrategy, 19 | JwtAdminRefreshStrategy, 20 | } from '../modules/jwt.admin.strategy'; 21 | 22 | import LocalStrategy from '../modules/local.strategy'; 23 | import { 24 | JwtAccessStrategy, 25 | JwtRefreshStrategy, 26 | } from '../modules/jwt.local.strategy'; 27 | 28 | passport.use('local', LocalStrategy()); 29 | passport.use('jwt', JwtAccessStrategy()); 30 | passport.use('jwtRefresh', JwtRefreshStrategy()); 31 | 32 | passport.use('admin', AdminStrategy()); 33 | passport.use('jwt-admin', JwtAdminAccessStrategy()); 34 | passport.use('jwtRefresh-admin', JwtAdminRefreshStrategy()); 35 | 36 | router.use(auth.path, auth.router); 37 | router.use(user.path, user.router); 38 | router.use(lesson.path, lesson.router); 39 | router.use(admin.path, admin.router); 40 | router.use(category.path, category.router); 41 | router.use(bookmark.path, bookmark.router); 42 | router.use(comment.path, comment.router); 43 | router.use(search.path, search.router); 44 | router.use(lessonLike.path, lessonLike.router); 45 | router.use(etc.path, etc.router); 46 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Lesson/WriterInfo.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { useInfo } from '../../../contexts/info'; 3 | import Button from '../../Button'; 4 | import Title from '../Common/Title'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | export default function WriterInfo() { 8 | const info = useInfo() as any; 9 | const navigate = useNavigate(); 10 | 11 | const MoveToUserProfile = () => { 12 | navigate(`/users/${info.tutor_id}`); 13 | }; 14 | 15 | return ( 16 | <> 17 | 작성자 18 | 19 | {info['User.nickname']} 20 | {info['User.login_id']} 21 | 유저 프로필 보기 22 | 23 | ); 24 | } 25 | 26 | const UserImage = styled.div<{ url: string }>` 27 | background-color: rgb(149, 167, 255); 28 | background-image: ${props => `url(${props.url})`}; 29 | background-size: cover; 30 | background-position: center; 31 | width: 11rem; 32 | height: 11rem; 33 | margin: 1rem auto 1.5rem auto; 34 | overflow: hidden; 35 | border-radius: 50%; 36 | `; 37 | 38 | const UserNickname = styled.p` 39 | font-size: 1.5rem; 40 | text-align: center; 41 | margin-bottom: 0.7rem; 42 | color: #616060; 43 | `; 44 | 45 | const UserId = styled.p` 46 | font-size: 1.8rem; 47 | text-align: center; 48 | margin-bottom: 2rem; 49 | `; 50 | 51 | const CustomButton = styled(Button)` 52 | width: 15rem; 53 | background-color: rgb(82, 111, 255); 54 | display: block; 55 | margin: 0 auto; 56 | ${props => 57 | props.disabled && 58 | css` 59 | background-color: rgb(140, 140, 140); 60 | `} 61 | `; 62 | -------------------------------------------------------------------------------- /munetic_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "munetic_app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "npx vite", 6 | "build": "tsc && npx vite build", 7 | "test": "vite-jest", 8 | "dev": "tsc && npx vite build && vite serve", 9 | "serve": "tsc && npx vite build && serve dist -s -l 2424" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.7.0", 13 | "@emotion/styled": "^11.6.0", 14 | "@mui/icons-material": "^5.2.1", 15 | "@mui/material": "^5.2.3", 16 | "@mui/styled-engine": "^5.2.0", 17 | "@mui/styled-engine-sc": "^5.1.0", 18 | "@types/styled-components": "^5.1.17", 19 | "react": "^17.0.0", 20 | "react-dom": "^17.0.0", 21 | "react-paginate": "^8.1.0", 22 | "react-router-dom": "^6.0.2", 23 | "serve": "^13.0.2", 24 | "styled-components": "^5.3.3", 25 | "styled-reset": "^4.3.4" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/jest-dom": "^5.16.1", 29 | "@testing-library/react": "^12.1.2", 30 | "@testing-library/user-event": "^13.5.0", 31 | "@types/react": "^17.0.0", 32 | "@types/react-dom": "^17.0.0", 33 | "@typescript-eslint/eslint-plugin": "^5.6.0", 34 | "@typescript-eslint/parser": "^5.6.0", 35 | "@vitejs/plugin-react": "^1.0.0", 36 | "axios": "^0.24.0", 37 | "eslint": "^8.4.1", 38 | "eslint-config-airbnb": "^19.0.2", 39 | "eslint-config-prettier": "^8.3.0", 40 | "eslint-plugin-import": "^2.25.3", 41 | "eslint-plugin-jsx-a11y": "^6.5.1", 42 | "eslint-plugin-prettier": "^4.0.0", 43 | "eslint-plugin-react": "^7.27.1", 44 | "eslint-plugin-react-hooks": "^4.3.0", 45 | "jest": "^27.4.4", 46 | "jest-environment-jsdom": "^27.4.4", 47 | "typescript": "^4.3.2", 48 | "vite": "^2.6.4", 49 | "vite-jest": "^0.1.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /munetic_express/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cookieParser from 'cookie-parser'; 3 | import cors from 'cors'; 4 | import { options } from './swagger/swagger'; 5 | import swaggerUi from 'swagger-ui-express'; 6 | import swaggerJSDoc from 'swagger-jsdoc'; 7 | import { router } from './routes'; 8 | import { Models } from './models'; 9 | import errorHandler from './modules/errorHandler'; 10 | import passport from 'passport'; 11 | import UserInit from './models/initdata/user.init' 12 | import CategoryInit from './models/initdata/category.init' 13 | import EtcInit from './models/initdata/etc.init' 14 | 15 | const app: express.Application = express(); 16 | 17 | app.use(express.json()); 18 | app.use( 19 | cors({ 20 | origin: ['http://localhost:2424', 'http://localhost:4242'], 21 | credentials: true, 22 | exposedHeaders: 'Authorization', 23 | }), 24 | ); 25 | app.use(cookieParser()); 26 | app.use(passport.initialize()); 27 | app.use('/api', router); 28 | 29 | /** 30 | * Swagger 연결 31 | */ 32 | const specs = swaggerJSDoc(options); 33 | app.use( 34 | '/api/swagger', 35 | swaggerUi.serve, 36 | swaggerUi.setup(specs, { explorer: true }), 37 | ); 38 | 39 | /** 40 | * MariaDB 테이블 연결 41 | */ 42 | const init: boolean = true; // express가 재시작 될 때 데이터베이스 초기화를 할 지의 여부 43 | Models() 44 | .sync({ force: init }) 45 | .then(() => { 46 | app.emit('dbconnected'); 47 | console.log('👍 Modeling Successed'); 48 | 49 | if (init) { 50 | // admin Owner 계정 자동 생성 51 | UserInit(); 52 | // app category 자동 생성 53 | CategoryInit(); 54 | // 약관, 라이센스 자동 생성 55 | EtcInit(); 56 | } 57 | }) 58 | .catch(err => console.log(err, '🙀 Modeling Failed')); 59 | 60 | 61 | /** 62 | * 에러 핸들링 63 | */ 64 | app.use(errorHandler); 65 | export default app; 66 | -------------------------------------------------------------------------------- /munetic_express/src/models/etc.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes, Model, Optional } from 'sequelize'; 2 | 3 | export enum Key { 4 | Terms = 1, 5 | License = 2, 6 | } 7 | 8 | /** 9 | * Etc (약관, 라이센스 등이 저장되는 테이블) 테이블의 어트리뷰트들을 명시합니다. 10 | * 11 | * @Author joohongpark 12 | */ 13 | export interface etcAttributes { 14 | id: number; 15 | content: string; 16 | createdAt: Date; 17 | updatedAt: Date; 18 | deletedAt: Date; 19 | } 20 | 21 | /** 22 | * Etc 테이블에 값을 삽입할 때 (자동 생성되어서) 생략해도 되는 데이터를 명시합니다. 23 | * 24 | * @Author joohongpark 25 | */ 26 | type etcCreationAttributes = Optional; 29 | 30 | /** 31 | * Etc 데이터 모델(및 테이블)을 정의합니다. 32 | * 33 | * @Author joohongpark 34 | */ 35 | export class Etc 36 | extends Model 37 | implements etcAttributes 38 | { 39 | public id!: number; 40 | public content!: string; 41 | public readonly createdAt!: Date; 42 | public readonly updatedAt!: Date; 43 | public readonly deletedAt!: Date; 44 | 45 | static initModel(sequelize: Sequelize): typeof Etc { 46 | return Etc.init( 47 | { 48 | id: { 49 | allowNull: false, 50 | type: DataTypes.INTEGER, 51 | primaryKey: true, 52 | }, 53 | content: { 54 | allowNull: true, 55 | type: DataTypes.TEXT('medium'), 56 | }, 57 | createdAt: { 58 | field: 'createdAt', 59 | type: DataTypes.DATE, 60 | }, 61 | updatedAt: { 62 | field: 'updatedAt', 63 | type: DataTypes.DATE, 64 | }, 65 | deletedAt: { 66 | field: 'deletedAt', 67 | type: DataTypes.DATE, 68 | }, 69 | }, 70 | { tableName: 'Etc', sequelize }, 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /munetic_express/src/modules/jwt.local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { Op } from 'sequelize'; 3 | import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt'; 4 | import * as UserService from '../service/user.service'; 5 | import passport from 'passport'; 6 | 7 | const { development } = require('../config/config'); 8 | const { access_secret, refresh_secret } = development; 9 | 10 | export const accessOpts: StrategyOptions = { 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: access_secret, 14 | }; 15 | 16 | export const cookieExtractor = function (req: Request) { 17 | var token = null; 18 | if (req && req.cookies) { 19 | token = req.cookies['refreshToken']; 20 | } 21 | return token; 22 | }; 23 | 24 | export const refreshOpts: StrategyOptions = { 25 | jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor]), 26 | ignoreExpiration: false, 27 | secretOrKey: refresh_secret, 28 | }; 29 | 30 | const JwtStrategyCallback = async ( 31 | jwt_payload: { sub: any; login_id: any }, 32 | done: any, 33 | ) => { 34 | const [user] = await UserService.searchActiveUser({ 35 | login_id: jwt_payload.login_id, 36 | type: { 37 | [Op.or]: ['Tutor', 'Student'], 38 | }, 39 | }); 40 | if (user) { 41 | return done(null, user.toJSON()); 42 | } else { 43 | return done(null, false); 44 | } 45 | }; 46 | 47 | export const JwtAccessStrategy = () => 48 | new Strategy(accessOpts, JwtStrategyCallback); 49 | 50 | export const JwtRefreshStrategy = () => 51 | new Strategy(refreshOpts, JwtStrategyCallback); 52 | 53 | export const jwtAuth = () => passport.authenticate('jwt', { session: false }); 54 | 55 | export const jwtReAuth = () => 56 | passport.authenticate('jwtRefresh', { session: false }); 57 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/UserListPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import MUITable from '../components/Table/MUITable'; 3 | import * as Api from '../lib/api'; 4 | 5 | function delUsers(arr: ReadonlyArray) { 6 | Api.deleteUsers(arr, false) 7 | .then(res => console.log(`${res.data.data}명의 유저 삭제`)) 8 | .catch(e => console.log(`err : ${e}`)); 9 | } 10 | 11 | export default function UserListPage() { 12 | const [page, setPage] = useState(0); 13 | const [rowsPerPage, setRowsPerPage] = useState(10); 14 | const [rows, setRows] = useState<[]>([]); 15 | const [count, setCount] = useState(0); 16 | 17 | /** 18 | * Page 전환 19 | */ 20 | const handleChangePage = ( 21 | event: React.MouseEvent | null, 22 | newPage: number, 23 | ) => { 24 | setPage(newPage); 25 | }; 26 | 27 | /** 28 | * 한 페이지에 노출하는 row수 29 | */ 30 | const handleChangeRowsPerPage = ( 31 | event: React.ChangeEvent, 32 | ) => { 33 | setRowsPerPage(parseInt(event.target.value, 10)); 34 | setPage(0); 35 | }; 36 | 37 | const getUsers = () => { 38 | // FIXME: Offset 추가해야 할듯? 39 | Api.getAppUserList(page).then(({ data }: any) => { 40 | setRows(data.data.rows); 41 | setCount(parseInt(data.data.count, 10)); 42 | }); 43 | } 44 | 45 | useEffect(() => { 46 | getUsers(); 47 | }, [page]); 48 | 49 | return ( 50 | <> 51 | { 57 | delUsers(arr); 58 | getUsers(); 59 | }} 60 | handleChangePage={handleChangePage} 61 | handleChangeRowsPerPage={handleChangeRowsPerPage} 62 | /> 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/LessonListPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import MUITable from '../components/Table/MUITable'; 3 | import * as Api from '../lib/api'; 4 | 5 | function delLessons(arr: ReadonlyArray) { 6 | Api.deleteLessons(arr, false) 7 | .then(res => console.log(`${res.data.data}개의 레슨 삭제`)) 8 | .catch(e => console.log(`err : ${e}`)); 9 | } 10 | 11 | export default function LessonListPage() { 12 | const [page, setPage] = useState(0); 13 | const [rowsPerPage, setRowsPerPage] = useState(10); 14 | const [rows, setRows] = useState<[]>([]); 15 | const [count, setCount] = useState(0); 16 | 17 | /** 18 | * Page 전환 19 | */ 20 | const handleChangePage = ( 21 | event: React.MouseEvent | null, 22 | newPage: number, 23 | ) => { 24 | setPage(newPage); 25 | }; 26 | 27 | /** 28 | * 한 페이지에 노출하는 row수 29 | */ 30 | const handleChangeRowsPerPage = ( 31 | event: React.ChangeEvent, 32 | ) => { 33 | setRowsPerPage(parseInt(event.target.value, 10)); 34 | setPage(0); 35 | }; 36 | 37 | const getLessons = () => { 38 | const limit = 10; 39 | const offset = page * limit; 40 | Api.getAllLessons(offset, limit).then(({ data }: any) => { 41 | setRows(data.data.rows); 42 | setCount(parseInt(data.data.count, 10)); 43 | }); 44 | } 45 | 46 | useEffect(() => { 47 | getLessons(); 48 | }, [page]); 49 | 50 | return ( 51 | { 57 | delLessons(arr); 58 | getLessons(); 59 | }} 60 | handleChangePage={handleChangePage} 61 | handleChangeRowsPerPage={handleChangeRowsPerPage} 62 | /> 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/CommentListPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import MUITable from '../components/Table/MUITable'; 3 | import * as Api from '../lib/api'; 4 | 5 | function delComments(arr: ReadonlyArray) { 6 | Api.deleteComments(arr, false) 7 | .then(res => console.log(`${res.data.data}개의 댓글 삭제`)) 8 | .catch(e => console.log(`err : ${e}`)); 9 | } 10 | 11 | export default function CommentListPage() { 12 | const [page, setPage] = useState(0); 13 | const [rowsPerPage, setRowsPerPage] = useState(10); 14 | const [rows, setRows] = useState<[]>([]); 15 | const [count, setCount] = useState(0); 16 | 17 | /** 18 | * Page 전환 19 | */ 20 | const handleChangePage = ( 21 | event: React.MouseEvent | null, 22 | newPage: number, 23 | ) => { 24 | setPage(newPage); 25 | }; 26 | 27 | /** 28 | * 한 페이지에 노출하는 row수 29 | */ 30 | const handleChangeRowsPerPage = ( 31 | event: React.ChangeEvent, 32 | ) => { 33 | setRowsPerPage(parseInt(event.target.value, 10)); 34 | setPage(0); 35 | }; 36 | 37 | const getComments = () => { 38 | const limit = 10; 39 | const offset = page * limit; 40 | Api.getAllComments(offset, limit).then(({ data }: any) => { 41 | setRows(data.data.rows); 42 | setCount(parseInt(data.data.count, 10)); 43 | }); 44 | } 45 | 46 | useEffect(() => { 47 | getComments(); 48 | }, [page]); 49 | 50 | return ( 51 | { 57 | delComments(arr); 58 | getComments(); 59 | }} 60 | handleChangePage={handleChangePage} 61 | handleChangeRowsPerPage={handleChangeRowsPerPage} 62 | /> 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /munetic_express/src/modules/jwt.ts: -------------------------------------------------------------------------------- 1 | import * as Status from 'http-status'; 2 | import { User } from '../models/user'; 3 | import jwt, { TokenExpiredError } from 'jsonwebtoken'; 4 | import ErrorResponse from './errorResponse'; 5 | import * as UserService from '../service/user.service'; 6 | import { Request } from 'express'; 7 | 8 | const { development } = require('../config/config'); 9 | const { access_secret, refresh_secret, domain } = development; 10 | 11 | export const accessToken = async (user: User | Request['user']) => { 12 | const payload = { 13 | sub: user!.id, 14 | login_id: user!.login_id, 15 | }; 16 | const token = await jwt.sign(payload, access_secret, { expiresIn: '24h' }); 17 | return token; 18 | }; 19 | 20 | export const refreshToken = async (user: User | Request['user']) => { 21 | const payload = { 22 | id: user!.id, 23 | login_id: user!.login_id, 24 | }; 25 | const token = await jwt.sign(payload, refresh_secret, { expiresIn: '7d' }); 26 | const decoded = jwt.decode(token) as any; 27 | const cookieOptions = { 28 | domain: `${domain}`, 29 | path: '/', 30 | expires: new Date(decoded.exp * 1000), 31 | sameSite: 'strict' as 'strict', 32 | httpOnly: true, 33 | // secure: true, //https 환경에서 on합니다. 34 | }; 35 | return { token, cookieOptions }; 36 | }; 37 | 38 | export const checkRefreshToken = async (refreshToken: string, next: any) => { 39 | try { 40 | const decoded = (await jwt.verify(refreshToken, refresh_secret)) as { 41 | login_id: string; 42 | }; 43 | const [userInfo] = await UserService.searchActiveUser({ 44 | login_id: decoded.login_id, 45 | }); 46 | return userInfo.toJSON(); 47 | } catch (err) { 48 | if (err instanceof TokenExpiredError) 49 | throw new ErrorResponse(Status.BAD_REQUEST, err.message); 50 | next(err); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /munetic_express/src/service/tutorInfo.service.ts: -------------------------------------------------------------------------------- 1 | import Status from 'http-status'; 2 | import * as TutorInfoMapper from '../mapping/TutorInfoMapper'; 3 | import { TutorInfo } from '../models/tutorInfo' 4 | import { ITutorInfoInsertType, ITutorInfoType } from '../types/controller/tutorInfoData'; 5 | import ErrorResponse from '../modules/errorResponse'; 6 | 7 | /** 8 | * 튜터의 추가 데이터를 새로 추가하거나 업데이트 9 | * 10 | * @param user_id user ID 11 | * @param tutor_info ITutorInfoType 12 | * @returns Promise 13 | * @throws ErrorResponse if the user ID or lesson ID is not exists. 14 | * @author joohongpark 15 | */ 16 | export const addTutorDataById = async ( 17 | user_id: number, 18 | tutor_info: ITutorInfoType, 19 | ): Promise< boolean > => { 20 | let rtn: boolean = false; 21 | const data: ITutorInfoInsertType = TutorInfoMapper.toTutorInfoEntity(user_id, tutor_info); 22 | try { 23 | const newTutorData: [TutorInfo, boolean] = await TutorInfo.findOrCreate({ 24 | where: { user_id }, 25 | defaults: data, 26 | }); 27 | const check = await newTutorData[0].update({ ...tutor_info }); 28 | rtn = check !== null; 29 | } catch (e) { 30 | throw new ErrorResponse(Status.BAD_REQUEST, '유효하지 않은 강의 id입니다.'); 31 | } 32 | return rtn; 33 | } 34 | 35 | /** 36 | * 튜터의 추가 데이터를 조회 37 | * 38 | * @param user_id user ID 39 | * @returns Promise 없으면 Null을 반환 40 | * @author joohongpark 41 | */ 42 | export const getTutorDataById = async ( 43 | user_id: number, 44 | ): Promise< ITutorInfoType | undefined > => { 45 | try { 46 | const tutorInfo: TutorInfo | null = await TutorInfo.findOne({ 47 | where: { user_id }, 48 | }); 49 | if (tutorInfo) { 50 | return TutorInfoMapper.toTutorInfoType(tutorInfo); 51 | } else { 52 | return undefined; 53 | } 54 | } catch (e) { 55 | return undefined; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/EditTermsPage.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useState, useEffect } from 'react'; 3 | import { TextField, Button } from '@mui/material'; 4 | import Item from '../components/Info/Common/Item'; 5 | import * as Api from '../lib/api'; 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | width: 100%; 10 | background-color: white; 11 | border-radius: 0.3rem; 12 | margin-bottom: 1rem; 13 | `; 14 | 15 | const Label = styled.div` 16 | font-family: "Roboto","Arial",sans-serif; 17 | font-size: 2.0rem; 18 | line-height: 2.2rem; 19 | font-weight: 400; 20 | width: 100%; 21 | margin: 6px; 22 | padding: 0; 23 | border: 0; 24 | background: transparent; 25 | `; 26 | 27 | export default function EditTermsPage() { 28 | const [text, setText] = useState(""); 29 | 30 | const getTerms = () => { 31 | Api.getTerms() 32 | .then(({ data }: any) => { 33 | setText(data.data); 34 | }) 35 | .catch(err => { 36 | console.log(err); 37 | }); 38 | } 39 | 40 | const saveTerms = () => { 41 | Api.saveTerms({data: text}) 42 | .then(({ data }: any) => { 43 | alert("저장하였습니다."); 44 | }) 45 | .catch(err => { 46 | console.log(err); 47 | }); 48 | } 49 | 50 | useEffect(() => { 51 | getTerms(); 52 | }, []); 53 | 54 | return ( 55 | 56 | 57 | 58 | 64 | 65 | setText(e.target.value)} 71 | fullWidth 72 | /> 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/EditLicensePage.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useState, useEffect } from 'react'; 3 | import { TextField, Button } from '@mui/material'; 4 | import Item from '../components/Info/Common/Item'; 5 | import * as Api from '../lib/api'; 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | width: 100%; 10 | background-color: white; 11 | border-radius: 0.3rem; 12 | margin-bottom: 1rem; 13 | `; 14 | 15 | const Label = styled.div` 16 | font-family: "Roboto","Arial",sans-serif; 17 | font-size: 2.0rem; 18 | line-height: 2.2rem; 19 | font-weight: 400; 20 | width: 100%; 21 | margin: 6px; 22 | padding: 0; 23 | border: 0; 24 | background: transparent; 25 | `; 26 | 27 | export default function EditLicensePage() { 28 | const [text, setText] = useState(""); 29 | 30 | const getLicense = () => { 31 | Api.getLicense() 32 | .then(({ data }: any) => { 33 | setText(data.data); 34 | }) 35 | .catch(err => { 36 | console.log(err); 37 | }); 38 | } 39 | 40 | const saveLicense = () => { 41 | Api.saveLicense({data: text}) 42 | .then(({ data }: any) => { 43 | alert("저장하였습니다."); 44 | }) 45 | .catch(err => { 46 | console.log(err); 47 | }); 48 | } 49 | 50 | useEffect(() => { 51 | getLicense(); 52 | }, []); 53 | 54 | return ( 55 | 56 | 57 | 58 | 64 | 65 | setText(e.target.value)} 71 | fullWidth 72 | /> 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/Lesson/LessonGrid.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { Grid } from '@mui/material'; 3 | import { useInfo } from '../../../contexts/info'; 4 | import Item from '../Common/Item'; 5 | import LessonContent from './LessonContent'; 6 | import LessonInfo from './LessonInfo'; 7 | import WriterInfo from './WriterInfo'; 8 | import Button from '../../Button'; 9 | import * as Api from '../../../lib/api'; 10 | import { useNavigate } from 'react-router-dom'; 11 | 12 | export default function LessonGrid() { 13 | const info = useInfo() as any; 14 | const navigate = useNavigate(); 15 | 16 | const deleteLesson = () => { 17 | if (window.confirm(`이 게시물을 삭제하시겠습니까?`)) { 18 | Api.deleteLesson(info.id) 19 | .then(() => { 20 | alert('삭제되었습니다.'); 21 | navigate(0); 22 | }) 23 | .catch(err => alert(err.response.data)); 24 | } 25 | }; 26 | 27 | return ( 28 | <> 29 | 30 | 게시물 삭제 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | const CustomButton = styled(Button)` 54 | width: 10rem; 55 | border-radius: 0.5rem; 56 | background-color: rgb(82, 111, 255); 57 | display: block; 58 | margin-top: 1rem; 59 | margin-bottom: 1.5rem; 60 | ${props => 61 | props.disabled && 62 | css` 63 | background-color: rgb(140, 140, 140); 64 | `} 65 | `; 66 | -------------------------------------------------------------------------------- /munetic_app/src/lib/api/comment.ts: -------------------------------------------------------------------------------- 1 | import client from './client'; 2 | 3 | /** 4 | * 강의에 대한 댓글들을 받아옵니다. 5 | * 6 | * @param lesson_id 강의 ID 7 | * @returns Axios response 8 | * @author joohongpark 9 | */ 10 | export const getCommentByLesson = async (lesson_id: number) => { 11 | return await client.get(`/comment/lesson/${lesson_id}`) 12 | } 13 | 14 | /** 15 | * 유저에 대한 댓글들을 받아옵니다. 16 | * 17 | * @param user_id 유저 로그인 ID 18 | * @returns Axios response 19 | * @author joohongpark 20 | */ 21 | export const getCommentByUser = async (user_id: string) => { 22 | return await client.get(`/comment/user/${user_id}`) 23 | } 24 | 25 | /** 26 | * 강의에 대해 댓글을 추가합니다. 27 | * 28 | * @param lesson_id 강의 ID 29 | * @param comment 댓글 내용 30 | * @param stars 별 개수 31 | * @returns Axios response 32 | * @author joohongpark 33 | */ 34 | export const addComment = async (lesson_id: number, comment: string, stars: number) => { 35 | const data = { 36 | comment, 37 | stars 38 | }; 39 | return await client.post(`/comment/lesson/${lesson_id}`, data); 40 | } 41 | 42 | /** 43 | * 댓글을 수정합니다. 44 | * 45 | * @param comment_id 댓글 ID 46 | * @param comment 댓글 내용 47 | * @param stars 별 개수 48 | * @returns Axios response 49 | * @author joohongpark 50 | */ 51 | export const modComment = async (comment_id: number, comment: string, stars: number) => { 52 | const data = { 53 | comment, 54 | stars 55 | }; 56 | return await client.put(`/comment/${comment_id}`, data); 57 | } 58 | 59 | /** 60 | * 댓글을 삭제합니다. 61 | * 62 | * @param comment_id 댓글 ID 63 | * @returns Axios response 64 | * @author joohongpark 65 | */ 66 | export const delComment = async (comment_id: number) => { 67 | return await client.delete(`/comment/${comment_id}`); 68 | } 69 | 70 | /** 71 | * 강사 당 달린 댓글 수를 가져옵니다. 72 | * 73 | * @returns Axios response 74 | * @author joohongpark 75 | */ 76 | export const getStarTutors = async () => { 77 | return await client.get(`/comment/startutor`); 78 | } -------------------------------------------------------------------------------- /munetic_express/src/tests/integration/auth.int.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../app'; 2 | import request from 'supertest'; 3 | import * as Status from 'http-status'; 4 | import { sequelize } from './../../models'; 5 | import userInfo from '../dummy/userInfo.json'; 6 | 7 | beforeAll(async () => { 8 | try { 9 | await sequelize.sync({ force: false }); 10 | } catch (e) { 11 | console.log(e); 12 | } 13 | }); 14 | describe('로그인 및 회원가입 api/auth/', () => { 15 | describe('회원가입 POST + /signup', () => { 16 | it('회원가입이 완료되면 회원정보가 반환된다.', async () => { 17 | const response = await request(app) 18 | .post('/api/auth/signup') 19 | .send({ ...userInfo }); 20 | expect(response.statusCode).toBe(Status.CREATED); 21 | expect(response.body.message).toBe('request success'); 22 | expect(response.body.data.login_id).toBe(userInfo.login_id); 23 | }); 24 | }); 25 | 26 | describe('GET + /signup/user', () => { 27 | it('중복된 ID가 있으면 상태코드 BAD_REQUEST로 응답한다.', async () => { 28 | const response = await request(app) 29 | .get('/api/auth/signup/user') 30 | .query({ login_id: userInfo.login_id }); 31 | expect(response.statusCode).toBe(Status.BAD_REQUEST); 32 | expect(response.body).toBe('이미 존재하는 유저 정보 입니다.'); 33 | }); 34 | it('중복된 email이 있으면 상태코드 BAD_REQUEST로 응답한다.', async () => { 35 | const response = await request(app) 36 | .get('/api/auth/signup/user') 37 | .query({ email: userInfo.email }); 38 | expect(response.statusCode).toBe(Status.BAD_REQUEST); 39 | expect(response.body).toBe('이미 존재하는 유저 정보 입니다.'); 40 | }); 41 | it('중복된 ID가 없으면 상태코드 OK로 응답한다.', async () => { 42 | const response = await request(app) 43 | .get('/api/auth/signup/user') 44 | .query({ login_id: 'wi2238' }); 45 | expect(response.statusCode).toBe(Status.OK); 46 | expect(response.body.message).toBe('사용할 수 있는 유저 정보 입니다.'); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Info/User/OverView.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { useInfo } from '../../../contexts/info'; 3 | import Button from '../../Button'; 4 | import * as Api from '../../../lib/api'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | export default function OverView() { 8 | const info = useInfo() as any; 9 | const navigate = useNavigate(); 10 | 11 | const deleteUserHandler = () => { 12 | if (window.confirm(`${info.login_id} 유저를 삭제하시겠습니까?`)) { 13 | Api.deleteUser(info.id) 14 | .then(() => { 15 | alert('삭제되었습니다.'); 16 | navigate(0); 17 | }) 18 | .catch(err => alert(err.response.data)); 19 | } 20 | }; 21 | 22 | return ( 23 | <> 24 | 25 | {info.nickname} 26 | {info.login_id} 27 | 28 | 회원 삭제 29 | 30 | 31 | ); 32 | } 33 | 34 | const UserImage = styled.div<{ url: string }>` 35 | background-color: rgb(149, 167, 255); 36 | background-image: ${props => `url(${props.url})`}; 37 | background-size: cover; 38 | background-position: center; 39 | width: 11rem; 40 | height: 11rem; 41 | margin: 2rem auto 1.5rem auto; 42 | overflow: hidden; 43 | border-radius: 50%; 44 | `; 45 | 46 | const UserNickname = styled.p` 47 | font-size: 1.5rem; 48 | text-align: center; 49 | margin-bottom: 0.7rem; 50 | color: #616060; 51 | `; 52 | 53 | const UserId = styled.p` 54 | font-size: 1.8rem; 55 | text-align: center; 56 | margin-bottom: 2rem; 57 | `; 58 | 59 | const CustomButton = styled(Button)` 60 | width: 15rem; 61 | background-color: rgb(82, 111, 255); 62 | display: block; 63 | margin: 0 auto; 64 | ${props => 65 | props.disabled && 66 | css` 67 | background-color: rgb(140, 140, 140); 68 | `} 69 | `; 70 | -------------------------------------------------------------------------------- /munetic_express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "munetic_express", 3 | "version": "0.0.0", 4 | "description": "backend server for the project munetic", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test:watch": "jest --watchAll ", 9 | "build": "tsc", 10 | "dev": "tsc-watch --onSuccess \"ts-node dist/server.js\"", 11 | "serve": "tsc && node dist/server.js" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcrypt": "^5.0.1", 17 | "cookie-parser": "^1.4.6", 18 | "cors": "^2.8.5", 19 | "dotenv": "^10.0.0", 20 | "express": "^4.17.1", 21 | "http-status": "^1.5.0", 22 | "jsonwebtoken": "^8.5.1", 23 | "mariadb": "^2.5.5", 24 | "passport": "^0.5.2", 25 | "passport-jwt": "^4.0.0", 26 | "passport-local": "^1.0.0", 27 | "sequelize": "^6.12.0-beta.1" 28 | }, 29 | "devDependencies": { 30 | "@types/bcrypt": "^5.0.0", 31 | "@types/cookie-parser": "^1.4.2", 32 | "@types/cors": "^2.8.12", 33 | "@types/express": "^4.17.13", 34 | "@types/jest": "^27.0.3", 35 | "@types/jsonwebtoken": "^8.5.6", 36 | "@types/multer": "^1.4.7", 37 | "@types/node": "^16.11.11", 38 | "@types/passport": "^1.0.7", 39 | "@types/passport-jwt": "^3.0.6", 40 | "@types/passport-local": "^1.0.34", 41 | "@types/supertest": "^2.0.11", 42 | "@types/swagger-jsdoc": "^6.0.1", 43 | "@types/swagger-ui-express": "^4.1.3", 44 | "@typescript-eslint/eslint-plugin": "5.6.0", 45 | "@typescript-eslint/parser": "5.6.0", 46 | "eslint": "8.4.1", 47 | "eslint-config-airbnb": "19.0.2", 48 | "jest": "^27.4.5", 49 | "multer": "^1.4.4", 50 | "node-mocks-http": "^1.11.0", 51 | "sequelize-cli": "^6.3.0", 52 | "supertest": "^6.1.6", 53 | "swagger-jsdoc": "^6.1.0", 54 | "swagger-ui-express": "^4.3.0", 55 | "ts-jest": "^27.1.2", 56 | "ts-node": "^10.4.0", 57 | "tsc-watch": "^4.5.0", 58 | "typescript": "^4.5.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Routing.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Route, Routes } from 'react-router'; 3 | import HomePage from '../pages/HomePage'; 4 | import LessonListPage from '../pages/LessonListPage'; 5 | import LessonInfoPage from '../pages/LessonInfoPage'; 6 | import UserListPage from '../pages/UserListPage'; 7 | import AdminUserPage from '../pages/AdminUserPage'; 8 | import AdminUserInfoPage from '../pages/AdminUserInfoPage'; 9 | import InfoProvider from '../contexts/info'; 10 | import UserInfoPage from '../pages/UserInfoPage'; 11 | import PasswordChangePage from '../pages/PasswordChangePage'; 12 | import CommentListPage from '../pages/CommentListPage'; 13 | import EditTermsPage from '../pages/EditTermsPage'; 14 | import EditLicensePage from '../pages/EditLicensePage'; 15 | 16 | export default function Routing() { 17 | return ( 18 | 19 | 20 | 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | const RoutesContainer = styled.div` 39 | position: relative; 40 | padding: 8rem 3rem 3rem 3rem; 41 | min-width: 110rem; 42 | justify-content: right; 43 | font-size: 1.6rem; 44 | `; 45 | -------------------------------------------------------------------------------- /munetic_express/src/tests/dummy/userProfileInstance.ts: -------------------------------------------------------------------------------- 1 | import { Account, Gender, User } from '../../models/user'; 2 | 3 | export const kunlee = new User({ 4 | type: Account.Student, 5 | login_id: '42kunlee', 6 | login_password: 7 | '$2b$10$fO/O6fF5w1HDkXNab8AMBOYE/9ByW8/sjIeXpQONQgJxkegxdFDIq', 8 | nickname: 'kunlee', 9 | name: '쿠운리', 10 | name_public: true, 11 | gender: Gender.Male, 12 | birth: new Date('1992-10-05'), 13 | email: '42.kunlee@gmail.com', 14 | phone_number: '010-1234-1234', 15 | phone_public: true, 16 | image_url: '../../munetic_app/public/img/testImg.png', 17 | introduction: '안녕하세요. kunlee입니다. test data입니다.', 18 | createdAt: new Date(), 19 | updatedAt: new Date(), 20 | }); 21 | 22 | const jolim = new User({ 23 | type: Account.Tutor, 24 | login_id: '42jolim', 25 | login_password: 26 | '$2b$10$fO/O6fF5w1HDkXNab8AMBOYE/9ByW8/sjIeXpQONQgJxkegxdFDIq', 27 | nickname: 'jolim', 28 | name: '조올림', 29 | name_public: true, 30 | gender: Gender.Male, 31 | birth: new Date('1992-10-05'), 32 | email: '42.jolim@gmail.com', 33 | phone_number: '010-5678-5678', 34 | phone_public: false, 35 | image_url: '../../munetic_app/public/img/testImg.png', 36 | introduction: '안녕하세요. jolim입니다. test data입니다.', 37 | createdAt: new Date(), 38 | updatedAt: new Date(), 39 | }); 40 | 41 | const chaepark = new User({ 42 | type: Account.Student, 43 | login_id: '42chaepark', 44 | login_password: 45 | '$2b$10$fO/O6fF5w1HDkXNab8AMBOYE/9ByW8/sjIeXpQONQgJxkegxdFDIq', 46 | nickname: 'chaepark', 47 | name: '채애팤', 48 | name_public: false, 49 | gender: Gender.Female, 50 | birth: new Date('1992-10-05'), 51 | email: '42.chaepark@gmail.com', 52 | phone_number: '010-1234-5678', 53 | phone_public: true, 54 | image_url: '../../munetic_app/public/img/testImg.png', 55 | introduction: '안녕하세요. chaepark입니다. test data입니다.', 56 | createdAt: new Date(), 57 | updatedAt: new Date(), 58 | }); 59 | 60 | export const userProfileInstance = [kunlee, jolim, chaepark]; 61 | -------------------------------------------------------------------------------- /munetic_express/src/models/bookmark.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes, Model, Optional, HasOneGetAssociationMixin } from 'sequelize'; 2 | import { Lesson } from './lesson'; 3 | 4 | export interface bookmarkAttributes { 5 | id: number; 6 | user_id: number; 7 | lesson_id: number; 8 | lesson_bookmark: boolean; 9 | createdAt: Date; 10 | updatedAt: Date; 11 | deletedAt: Date; 12 | } 13 | 14 | type bookmarkCreationAttributes = Optional; 17 | 18 | export class Bookmark 19 | extends Model 20 | implements bookmarkAttributes 21 | { 22 | public id!: number; 23 | public user_id!: number; 24 | public lesson_id!: number; 25 | public lesson_bookmark!: boolean; 26 | public readonly createdAt!: Date; 27 | public readonly updatedAt!: Date; 28 | public readonly deletedAt!: Date; 29 | 30 | declare getLesson: HasOneGetAssociationMixin; 31 | 32 | static initModel(sequelize: Sequelize): typeof Bookmark { 33 | return Bookmark.init( 34 | { 35 | id: { 36 | allowNull: false, 37 | autoIncrement: true, 38 | type: DataTypes.INTEGER, 39 | primaryKey: true, 40 | }, 41 | user_id: { 42 | allowNull: false, 43 | type: DataTypes.INTEGER, 44 | }, 45 | lesson_id: { 46 | allowNull: false, 47 | type: DataTypes.INTEGER, 48 | }, 49 | lesson_bookmark: { 50 | allowNull: false, 51 | type: DataTypes.BOOLEAN, 52 | }, 53 | createdAt: { 54 | field: 'createdAt', 55 | type: DataTypes.DATE, 56 | }, 57 | updatedAt: { 58 | field: 'updatedAt', 59 | type: DataTypes.DATE, 60 | }, 61 | deletedAt: { 62 | field: 'deletedAt', 63 | type: DataTypes.DATE, 64 | }, 65 | }, 66 | { tableName: 'Bookmark', sequelize }, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /munetic_express/src/models/lessonLike.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes, Model, Optional, HasOneGetAssociationMixin } from 'sequelize'; 2 | import { Lesson } from './lesson'; 3 | 4 | export interface lessonLikeAttributes { 5 | id: number; 6 | user_id: number; 7 | lesson_id: number; 8 | lesson_like: boolean; 9 | createdAt: Date; 10 | updatedAt: Date; 11 | deletedAt: Date; 12 | } 13 | 14 | type lessonLikeCreationAttributes = Optional; 17 | 18 | export class LessonLike 19 | extends Model 20 | implements lessonLikeAttributes 21 | { 22 | public id!: number; 23 | public user_id!: number; 24 | public lesson_id!: number; 25 | public lesson_like!: boolean; 26 | public readonly createdAt!: Date; 27 | public readonly updatedAt!: Date; 28 | public readonly deletedAt!: Date; 29 | 30 | declare getLesson: HasOneGetAssociationMixin; 31 | 32 | static initModel(sequelize: Sequelize): typeof LessonLike { 33 | return LessonLike.init( 34 | { 35 | id: { 36 | allowNull: false, 37 | autoIncrement: true, 38 | type: DataTypes.INTEGER, 39 | primaryKey: true, 40 | }, 41 | user_id: { 42 | allowNull: false, 43 | type: DataTypes.INTEGER, 44 | }, 45 | lesson_id: { 46 | allowNull: false, 47 | type: DataTypes.INTEGER, 48 | }, 49 | lesson_like: { 50 | allowNull: false, 51 | type: DataTypes.BOOLEAN, 52 | }, 53 | createdAt: { 54 | field: 'createdAt', 55 | type: DataTypes.DATE, 56 | }, 57 | updatedAt: { 58 | field: 'updatedAt', 59 | type: DataTypes.DATE, 60 | }, 61 | deletedAt: { 62 | field: 'deletedAt', 63 | type: DataTypes.DATE, 64 | }, 65 | }, 66 | { tableName: 'LessonLike', sequelize }, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /munetic_express/src/controllers/admin/comment.controller.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import Status from 'http-status'; 3 | import ErrorResponse from '../../modules/errorResponse'; 4 | import { ResJSON } from '../../modules/types'; 5 | import * as CommentService from '../../service/comment.service'; 6 | 7 | /** 8 | * 댓글들을 삭제하는 미들웨어 9 | * 10 | * @param req request Objrct 11 | * @param res response Objrct 12 | * @param next next middleware function Object 13 | * @author joohongpark 14 | */ 15 | export const delComments: RequestHandler = async (req, res, next) => { 16 | try { 17 | const numbers = req.body as number[]; 18 | if (numbers.length === undefined) { 19 | next(new ErrorResponse(Status.BAD_REQUEST, '잘못된 요청입니다.')); 20 | } else { 21 | let force: boolean = 'true'.localeCompare(req.query.force as string) == 0; 22 | const del: number = await CommentService.removeComments( 23 | numbers, 24 | force, 25 | ); 26 | let result: ResJSON = new ResJSON( 27 | '댓글들을 삭제했습니다.' , 28 | del, 29 | ); 30 | res.status(Status.OK).json(result); 31 | } 32 | } catch (err) { 33 | next(err); 34 | } 35 | }; 36 | 37 | /** 38 | * 모든 댓글을 읽어오는 미들웨어 39 | * GET Request -> 200, 401 Response 40 | * 41 | * @param req request Objrct 42 | * @param res response Objrct 43 | * @param next next middleware function Object 44 | * @author joohongpark 45 | */ 46 | export const getAllComments: RequestHandler = async (req, res, next) => { 47 | try { 48 | let offset: number | undefined = parseInt(req.query.offset as string); 49 | let limit: number | undefined = parseInt(req.query.limit as string); 50 | offset = Number.isNaN(offset) ? undefined : offset; 51 | limit = Number.isNaN(limit) ? undefined : limit; 52 | let comments = await CommentService.searchAllComments(offset, limit, true); 53 | let result: ResJSON = new ResJSON( 54 | '댓글들을 불러오는데 성공하였습니다.', 55 | comments, 56 | ); 57 | res.status(Status.OK).json(result); 58 | } catch (err) { 59 | next(err); 60 | } 61 | }; -------------------------------------------------------------------------------- /munetic_express/src/tests/unit/user.service.unit.test.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../models/user.model'; 2 | import UserInstance from '../dummy/userInstance'; 3 | import * as UserService from '../../service/user.service'; 4 | import { kunlee, userProfileInstance } from '../dummy/userProfileInstance'; 5 | 6 | jest.mock('../../models/user'); 7 | const userFindAll = jest.spyOn(User, 'findAll'); 8 | 9 | describe('유저 검색: UserService.search unit test', () => { 10 | const userInfo = { 11 | login_id: 'pca0046', 12 | }; 13 | 14 | it('인자로 들어온 조건에 대해 findAll 함수로 조회한다.', async () => { 15 | await UserService.search(userInfo); 16 | expect(User.findAll).toBeCalled(); 17 | }); 18 | it('findAll 함수로 조회된 리스트를 리턴한다.', () => { 19 | userFindAll.mockResolvedValueOnce([UserInstance]); 20 | UserService.search(userInfo).then(data => 21 | expect(data).toStrictEqual([UserInstance]), 22 | ); 23 | }); 24 | }); 25 | 26 | const userFindAndCountAll = jest.spyOn(User, 'findAndCountAll'); 27 | 28 | describe('전체 유저 프로필 검색: UserService.findAllUser unit test', () => { 29 | const page = 0; 30 | it('전체 유저 get 요청이 들어오면 findAndCountAll 함수가 실행된다.', async () => { 31 | await UserService.findAllUser(page); 32 | expect(User.findAndCountAll).toBeCalled(); 33 | }); 34 | it('findAndCountAll 함수로 조회된 리스트를 리턴한다.', () => { 35 | const resValue = { count: [3], rows: userProfileInstance }; 36 | userFindAndCountAll.mockResolvedValueOnce(resValue); 37 | UserService.findAllUser(page).then(data => 38 | expect(data).toStrictEqual(resValue), 39 | ); 40 | }); 41 | }); 42 | 43 | const userFindOne = jest.spyOn(User, 'findOne'); 44 | 45 | describe('유저 프로필 id로 검색: UserService.findUserById unit test', () => { 46 | const id = 1; 47 | 48 | it('유저 get 요청이 id와 함께 들어오면 findOne 함수가 실행된다.', async () => { 49 | await UserService.findUserById(id); 50 | expect(User.findOne).toBeCalled(); 51 | }); 52 | it('findOne 함수로 조회된 유저 정보를 리턴한다.', () => { 53 | userFindOne.mockResolvedValueOnce(kunlee); 54 | UserService.findUserById(id).then(data => 55 | expect(data).toStrictEqual(kunlee), 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /munetic_admin/src/pages/AdminUserPage.tsx: -------------------------------------------------------------------------------- 1 | import { AddAdminUser } from '../components/AdminUser/AddAdminUser'; 2 | import MUITable from '../components/Table/MUITable'; 3 | import { useEffect, useState } from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import * as Api from '../lib/api'; 6 | 7 | function delUsers(arr: ReadonlyArray) { 8 | Api.deleteUsers(arr, false) 9 | .then(res => console.log(`${res.data.data}명의 유저 삭제`)) 10 | .catch(e => console.log(`err : ${e}`)); 11 | } 12 | 13 | export default function AdminUserPage() { 14 | const [page, setPage] = useState(0); 15 | const [rowsPerPage, setRowsPerPage] = useState(10); 16 | const [rows, setRows] = useState<[]>([]); 17 | const [count, setCount] = useState(0); 18 | 19 | const navigate = useNavigate(); 20 | 21 | /** 22 | * Page 전환 23 | */ 24 | const handleChangePage = ( 25 | event: React.MouseEvent | null, 26 | newPage: number, 27 | ) => { 28 | setPage(newPage); 29 | }; 30 | 31 | /** 32 | * 한 페이지에 노출하는 row수 33 | */ 34 | const handleChangeRowsPerPage = ( 35 | event: React.ChangeEvent, 36 | ) => { 37 | setRowsPerPage(parseInt(event.target.value, 10)); 38 | setPage(0); 39 | }; 40 | 41 | const getAdminUsers = () => { 42 | // FIXME: Offset 추가해야 할듯? 43 | Api.getAdminUserList(page) 44 | .then(({ data }: any) => { 45 | setRows(data.data.rows); 46 | setCount(parseInt(data.data.count, 10)); 47 | }) 48 | .catch(err => { 49 | if (err.response) alert(err.response.data); 50 | navigate('/'); 51 | }); 52 | } 53 | 54 | useEffect(() => { 55 | getAdminUsers(); 56 | }, []); 57 | 58 | return ( 59 | <> 60 | 61 | { 67 | delUsers(arr); 68 | getAdminUsers(); 69 | }} 70 | handleChangePage={handleChangePage} 71 | handleChangeRowsPerPage={handleChangeRowsPerPage} 72 | /> 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /munetic_app/src/components/common/Select.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import styled from 'styled-components'; 3 | import Contexts from '../../context/Contexts'; 4 | import palette from '../../style/palette'; 5 | 6 | const Container = styled.div` 7 | span { 8 | margin-top: 15px; 9 | display: block; 10 | font-size: 13px; 11 | color: ${palette.grayBlue}; 12 | margin-bottom: 3px; 13 | } 14 | select { 15 | width: 100%; 16 | height: 30px; 17 | padding-left: 10px; 18 | background-color: white; 19 | border-radius: 4px; 20 | outline: none; 21 | color: ${palette.grayBlue}; 22 | font-size: 15px; 23 | border: none; 24 | border-bottom: 1px solid ${palette.grayBlue}; 25 | } 26 | .errorMessage { 27 | font-size: 12px; 28 | font-weight: normal; 29 | color: ${palette.red}; 30 | margin: 5px 0; 31 | } 32 | `; 33 | 34 | interface IProps extends React.SelectHTMLAttributes { 35 | title?: string; 36 | options?: string[]; 37 | disabledOptions?: string[]; 38 | isValid?: boolean; 39 | useValidation?: boolean; 40 | errorMessage?: string; 41 | } 42 | 43 | export default function Select({ 44 | title, 45 | options = [], 46 | disabledOptions = [], 47 | isValid, 48 | useValidation = true, 49 | errorMessage = '값을 입력하세요.', 50 | ...props 51 | }: IProps) { 52 | //폼 제출할 때 validationMode를 true 로 바꿔서 유효값이 들어갔는지 판단하기위한 것 53 | const { state } = useContext(Contexts); 54 | 55 | return ( 56 | 57 | 72 | {useValidation && state.validationMode && !isValid && ( 73 |
74 |

{errorMessage}

75 |
76 | )} 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /docs/scenario.md: -------------------------------------------------------------------------------- 1 | # 뮤네틱 유저 시나리오 2 | 3 | # figma 와이어프레임 4 | - [일반 화면(~2022.04.19)](https://www.figma.com/file/6THnbJkS1vHRshCWU7422o/%EB%AE%A4%EB%84%A4%ED%8B%B1-%ED%99%94%EB%A9%B4-%EC%84%A4%EB%AA%85%EC%84%9C) 5 | - [새로운 화면(2022.04.22~)](https://www.figma.com/file/9EYawhC2W8W243ADmDt5iw/MUNETIC-APP-%ED%99%94%EB%A9%B4%EC%84%A4%EA%B3%84%EC%84%9C%2B%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%B0%A8%ED%8A%B8(FIXED-VER.)) 6 | 7 | 8 | ## 뮤네틱의 유저 계정은 크게 3가지 유저로 나뉩니다. 9 | * 선생님 계정 Tutor Account 10 | * 학생 계정 Student Account 11 | * 어드민 Admin 12 | 13 | ## 프로세스 14 | 15 | * 초기 가입 경로는 학생계정으로 진행됩니다. 16 | * 학생계정은 앱에서 레슨 정보글에 한정하여 열람할 수 있는 권한을 주요 기능으로 갖습니다. 17 | * 선생님 계정은 홈 화면의 ‘레슨 등록’을 통하거나 설정 탭에서 ‘선생님 계정 가입’ 탭을 통해 가입할 수 있어야 합니다. 18 | * 선생님 계정은 학생계정에서 기능이 확장되어 수업 등록/수정과 프로필 관리등이 추가적으로 작동합니다. 나머지 기능은 학생 계정과 동일하게 작동합니다. 19 | * 어드민 계정은 앱의 모든 페이지에 대한 관리를 담당하며 어드민 페이지에 대한 접근 권한을 가집니다. 20 | * 학생/선생님 계정은 서로 접근 권한이 다르며 이에 따라 일부는 보여지는 화면이 다릅니다. 21 | * Ex) 학생 계정과 선생님 계정의 보여지는 화면이 다르다는 뜻은 22 | * 프로필 등록 및 수정 등에서 보여집니다. (MNT-C-002 와 MNT-C-004의 차이. *Figma 참조) 23 | 24 | ## 접근 권한 25 | 26 | * 학생 계정 : 레슨 검색, 내가 단 댓글, 북마크, 설정, 학생 프로필 27 | * 선생님 계정 : 레슨 등록 및 수정, 레슨 검색, 내가 단 댓글, 북마크 선생님 프로필 등 28 | * 어드민 : 관리자(어드민) 페이지, 앱 내의 모든 게시물에 대한 등록,수정 및 삭제 권한. 29 | 30 | ## 3가지 주요 시나리오 31 | 가장 핵심적인 3가지 주요 시나리오에 대해서 설명드리겠습니다. 32 | 33 | ### 학생 유저의 레슨 정보 검색 시나리오 34 | * 앱 시동시 MNT-A-001 (첫화면)에 진입 후 가입하기 탭. 35 | * 약관 동의 및 MNT-A-003 학생 계정 정보 기입에서 정보 기입 및 가입 후 [MNT-H-001.홈 화면]으로 진입 36 | * 레슨 찾기 탭을 통해 MNT-L-001 레슨 카테고리로 진입 37 | * 카테고리중 원하는 레슨 분류를 하나 선택하여 탭하여 해당 카테고리의 레슨목록 [MNT-L-002 레슨 글 목록]진입 38 | * 원하는 레슨글을 선택하여 [MNT-L-003 레슨 글 상세 정보]를 열람 39 | * 선생님 계정이 등록한 레슨 상세 정보를 통해 **연락처를 얻고 선생님에게 직접 연락** 40 | 41 | ### 선생님 유저의 레슨 등록 시나리오 42 | * 앱 시동시 MNT-A-001 (첫화면)에 진입 후 가입하기 탭. 43 | * 약관 동의 및 MNT-A-003 학생 계정 정보 기입에서 정보 기입 및 가입 후 [MNT-H-001.홈 화면]으로 진입->여기까지는 학생 계정의 가입 절차와 동일 44 | * 홈 화면에서 ‘레슨 등록’ 클릭 후,[MNT-A-004 선생님 계정 가입]으로 이동 후 가입 양식 작성 후 가입 성공시 [MNT-H-001.홈 화면]으로 재진입 45 | * 홈 화면에서 레슨 등록 탭을 통해 [MNT-C-007 레슨 등록/수정 페이지]으로 이동 46 | * 이동 후 ‘레슨 등록하기’ 탭을 통해 [MNT-C-008 레슨 글 등록]으로 이동하여 카테고리 설정 및 양식 작성 후 등록 완료 47 | * 이후 등록된 글은 성공적으로 [MNT-L-002 레슨 글 목록]에 디스플레이 됨. 48 | 49 | ### 어드민 시나리오 50 | * 관리자 페이지에 접속하면 첫 화면으로는 [MNT-S-001 어드민 페이지 홈]으로 진입 51 | * MNT-S-001 어드민 페이지 홈에서 신규 가입자 및 게시물 내역과 데이터 지표를 확인 가능 52 | * 각 상단 탭에서 관리도구들을 통하여 앱에 대한 전반적인 관리 진행.(세부 기능은 Figma 참조) 53 | -------------------------------------------------------------------------------- /munetic_app/src/components/common/BottomMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import BottomNavigation from '@mui/material/BottomNavigation'; 4 | import MuiBottomNavigationAction from '@mui/material/BottomNavigationAction'; 5 | import HomeIcon from '@mui/icons-material/Home'; 6 | import SearchIcon from '@mui/icons-material/Search'; 7 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 8 | import BookmarkIcon from '@mui/icons-material/Bookmark'; 9 | import SettingsIcon from '@mui/icons-material/Settings'; 10 | import { styled } from '@mui/material'; 11 | import palette from '../../style/palette'; 12 | import { useLocation, useNavigate } from 'react-router-dom'; 13 | 14 | const BottomNavigationAction = styled(MuiBottomNavigationAction)(` 15 | &.Mui-selected { 16 | color: ${palette.darkBlue}; 17 | } 18 | background-color: ${palette.green}; 19 | color: ${palette.grayBlue}; 20 | `); 21 | 22 | export default function BottomMenu() { 23 | const currentPath = useLocation().pathname; 24 | const navigate = useNavigate(); 25 | const [value, setValue] = useState(0); 26 | 27 | const onChangeMenu = (event: any, newValue: number) => { 28 | setValue(newValue); 29 | const paths = ['/', '/search', '/profile/manage', '/bookmark', '/setting']; 30 | navigate(paths[newValue]); 31 | }; 32 | 33 | useEffect(() => { 34 | if (currentPath.includes('/profile/')) { 35 | setValue(2); 36 | } //나머지 메뉴들 생기면 추가로 만들어줘야함 37 | else { 38 | setValue(0); 39 | } 40 | }, [currentPath]); 41 | 42 | return ( 43 | 46 | onChangeMenu(event, newValue)} 50 | > 51 | } /> 52 | } /> 53 | } /> 54 | } /> 55 | } /> 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /.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 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Databse 107 | munetic_database/db 108 | 109 | # package-lock 110 | package-lock.json 111 | 112 | _* 113 | .DS_Store -------------------------------------------------------------------------------- /munetic_express/src/models/comment.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes, Model, Optional, HasOneGetAssociationMixin } from 'sequelize'; 2 | 3 | /** 4 | * Comment 테이블의 어트리뷰트들을 명시합니다. 5 | * 6 | * @Author joohongpark 7 | */ 8 | export interface commentAttributes { 9 | id: number; 10 | user_id: number; 11 | lesson_id: number; 12 | content: string; 13 | stars: number; 14 | createdAt: Date; 15 | updatedAt: Date; 16 | deletedAt: Date; 17 | } 18 | 19 | /** 20 | * Comment 테이블에 값을 삽입할 때 (자동 생성되어서) 생략해도 되는 데이터를 명시합니다. 21 | * 22 | * @Author joohongpark 23 | */ 24 | type commentCreationAttributes = Optional; 27 | 28 | /** 29 | * Comment 데이터 모델(및 테이블)을 정의합니다. 30 | * 31 | * @Author joohongpark 32 | */ 33 | export class Comment 34 | extends Model 35 | implements commentAttributes 36 | { 37 | public id!: number; 38 | public user_id!: number; 39 | public lesson_id!: number; 40 | public content!: string; 41 | public stars!: number; 42 | public readonly createdAt!: Date; 43 | public readonly updatedAt!: Date; 44 | public readonly deletedAt!: Date; 45 | 46 | static initModel(sequelize: Sequelize): typeof Comment { 47 | return Comment.init( 48 | { 49 | id: { 50 | allowNull: false, 51 | autoIncrement: true, 52 | type: DataTypes.INTEGER, 53 | primaryKey: true, 54 | }, 55 | user_id: { 56 | allowNull: false, 57 | type: DataTypes.INTEGER, 58 | }, 59 | lesson_id: { 60 | allowNull: false, 61 | type: DataTypes.INTEGER, 62 | }, 63 | content: { 64 | allowNull: false, 65 | type: DataTypes.STRING(8192), 66 | }, 67 | stars: { 68 | allowNull: false, 69 | type: DataTypes.INTEGER, 70 | }, 71 | createdAt: { 72 | field: 'createdAt', 73 | type: DataTypes.DATE, 74 | }, 75 | updatedAt: { 76 | field: 'updatedAt', 77 | type: DataTypes.DATE, 78 | }, 79 | deletedAt: { 80 | field: 'deletedAt', 81 | type: DataTypes.DATE, 82 | }, 83 | }, 84 | { tableName: 'Comment', sequelize }, 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/MUITableToolbar.tsx: -------------------------------------------------------------------------------- 1 | import Toolbar from '@mui/material/Toolbar'; 2 | import Typography from '@mui/material/Typography'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import Tooltip from '@mui/material/Tooltip'; 5 | import DeleteIcon from '@mui/icons-material/Delete'; 6 | import FilterListIcon from '@mui/icons-material/FilterList'; 7 | import { alpha } from '@mui/material/styles'; 8 | import { useLocation } from 'react-router-dom'; 9 | import { useInfo } from '../../contexts/info'; 10 | 11 | interface MUITableToolbarProps { 12 | numSelected: number; 13 | onClickDeleteButton: () => void; 14 | } 15 | 16 | export default function MUITableToolbar(props: MUITableToolbarProps) { 17 | const { numSelected, onClickDeleteButton } = props; 18 | const path = useLocation().pathname; 19 | const info = useInfo() as any; 20 | 21 | return ( 22 | 0 && { 27 | bgcolor: theme => 28 | alpha( 29 | theme.palette.primary.main, 30 | theme.palette.action.activatedOpacity, 31 | ), 32 | }), 33 | }} 34 | > 35 | {numSelected > 0 ? ( 36 | 42 | {numSelected} selected 43 | 44 | ) : ( 45 | 51 | {path === '/users' && 'App User'} 52 | {path === '/comments' && 'Comments'} 53 | {path === '/admin_users' && 'Admin User'} 54 | {(path === '/lessons' || path === `/users/${info!.id}`) && 'Lesson'} 55 | 56 | )} 57 | {numSelected > 0 ? ( 58 | 59 | 60 | 61 | 62 | 63 | ) : ( 64 | 65 | 66 | 67 | 68 | 69 | )} 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /munetic_app/src/components/lesson/CategoryContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import palette from '../../style/palette'; 4 | import { ICategoryTable } from '../../types/categoryData'; 5 | import Button from '../common/Button'; 6 | import * as CategoryAPI from '../../lib/api/category'; 7 | 8 | const CategoryPageContainer = styled.div` 9 | margin: 30px; 10 | height: 70%; 11 | background-color: ${palette.grayBlue}; 12 | border-radius: 5px; 13 | .categoryTitle { 14 | margin: 15px 0px 10px 0px; 15 | color: ${palette.green}; 16 | font-size: 20px; 17 | font-weight: bold; 18 | } 19 | .categoryWrapper { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | } 24 | .categoryIconWrapper { 25 | width: 80%; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | flex-wrap: wrap; 30 | margin-bottom: 35px; 31 | } 32 | `; 33 | 34 | const StyledButton = styled(Button)` 35 | width: 28%; 36 | border-radius: 7px; 37 | background-color: ${palette.green}; 38 | margin: 5px 5px; 39 | padding: 0; 40 | .buttonText { 41 | width: 100%; 42 | margin: 0; 43 | font-size: 15px; 44 | color: ${palette.darkBlue}; 45 | } 46 | `; 47 | 48 | export default function CategoryContainer() { 49 | const [categoryData, setCategoryData] = useState(); 50 | 51 | useEffect(() => { 52 | async function getCategory() { 53 | try { 54 | const res = await CategoryAPI.getCategories(); 55 | setCategoryData(res.data.data); 56 | } catch (e) { 57 | console.log(e, '카테고리를 불러오지 못했습니다.'); 58 | } 59 | } 60 | getCategory(); 61 | }, []); 62 | 63 | return ( 64 | 65 |
66 |
카테고리별 검색
67 | {categoryData && ( 68 |
69 | {categoryData.map((category, i) => ( 70 | 74 | {category.name} 75 | 76 | ))} 77 |
78 | )} 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /munetic_app/src/components/like/LikeButton.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Favorite, FavoriteBorder } from '@mui/icons-material'; 3 | import { IconButton } from '@mui/material'; 4 | import * as LikeAPI from '../../lib/api/like'; 5 | 6 | /** 7 | * 서버와의 비동기 통신으로 좋아요 상태를 가져오는 함수입니다. 8 | * 9 | * @param lesson_id number 강의 ID 10 | * @returns boolean 좋아요/싫어요 11 | * @author joohongpark 12 | */ 13 | async function getLessonLike(lesson_id: number): Promise { 14 | if (Number.isNaN(lesson_id)) 15 | return false; 16 | try { 17 | const res = await LikeAPI.getLessonLike(lesson_id); 18 | return res.data.data; 19 | } catch (e) { 20 | console.log(e, '좋아요 정보를 가져오는 데 오류가 발생했습니다.'); 21 | return false; 22 | } 23 | } 24 | 25 | /** 26 | * 서버와의 비동기 통신으로 좋아요 상태를 반영하는 함수입니다. 27 | * 28 | * @param lesson_id number 강의 ID 29 | * @param liked boolean 좋아요/싫어요 30 | * @returns boolean 반영 여부 31 | * @author joohongpark 32 | */ 33 | async function ToggleLessonLike(lesson_id: number, liked: boolean): Promise { 34 | if (Number.isNaN(lesson_id)) 35 | return false; 36 | try { 37 | let result: any; 38 | if (liked) { 39 | result = await LikeAPI.delLessonLike(lesson_id); 40 | } else { 41 | result = await LikeAPI.putLessonLike(lesson_id); 42 | } 43 | return result.data.data; 44 | } catch (e) { 45 | console.log(e, '좋아요 정보를 가져오는 데 오류가 발생했습니다.'); 46 | return false; 47 | } 48 | } 49 | 50 | /** 51 | * LikeButton 프로퍼티 인터페이스 52 | */ 53 | export interface LikeButtonIProps { 54 | lesson_id: number 55 | } 56 | 57 | /** 58 | * 좋아요 버튼 컴포넌트입니다. 서버와의 비동기 통신으로 강의에 대한 좋아요 여부 및 좋아요 상태를 업데이트 합니다. 59 | * 60 | * @param props.lesson_id 강의 ID 61 | * @returns 리액트 앨리먼트 62 | * @author joohongpark 63 | */ 64 | export default function LikeButton({lesson_id} : LikeButtonIProps) { 65 | const [like, setLike] = useState(false); 66 | 67 | useEffect(() => { 68 | getLessonLike(lesson_id).then(result => setLike(result)); 69 | }, [lesson_id]); 70 | 71 | return ( 72 | ToggleLessonLike(lesson_id, like).then( 76 | (result) => { 77 | if (result) { 78 | setLike(!like); 79 | } 80 | } 81 | ) 82 | }> 83 | {like ? : } 84 | 85 | ); 86 | } -------------------------------------------------------------------------------- /munetic_express/seeders/3-Comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | 6 | await queryInterface.bulkInsert('Comment', [ 7 | { 8 | user_id: 13, 9 | lesson_id: 2, 10 | content: '안녕하세요', 11 | stars: 5, 12 | createdAt: Sequelize.fn('now'), 13 | updatedAt: Sequelize.fn('now'), 14 | }, 15 | { 16 | user_id: 8, 17 | lesson_id: 1, 18 | content: '반가워요', 19 | stars: 5, 20 | createdAt: Sequelize.fn('now'), 21 | updatedAt: Sequelize.fn('now'), 22 | }, 23 | { 24 | user_id: 9, 25 | lesson_id: 3, 26 | content: '좋아요', 27 | stars: 5, 28 | createdAt: Sequelize.fn('now'), 29 | updatedAt: Sequelize.fn('now'), 30 | }, 31 | { 32 | user_id: 13, 33 | lesson_id: 3, 34 | content: '짱이에요', 35 | stars: 5, 36 | createdAt: Sequelize.fn('now'), 37 | updatedAt: Sequelize.fn('now'), 38 | }, 39 | { 40 | user_id: 13, 41 | lesson_id: 2, 42 | content: '너무 좋아요', 43 | stars: 2, 44 | createdAt: Sequelize.fn('now'), 45 | updatedAt: Sequelize.fn('now'), 46 | }, 47 | { 48 | user_id: 11, 49 | lesson_id: 5, 50 | content: '좋습니다', 51 | stars: 5, 52 | createdAt: Sequelize.fn('now'), 53 | updatedAt: Sequelize.fn('now'), 54 | }, 55 | { 56 | user_id: 3, 57 | lesson_id: 5, 58 | content: '굿이에요', 59 | stars: 5, 60 | createdAt: Sequelize.fn('now'), 61 | updatedAt: Sequelize.fn('now'), 62 | }, 63 | { 64 | user_id: 1, 65 | lesson_id: 5, 66 | content: '그저 그래요', 67 | stars: 3, 68 | createdAt: Sequelize.fn('now'), 69 | updatedAt: Sequelize.fn('now'), 70 | }, 71 | { 72 | user_id: 2, 73 | lesson_id: 5, 74 | content: '별로에요', 75 | stars: 1, 76 | createdAt: Sequelize.fn('now'), 77 | updatedAt: Sequelize.fn('now'), 78 | }, 79 | ], {}); 80 | }, 81 | 82 | down: async (queryInterface, Sequelize) => { 83 | await queryInterface.bulkDelete('Comment', null, {}); 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /munetic_admin/src/components/Table/MUITableRow.tsx: -------------------------------------------------------------------------------- 1 | import TableRow from '@mui/material/TableRow'; 2 | import TableCell from '@mui/material/TableCell'; 3 | import Checkbox from '@mui/material/Checkbox'; 4 | import { useLocation } from 'react-router-dom'; 5 | import UserTableCell from './User/UserTableCell'; 6 | import AdminUserTableCell from './AdminUser/AdminUserTableCell'; 7 | import LessonTableCell from './Lesson/LessonTableCell'; 8 | import { useInfo } from '../../contexts/info'; 9 | import { useNavigate } from 'react-router-dom'; 10 | import CommentTableCell from './Comment/CommentHeadCell'; 11 | 12 | export interface MUITableRowProps { 13 | numSelected: number; 14 | rowCount: number; 15 | isItemSelected: boolean; 16 | row: any; 17 | handleClick: (event: React.MouseEvent, id: number) => void; 18 | } 19 | 20 | export default function MUITableRow({ 21 | isItemSelected, 22 | row, 23 | handleClick, 24 | }: MUITableRowProps) { 25 | const path = useLocation().pathname; 26 | const info = useInfo() as any; 27 | const navigate = useNavigate(); 28 | 29 | const moveInfoPage = () => { 30 | if (path === '/users') navigate(`/users/${row.id}`); 31 | if (path === '/admin_users') navigate(`/admin_users/${row.id}`); 32 | if (path.slice(0, 7) === `/users/`) navigate(`/lessons/${row.id}`); 33 | if (path === '/lessons') navigate(`/lessons/${row.id}`); 34 | }; 35 | return ( 36 | 50 | 51 | handleClick(event, row.id)} 53 | color="primary" 54 | checked={isItemSelected} 55 | /> 56 | 57 | 58 | {row.id} 59 | 60 | {path === '/users' && } 61 | {path === '/comments' && } 62 | {path === '/admin_users' && } 63 | {(path === '/lessons' || path === `/users/${info!.id}`) && ( 64 | 65 | )} 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /munetic_app/src/components/bookmark/BookmarkButton.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Bookmark, BookmarkBorder } from '@mui/icons-material'; 3 | import { IconButton } from '@mui/material'; 4 | import * as BookmarkAPI from '../../lib/api/bookmark'; 5 | 6 | /** 7 | * 서버와의 비동기 통신으로 북마크 상태를 가져오는 함수입니다. 8 | * 9 | * @param lesson_id number 강의 ID 10 | * @returns boolean 북마크 여부 11 | * @author joohongpark 12 | */ 13 | async function getBookmark(lesson_id: number): Promise { 14 | if (Number.isNaN(lesson_id)) 15 | return false; 16 | try { 17 | const res = await BookmarkAPI.getLessonBookmark(lesson_id); 18 | return res.data.data; 19 | } catch (e) { 20 | console.log(e, '좋아요 정보를 가져오는 데 오류가 발생했습니다.'); 21 | return false; 22 | } 23 | } 24 | 25 | /** 26 | * 서버와의 비동기 통신으로 북마크 상태를 반영하는 함수입니다. 27 | * 28 | * @param lesson_id number 강의 ID 29 | * @param bookmark boolean 북마크 여부 30 | * @returns boolean 반영 여부 31 | * @author joohongpark 32 | */ 33 | async function ToggleBookmark(lesson_id: number, bookmark: boolean): Promise { 34 | if (Number.isNaN(lesson_id)) 35 | return false; 36 | try { 37 | let result: any; 38 | if (bookmark) { 39 | result = await BookmarkAPI.delLessonBookmark(lesson_id); 40 | } else { 41 | result = await BookmarkAPI.putLessonBookmark(lesson_id); 42 | } 43 | return result.data.data; 44 | } catch (e) { 45 | alert("요청을 처리하는 데 오류가 발생하였습니다."); 46 | console.log(e, '좋아요 정보를 가져오는 데 오류가 발생했습니다.'); 47 | return false; 48 | } 49 | } 50 | 51 | /** 52 | * BookmarkButton 프로퍼티 인터페이스 53 | */ 54 | export interface BookmarkButtonIProps { 55 | lesson_id: number 56 | } 57 | 58 | /** 59 | * 북마크 버튼 컴포넌트입니다. 서버와의 비동기 통신으로 강의에 대한 북마크 여부 및 북마크 상태를 업데이트 합니다. 60 | * 61 | * @returns 리액트 앨리먼트 62 | * @author joohongpark 63 | */ 64 | export default function BookmarkButton({lesson_id}: BookmarkButtonIProps) { 65 | const [bookmark, setBookmark] = useState(false); 66 | 67 | useEffect(() => { 68 | getBookmark(lesson_id).then(result => setBookmark(result)); 69 | }, [lesson_id]); 70 | 71 | return ( 72 | ToggleBookmark(lesson_id, bookmark).then( 75 | (result) => { 76 | if (result) { 77 | setBookmark(!bookmark); 78 | } 79 | } 80 | ) 81 | }> 82 | {bookmark ? : } 83 | 84 | ); 85 | } -------------------------------------------------------------------------------- /munetic_app/src/components/comment/CommentTop.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import palette from '../../style/palette'; 3 | import { FunctionPropsType } from '../../types/commentTopData'; 4 | 5 | /** 6 | * 댓글 상단 메뉴를 감싸는 컴포넌트입니다. 7 | * styled-components를 이용해 리액트 컴포넌트로 만들어 스타일을 적용합니다. 8 | * 9 | * @author joohongpark 10 | */ 11 | const ContentBox = styled.div` 12 | display: flex; 13 | align-items: center; 14 | padding: 12px 12px; 15 | `; 16 | 17 | /** 18 | * 댓글 상단 메뉴 내의 왼쪽 컴포넌트들을 감싸는 컴포넌트입니다. 19 | * styled-components를 이용해 리액트 컴포넌트로 만들어 스타일을 적용합니다. 20 | * 21 | * @author joohongpark 22 | */ 23 | const LeftBox = styled.div` 24 | color: #000; 25 | font-size: 15px; 26 | line-height: 1.5; 27 | `; 28 | 29 | /** 30 | * 텍스트를 감싸는 컴포넌트입니다. 31 | * styled-components를 이용해 리액트 컴포넌트로 만들어 스타일을 적용합니다. 32 | * 33 | * @author joohongpark 34 | */ 35 | const TextBox = styled.span` 36 | font-weight: normal; 37 | margin-left: 4px; 38 | display: inline-block; 39 | vertical-align: top; 40 | `; 41 | 42 | /** 43 | * 댓글 개수를 나타내는 컴포넌트입니다. 44 | * styled-components를 이용해 리액트 컴포넌트로 만들어 스타일을 적용합니다. 45 | * 46 | * @author joohongpark 47 | */ 48 | const CommentCount = styled.span` 49 | font-weight: normal; 50 | color: ${palette.red}; 51 | margin-left: 4px; 52 | vertical-align: top; 53 | display: inline-block; 54 | `; 55 | 56 | /** 57 | * 댓글 상단 메뉴 내의 오른쪽 컴포넌트들을 감싸는 컴포넌트입니다. 58 | * styled-components를 이용해 리액트 컴포넌트로 만들어 스타일을 적용합니다. 59 | * 60 | * @author joohongpark 61 | */ 62 | const RightBox = styled.div` 63 | margin-left: auto; 64 | font-size: 13px; 65 | vertical-align: top; 66 | `; 67 | 68 | /** 69 | * 댓글의 상단바 컴포넌트입니다. 70 | * 71 | * @param props.refrash 새로고침 함수 72 | * @param props.sortByTime 시간순 정렬 함수 73 | * @param props.sortByStar 별개수별 정렬 함수 74 | * @param props.commentCount 댓글 개수 75 | * @returns 리액트 앨리먼트 76 | * @author joohongpark 77 | */ 78 | export default function CommentTop({refrash, sortByTime, incSortByStar, decSortByStar, commentCount}: FunctionPropsType) { 79 | return ( 80 | 81 | 82 | 전체 댓글 83 | {commentCount} 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | } 94 | --------------------------------------------------------------------------------