├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── deploy.yml │ └── scoring-deploy-1.yml ├── .gitignore ├── .prettierrc ├── README.md ├── client ├── .eslintrc.json ├── .gitignore ├── axios.ts ├── components │ ├── Editor │ │ └── wrapperEditor.tsx │ ├── GNB │ │ ├── Logo.tsx │ │ ├── Menu.tsx │ │ ├── UserInfo.tsx │ │ └── index.tsx │ ├── List │ │ ├── index.tsx │ │ ├── list.tsx │ │ ├── listRow.tsx │ │ └── paginator.tsx │ ├── Modal │ │ ├── DeleteProblemModal.tsx │ │ └── index.tsx │ ├── Problem │ │ ├── CodeContainer.tsx │ │ └── ProblemContainer.tsx │ ├── common │ │ ├── Button.tsx │ │ └── IOList │ │ │ ├── InputContainer.tsx │ │ │ ├── Row.tsx │ │ │ └── index.tsx │ ├── paginator.tsx │ ├── problemList.tsx │ ├── problemListRow.tsx │ ├── status │ │ └── StatusList.tsx │ └── svgs │ │ ├── add-file.tsx │ │ ├── close.tsx │ │ ├── delete.tsx │ │ ├── edit.tsx │ │ ├── index.tsx │ │ ├── removeRow.tsx │ │ └── toggle.tsx ├── global.d.ts ├── mock │ └── problems.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── problem.tsx │ │ └── v0 │ │ │ ├── problems │ │ │ ├── [id].ts │ │ │ └── [id] │ │ │ │ ├── submissions.ts │ │ │ │ ├── tc.ts │ │ │ │ └── visible.ts │ │ │ ├── submissions.tsx │ │ │ ├── submissions │ │ │ └── [id].ts │ │ │ └── user │ │ │ └── login-status.ts │ ├── index.tsx │ ├── my-problem │ │ ├── edit │ │ │ └── [id].tsx │ │ ├── index.tsx │ │ ├── new.tsx │ │ └── tc │ │ │ └── [id].tsx │ ├── problem │ │ └── [id].tsx │ ├── status │ │ ├── [id].tsx │ │ └── index.tsx │ └── users │ │ └── oauth.tsx ├── public │ └── favicon.ico ├── styles │ ├── index.ts │ ├── modal.ts │ └── style.ts ├── tsconfig.json └── yarn.lock ├── deploy-client.sh ├── deploy-scoring-server.sh ├── deploy-server.sh ├── deploy.sh ├── scoring-server ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── docker │ ├── Dockerfile │ ├── run.sh │ └── start.sh ├── nest-cli.json ├── package.json ├── python │ └── run.py ├── src │ ├── app.module.ts │ ├── main.ts │ ├── queue │ │ ├── queue.consumer.ts │ │ └── queue.module.ts │ └── scoring │ │ ├── entities │ │ ├── language.entity.ts │ │ ├── problem.entity.ts │ │ ├── submission.entity.ts │ │ └── testcase.entity.ts │ │ ├── scoring.module.ts │ │ └── scoring.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock └── server ├── .eslintrc.js ├── .gitignore ├── nest-cli.json ├── package.json ├── src ├── app.module.ts ├── caching │ ├── caching.module.ts │ └── caching.service.ts ├── main.ts ├── problems │ ├── dtos │ │ ├── create-problem.dto.ts │ │ ├── post-submission.dto.ts │ │ ├── post-testcase.dto.ts │ │ └── update-problem.dto.ts │ ├── entities │ │ ├── example.entity.ts │ │ ├── problem.entity.ts │ │ └── testcase.entity.ts │ ├── problems.controller.ts │ ├── problems.module.ts │ ├── problems.service.ts │ └── throttler-behind-proxy.guard.ts ├── submissions │ ├── dtos │ │ └── post-result.dto.ts │ ├── entities │ │ ├── language.entity.ts │ │ ├── result.entity.ts │ │ ├── state.entity.ts │ │ └── submission.entity.ts │ ├── submissions.controller.ts │ ├── submissions.module.ts │ └── submissions.service.ts └── users │ ├── dtos │ └── github-login.dto.ts │ ├── entities │ └── user.entity.ts │ ├── users.controller.ts │ ├── users.module.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### TO DO 2 | 3 | ### Description 4 | 5 | ### Etc.. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 개요 2 | - Issue 번호를 적어주세요. 3 | - 내용을 적어주세요. 4 | 5 | ## 작업사항 6 | - 내용을 적어주세요. 7 | 8 | ## 변경로직(optional) 9 | - 내용을 적어주세요. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test 2 | 3 | on: 4 | pull_request: 5 | branches: [main, dev] 6 | types: [opened, synchronize] 7 | 8 | jobs: 9 | client-build-test: 10 | defaults: 11 | run: 12 | working-directory: client 13 | 14 | name: Client build test 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | 25 | - name: Get yarn cache directory path 26 | id: client-yarn-cache-dir-path 27 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 28 | 29 | - uses: actions/cache@v3 30 | id: client-yarn-cache 31 | with: 32 | path: | 33 | ${{ steps.client-yarn-cache-dir-path.outputs.dir }} 34 | ${{ github.workspace }}/client/.next/cache 35 | key: ${{ runner.os }}-client-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/**.[jt]s', '**/**.[jt]sx') }} 36 | restore-keys: | 37 | ${{ runner.os }}-client-yarn-${{ hashFiles('**/yarn.lock') }}- 38 | ${{ runner.os }}-client-yarn- 39 | - name: Install Dependencies 40 | run: yarn install --frozen-lockfile 41 | 42 | - name: Lint 43 | run: yarn lint 44 | 45 | - name: Build 46 | run: yarn build 47 | 48 | server-build-test: 49 | defaults: 50 | run: 51 | working-directory: server 52 | 53 | name: Server build test 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - uses: actions/checkout@v3 58 | 59 | - name: Node 60 | uses: actions/setup-node@v3 61 | with: 62 | node-version: 16 63 | 64 | - name: Get yarn cahce directory path 65 | id: server-yarn-cache-dir-path 66 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 67 | 68 | - uses: actions/cache@v3 69 | id: server-yarn-cache 70 | with: 71 | path: ${{ steps.server-yarn-cache-dir-path.outputs.dir }} 72 | key: ${{ runner.os }}-server-yarn-${{ hashFiles('**/yarn.lock') }} 73 | restore-keys: | 74 | ${{ runner.os }}-server-yarn- 75 | - name: Install Dependencies 76 | run: yarn install --frozen-lockfile 77 | 78 | - name: Lint 79 | run: yarn lint 80 | 81 | - name: Test 82 | run: yarn test 83 | 84 | - name: Build 85 | run: yarn build 86 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 'deploy' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: deploy 14 | uses: appleboy/ssh-action@v0.1.4 15 | with: 16 | host: ${{ secrets.SSH_HOST }} 17 | username: ${{ secrets.SSH_USERNAME }} 18 | password: ${{ secrets.SSH_PASSWORD }} 19 | port: ${{ secrets.SSH_PORT }} 20 | script: | 21 | cd ~/web12-MOJ 22 | ./deploy.sh 23 | -------------------------------------------------------------------------------- /.github/workflows/scoring-deploy-1.yml: -------------------------------------------------------------------------------- 1 | name: 'scoring-deploy-1' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: scoring-deploy-1 14 | uses: appleboy/ssh-action@v0.1.4 15 | with: 16 | host: ${{ secrets.SC_SSH_HOST_1 }} 17 | username: ${{ secrets.SC_SSH_USERNAME_1 }} 18 | password: ${{ secrets.SC_SSH_PASSWORD_1 }} 19 | port: ${{ secrets.SC_SSH_PORT_1 }} 20 | script: | 21 | cd ~/web12-MOJ 22 | git pull 23 | ./deploy-scoring-server.sh 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 80, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MintChoco Online Judge 2 | 3 | 민트초코 4 | 5 |
6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | ## 📍 프로젝트 간단 소개 31 | 32 | _그동안 알고리즘 문제를 출제해보고 싶었지만, 기회가 없으셨나요?_ 33 | 34 | _그렇다면 **누구나 알고리즘 문제를 출제하고 풀 수 있는 MOJ**에서 그 기회를 잡아보세요!_ 35 | 36 |        [🌐 배포 사이트](https://www.mincho.life/)         |       [🗒️ 팀 Notion](https://dull-smelt-df1.notion.site/Mintchoco-Online-Judge-e2a85b23094949ffa6ee5c1bf0cc326a)        |        37 | [📺 데모 영상](https://youtu.be/Zjw0KJm_lWA) 38 | 39 | ## 📍 주요 기능 40 | 41 | ### 문제 채점 💯 42 | 43 | - 내 결과 및 다른 사람 코드 확인 44 | 45 | ### 문제 출제 🗒️ 46 | 47 | - 문제 내용 작성 48 | - 예제 및 테스트 케이스 등록 49 | - 문제 공개/비공개 설정 50 | 51 | ### 문제 제출 💻 52 | 53 | - Python 코드 제출 54 | 55 | ## 📍 기술적 도전 56 | 57 | 민트초코팀이 가장 중요하게 생각하고 많은 시간을 투자한 서비스는 ✨**채점 서비스**✨입니다. 58 | 59 | ### **_‘어떻게 하면 채점을 순차적으로 빠르게 채점할 수 있을지 ‘_** 60 | 61 | ### **_‘동시에 많은 채점이 몰려도 API 서버에 부하 없이 안정적으로 처리 가능한지 ’_** 62 | 63 | 끊임없이 고민하고 여러 방법을 시도해보고 있으며 아래 문서에서 확인할 수 있습니다. 64 | 65 | - [채점 서비스 발전 과정 요약](https://dull-smelt-df1.notion.site/6869a83fb5644124838e7923558e4e92) 66 | - [PART1. API 통신으로 구현](https://dull-smelt-df1.notion.site/PART1-API-78473310aa9e46f98b2e705492e97b12) 67 | - [PART2. Redis 메시지 큐를 이용한 구현](https://dull-smelt-df1.notion.site/PART2-Redis-89ae23f7c2d14556b4608421164f8ce6) 68 | - [채점 성능 측정](https://dull-smelt-df1.notion.site/09fa1208df60478b95c4e173fa6b7880) 69 | - [실시간 채점 현황](https://dull-smelt-df1.notion.site/95d0bf2a71394124a82057f1a927f087) 70 | - [채점 프로그램 작동 원리](https://dull-smelt-df1.notion.site/c7146f0dc58b4299bdd9ad4c5341f8e2) 71 | 72 | ## 📍 기술 스택 73 | 74 | moj기술스택 75 | 76 | ## 📍 아키텍쳐 77 | 78 | moj아키텍쳐 79 | 채점 모델 아키텍처 80 | 81 | ## 📍 팀원 82 | 83 | | J067 노효주 | J075 박민희 | J138 이민규 | J176 장효석 | 84 | | :------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------: | 85 | | | | | | 86 | | 1114nhj@naver.com | qag331@naver.com | dolphinlmg@naver.com | janghs0604@naver.com | 87 | | [joo-prog](https://github.com/joo-prog) | [Minhee331](https://github.com/Minhee331) | [dolphinlmg](https://github.com/dolphinlmg) | [hyoseok0604](https://github.com/hyoseok0604) | 88 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | -------------------------------------------------------------------------------- /client/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const axiosInstance = axios.create({ 4 | withCredentials: true, 5 | }); 6 | 7 | export default axiosInstance; 8 | -------------------------------------------------------------------------------- /client/components/Editor/wrapperEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Editor, EditorProps } from '@toast-ui/react-editor'; 3 | import '@toast-ui/editor/dist/toastui-editor.css'; 4 | 5 | const WrappedEditor = ( 6 | props: EditorProps & { 7 | forwardedRef: React.LegacyRef; 8 | }, 9 | ) => { 10 | const { forwardedRef } = props; 11 | 12 | return ( 13 | 29 | ); 30 | }; 31 | 32 | export default WrappedEditor; 33 | -------------------------------------------------------------------------------- /client/components/GNB/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { useRouter } from 'next/router'; 3 | 4 | const logoStyle = css` 5 | font-weight: bold; 6 | font-size: 40px; 7 | color: white; 8 | font-family: 'NanumSquareNeoHeavy'; 9 | user-select: none; 10 | cursor: pointer; 11 | margin: 0 15px; 12 | `; 13 | 14 | function Logo() { 15 | const router = useRouter(); 16 | const handleClick = () => { 17 | router.push('/'); 18 | }; 19 | 20 | return ( 21 |
22 | MOJ 23 |
24 | ); 25 | } 26 | 27 | export default Logo; 28 | -------------------------------------------------------------------------------- /client/components/GNB/Menu.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const menuStyle = css` 6 | & span a { 7 | color: white; 8 | text-decoration: none; 9 | margin: 0 5px; 10 | } 11 | `; 12 | 13 | const unSelectedAnchor = css` 14 | color: rgba(255, 255, 255, 0.6) !important; 15 | `; 16 | 17 | interface MenuProps { 18 | isLoggedIn: boolean; 19 | } 20 | 21 | function Menu({ isLoggedIn }: MenuProps) { 22 | const router = useRouter(); 23 | const currentPath = router.pathname; 24 | 25 | return ( 26 |
27 | 28 | 29 | 문제 목록 30 | 31 | 32 | 33 | 37 | 채점 현황 38 | 39 | 40 | {isLoggedIn && ( 41 | 42 | 46 | 문제 출제 47 | 48 | 49 | )} 50 |
51 | ); 52 | } 53 | 54 | export default Menu; 55 | -------------------------------------------------------------------------------- /client/components/GNB/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { useRouter } from 'next/router'; 3 | import axiosInstance from '../../axios'; 4 | import Button from '../common/Button'; 5 | 6 | interface UserInfoProps { 7 | isLoggedIn: boolean; 8 | userName: string; 9 | } 10 | 11 | const userInfoStyle = css` 12 | display: flex; 13 | justify-content: space-around; 14 | align-items: center; 15 | `; 16 | 17 | const userNameStyle = css` 18 | color: white; 19 | `; 20 | 21 | function UserInfo({ isLoggedIn, userName }: UserInfoProps) { 22 | const makeRequestURL = () => { 23 | const requestURL = 'https://github.com/login/oauth/authorize'; 24 | const redirectURL = `${process.env.SERVER_ORIGIN}${process.env.REDIRECT_URL}`; 25 | const clientID = process.env.CLIENT_ID ?? ''; 26 | 27 | const result = new URL(requestURL); 28 | result.searchParams.append('client_id', clientID); 29 | result.searchParams.append('redirect_uri', encodeURI(redirectURL)); 30 | 31 | return result.href; 32 | }; 33 | 34 | const handleClick = async () => { 35 | if (isLoggedIn) { 36 | await axiosInstance.post('/api/users/logout'); 37 | location.href = '/'; 38 | } else { 39 | document.location.href = makeRequestURL(); 40 | } 41 | }; 42 | 43 | return ( 44 |
45 | {isLoggedIn ? userName : ''} 46 | 49 |
50 | ); 51 | } 52 | 53 | export default UserInfo; 54 | -------------------------------------------------------------------------------- /client/components/GNB/index.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { useRouter } from 'next/router'; 3 | import { useEffect, useState } from 'react'; 4 | import axiosInstance from '../../axios'; 5 | import Logo from './Logo'; 6 | import Menu from './Menu'; 7 | import UserInfo from './UserInfo'; 8 | 9 | const gnbStyle = css` 10 | background-color: #3949ab; 11 | width: 100%; 12 | height: 70px; 13 | 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | 23 | z-index: 100; 24 | `; 25 | 26 | function GNB() { 27 | const [isLoggedIn, setLoggedIn] = useState(false); 28 | const [userName, setUserName] = useState(''); 29 | const router = useRouter(); 30 | 31 | useEffect(() => { 32 | (async () => { 33 | try { 34 | const result = (await axiosInstance('/api/users/login-status')).data; 35 | setLoggedIn(true); 36 | setUserName(result.userName); 37 | } catch (error) { 38 | setLoggedIn(false); 39 | } 40 | })(); 41 | }, [router.pathname]); 42 | 43 | return ( 44 |
45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | export default GNB; 53 | -------------------------------------------------------------------------------- /client/components/List/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './list'; 2 | -------------------------------------------------------------------------------- /client/components/List/list.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import Paginator from './paginator'; 3 | import ListRow from './listRow'; 4 | 5 | interface ListProps { 6 | pageCount: number; 7 | currentPage: number; 8 | 9 | data: T[]; 10 | 11 | mapper: ListMapper[]; 12 | 13 | isShowPaginator?: boolean; 14 | 15 | rowHref: (row: T) => string; 16 | pageHref: (page: number) => string; 17 | } 18 | 19 | const style = { 20 | container: css` 21 | display: flex; 22 | flex-direction: column; 23 | border-radius: 8px; 24 | box-shadow: 0px 0px 1.5px 1.5px rgba(0, 0, 0, 0.15); 25 | 26 | & > :not(:last-child) { 27 | border-bottom: 1px solid #e0e0e0; 28 | } 29 | `, 30 | head: css` 31 | display: flex; 32 | font-size: 14px; 33 | font-weight: bold; 34 | color: #636971; 35 | 36 | align-items: center; 37 | `, 38 | flexWeight: (weight: number) => css` 39 | flex: ${weight}; 40 | `, 41 | cell: css` 42 | padding: 20px; 43 | `, 44 | center: css` 45 | text-align: center; 46 | `, 47 | unset: css` 48 | all: unset; 49 | `, 50 | row: css` 51 | display: flex; 52 | font-size: 16px; 53 | font-weight: 400; 54 | color: #636971; 55 | :hover { 56 | cursor: pointer; 57 | } 58 | `, 59 | }; 60 | 61 | function List({ 62 | pageCount, 63 | currentPage, 64 | data, 65 | mapper, 66 | rowHref, 67 | pageHref, 68 | isShowPaginator, 69 | }: ListProps) { 70 | return ( 71 |
72 |
73 | {mapper.map(({ name, weight, style: _style }) => ( 74 |
83 | {name} 84 |
85 | ))} 86 |
87 | {data.map((row, index) => ( 88 | 89 | ))} 90 | {isShowPaginator === true || isShowPaginator === undefined ? ( 91 | 96 | ) : undefined} 97 |
98 | ); 99 | } 100 | 101 | export default List; 102 | -------------------------------------------------------------------------------- /client/components/List/listRow.tsx: -------------------------------------------------------------------------------- 1 | import { css, SerializedStyles } from '@emotion/react'; 2 | import Link from 'next/link'; 3 | import { ReactNode, MouseEvent } from 'react'; 4 | 5 | interface ListRowProps { 6 | row: T; 7 | 8 | mapper: ListMapper[]; 9 | 10 | rowHref: (row: T) => string; 11 | } 12 | 13 | const style = { 14 | flexWeight: (weight: number) => css` 15 | flex: ${weight}; 16 | `, 17 | cell: css` 18 | padding: 20px; 19 | `, 20 | unset: css` 21 | all: unset; 22 | `, 23 | row: css` 24 | display: flex; 25 | font-size: 16px; 26 | font-weight: 400; 27 | color: #636971; 28 | 29 | align-items: center; 30 | :hover { 31 | cursor: pointer; 32 | } 33 | `, 34 | }; 35 | 36 | function ListRow({ rowHref, mapper, row }: ListRowProps) { 37 | return ( 38 | 39 |
40 | {mapper.map( 41 | ({ weight, style: _style, path, format, name, onclick }) => { 42 | const get = ( 43 | style: 44 | | SerializedStyles 45 | | ((row: T) => SerializedStyles) 46 | | undefined, 47 | row: T, 48 | ) => { 49 | if (style === undefined) return undefined; 50 | else if (typeof style === 'function') return style(row); 51 | else return style; 52 | }; 53 | 54 | return ( 55 |
onclick(e, row) : undefined 65 | } 66 | > 67 | { 68 | format 69 | ? format(path ? row[path] : '') 70 | : path 71 | ? (row[path] as ReactNode) 72 | : '' /* TODO: 너 왜 타입에러야. */ 73 | } 74 |
75 | ); 76 | }, 77 | )} 78 |
79 | 80 | ); 81 | } 82 | 83 | export default ListRow; 84 | -------------------------------------------------------------------------------- /client/components/List/paginator.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import Link from 'next/link'; 3 | 4 | interface PaginatorProps { 5 | pageCount: number; 6 | currentPage: number; 7 | href: (page: number) => string; 8 | } 9 | 10 | const style = { 11 | container: css` 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | font-size: 16px; 16 | color: #636971; 17 | padding: 15px; 18 | `, 19 | page: css` 20 | padding: 4px 8px; 21 | border-radius: 2px; 22 | :hover { 23 | cursor: pointer; 24 | } 25 | `, 26 | 'page-active': css` 27 | background-color: #f6f4f3; 28 | `, 29 | active: css` 30 | color: #000000; 31 | font-weight: 500; 32 | :hover { 33 | cursor: pointer; 34 | } 35 | `, 36 | buttonContainer: css` 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | margin: 0px 8px; 41 | `, 42 | unset: css` 43 | all: unset; 44 | `, 45 | disabled: css` 46 | pointer-events: none; 47 | `, 48 | }; 49 | 50 | function Paginator({ pageCount, currentPage, href }: PaginatorProps) { 51 | let start = Math.max(1, currentPage - 2); 52 | let end = Math.min(currentPage + 2, pageCount); 53 | const isPrev = start > 1; 54 | const isNext = end < pageCount; 55 | 56 | if (end - start + 1 < Math.min(5, pageCount)) { 57 | const need = Math.min(5, pageCount) - (end - start + 1); 58 | if (start == 1) end += need; 59 | else start -= need; 60 | } 61 | 62 | const pages = Array.from({ length: end - start + 1 }, (_, i) => start + i); 63 | 64 | return ( 65 |
66 | 70 |
73 | 80 | 87 | 88 |
{'Prev'}
89 |
90 | 91 | {pages.map((page) => ( 92 | 93 |
101 | {page} 102 |
103 | 104 | ))} 105 | 109 |
110 |
{'Next'}
111 | 118 | 125 | 126 |
127 | 128 |
129 | ); 130 | } 131 | 132 | export default Paginator; 133 | -------------------------------------------------------------------------------- /client/components/Modal/DeleteProblemModal.tsx: -------------------------------------------------------------------------------- 1 | import { modal } from '../../styles'; 2 | import Button from '../common/Button'; 3 | 4 | interface DeleteProblemModalProps { 5 | title: string; 6 | handleCancel: () => void; 7 | handleDelete: () => void; 8 | } 9 | 10 | function DeleteProblemModal({ 11 | title, 12 | handleDelete, 13 | handleCancel, 14 | }: DeleteProblemModalProps) { 15 | return ( 16 | <> 17 |
{title}
18 |
문제를 정말 삭제하시겠습니까?
19 |
20 | 23 | 26 |
27 | 28 | ); 29 | } 30 | 31 | export default DeleteProblemModal; 32 | -------------------------------------------------------------------------------- /client/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { css, Global } from '@emotion/react'; 2 | import React from 'react'; 3 | import modal from '../../styles/modal'; 4 | import { CloseSvg } from '../svgs'; 5 | 6 | interface ModalCloseState { 7 | isShowModal: false; 8 | } 9 | 10 | interface ModalOpenstate { 11 | isShowModal: true; 12 | data: T; 13 | } 14 | 15 | interface ModalProps { 16 | isShow: boolean; 17 | data: T; 18 | render: (data: T) => React.ReactNode; 19 | setIsShowModal: React.Dispatch>; 20 | } 21 | 22 | function Modal({ data, render, setIsShowModal, isShow }: ModalProps) { 23 | if (!isShow) return <>; 24 | 25 | return ( 26 | <> 27 | 34 |
35 |
36 |
setIsShowModal(false)} 39 | > 40 | 41 |
42 |
{render(data)}
43 |
44 |
45 | 46 | ); 47 | } 48 | 49 | export default Modal; 50 | -------------------------------------------------------------------------------- /client/components/Problem/CodeContainer.tsx: -------------------------------------------------------------------------------- 1 | import Editor from '@monaco-editor/react'; 2 | import { css } from '@emotion/react'; 3 | import { Dispatch, SetStateAction, useEffect } from 'react'; 4 | 5 | interface CodeContainerProps { 6 | setCode: Dispatch>; 7 | } 8 | 9 | function CodeContainer({ setCode }: CodeContainerProps) { 10 | const handleChange = (newCode: string | undefined) => { 11 | if (!newCode) return; 12 | setCode(newCode); 13 | }; 14 | 15 | const heightStyle = (height: string) => css` 16 | height: ${height}; 17 | `; 18 | 19 | const languageHeaderStyle = css` 20 | display: flex; 21 | align-items: center; 22 | padding-left: 10px; 23 | font-weight: bold; 24 | `; 25 | 26 | const defaultCode = `# print('Hello World!')\n`; 27 | 28 | useEffect(() => { 29 | setCode(defaultCode); 30 | }, []); 31 | 32 | return ( 33 |
34 |
Python
35 |
36 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default CodeContainer; 47 | -------------------------------------------------------------------------------- /client/components/Problem/ProblemContainer.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import '@toast-ui/editor/dist/toastui-editor-viewer.css'; 3 | import { Viewer } from '@toast-ui/react-editor'; 4 | import { MdContentCopy } from 'react-icons/md'; 5 | 6 | interface ProblemContainerProps { 7 | problem: Problem; 8 | } 9 | 10 | const style = { 11 | limit: css` 12 | width: 100%; 13 | 14 | display: flex; 15 | justify-content: flex-start; 16 | 17 | & > span { 18 | font-weight: bold; 19 | margin-right: 10px; 20 | } 21 | `, 22 | problemContainer: css` 23 | margin-left: 10px; 24 | overflow: auto; 25 | height: calc(100% - 96px); 26 | `, 27 | header: css` 28 | padding: 0 10px; 29 | padding-bottom: 15px; 30 | `, 31 | h2: css` 32 | margin: 15px 0; 33 | `, 34 | h3: css` 35 | color: #00227b; 36 | display: inline; 37 | `, 38 | exampleContainer: css` 39 | display: flex; 40 | `, 41 | example: css` 42 | width: 50%; 43 | `, 44 | exampleText: css` 45 | background-color: rgba(0, 0, 0, 0.1); 46 | margin-right: 25px; 47 | padding: 5px; 48 | border-radius: 5px; 49 | `, 50 | exampleTitleContainer: css` 51 | display: flex; 52 | flex-direction: row; 53 | align-content: center; 54 | `, 55 | copyText: css` 56 | color: #00227b; 57 | :hover { 58 | cursor: pointer; 59 | } 60 | font-size: 0.8em; 61 | margin: auto 4px; 62 | `, 63 | input: css` 64 | border: none; 65 | background-color: #eaeaea; 66 | border-radius: 3px; 67 | padding: 10px 15px; 68 | overflow-x: auto; 69 | margin-right: 10px; 70 | font-family: 'NanumSquare'; 71 | `, 72 | 73 | copy: css` 74 | :hover { 75 | cursor: pointer; 76 | font-size: 13px; 77 | } 78 | color: #00227b; 79 | font-size: 12px; 80 | `, 81 | }; 82 | 83 | function ProblemContainer({ problem }: ProblemContainerProps) { 84 | async function copyExampleToClipboard(example: string) { 85 | await navigator.clipboard.writeText(example); 86 | } 87 | 88 | return ( 89 | <> 90 |
91 |

92 | {problem.id}번 - {problem.title} 93 |

94 |
95 | 시간제한 {problem.timeLimit}ms 96 | 메모리 제한 {problem.memoryLimit}MB 97 |
98 |
99 |
100 |

문제

101 | 102 |

입력

103 | 104 |

출력

105 | 106 |

제한

107 | 108 | {problem.examples.map(({ input, output }, idx) => { 109 | return ( 110 |
111 |
112 |

예제 입력 {idx + 1}

113 | copyExampleToClipboard(input)} 116 | /> 117 |
{input}
118 |
119 |
120 |

예제 출력 {idx + 1}

121 |
{output}
122 |
123 |
124 | ); 125 | })} 126 |

입출력 예제 설명

127 | 128 |
129 | 130 | ); 131 | } 132 | 133 | export default ProblemContainer; 134 | -------------------------------------------------------------------------------- /client/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { ReactNode } from 'react'; 3 | 4 | interface ButtonProps { 5 | children: ReactNode; 6 | onClick?: React.MouseEventHandler; 7 | style?: 'default' | 'cancel'; 8 | minWidth?: string; 9 | minHeight?: string; 10 | } 11 | 12 | const btnStyle = { 13 | default: css` 14 | background-color: #6f74dd; 15 | border-radius: 1rem; 16 | border: 1px solid #6f74dd; 17 | padding: 5px 15px; 18 | color: white; 19 | font-size: 14px; 20 | margin: 0 15px; 21 | transition: all 0.2s; 22 | user-select: none; 23 | text-align: center; 24 | 25 | &:hover { 26 | cursor: pointer; 27 | box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.2); 28 | } 29 | `, 30 | cancel: css` 31 | background-color: #ffffff; 32 | border-radius: 1rem; 33 | border: 1px solid #6f74dd; 34 | padding: 5px 15px; 35 | color: #6f74dd; 36 | font-size: 14px; 37 | margin: 0 15px; 38 | transition: all 0.2s; 39 | user-select: none; 40 | text-align: center; 41 | 42 | &:hover { 43 | cursor: pointer; 44 | box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.2); 45 | } 46 | `, 47 | minHeight: (height: string) => css` 48 | min-height: ${height}; 49 | `, 50 | minWidth: (width: string) => css` 51 | min-width: ${width}; 52 | `, 53 | }; 54 | 55 | function Button({ 56 | onClick, 57 | children, 58 | minWidth, 59 | minHeight, 60 | style = 'default', 61 | }: ButtonProps) { 62 | return ( 63 |
71 | {children} 72 |
73 | ); 74 | } 75 | 76 | export default Button; 77 | -------------------------------------------------------------------------------- /client/components/common/IOList/InputContainer.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | interface InputContaierProps { 4 | title: string; 5 | value: string; 6 | setValue: (value: string) => void; 7 | } 8 | 9 | const style = { 10 | container: css` 11 | display: flex; 12 | flex-direction: column; 13 | padding: 10px; 14 | width: 50%; 15 | max-width: 50%; 16 | `, 17 | input: css` 18 | border: none; 19 | background-color: #eaeaea; 20 | border-radius: 3px; 21 | min-height: 25px; 22 | height: 100px; 23 | appearance: none; 24 | padding: 10px 15px; 25 | resize: none; 26 | &:focus { 27 | outline: none; 28 | } 29 | `, 30 | title: css` 31 | font-weight: bold; 32 | padding-bottom: 10px; 33 | `, 34 | }; 35 | 36 | function InputContainer({ title, value, setValue }: InputContaierProps) { 37 | return ( 38 |
39 |
{title}
40 |