├── be ├── .gitignore ├── algo-with-me-score │ ├── .dockerignore │ ├── tsconfig.build.json │ ├── .prettierrc │ ├── src │ │ ├── score │ │ │ ├── entities │ │ │ │ ├── competition.enums.ts │ │ │ │ ├── competition.problem.entity.ts │ │ │ │ ├── competition.participant.entity.ts │ │ │ │ ├── problem.entity.ts │ │ │ │ ├── user.entity.ts │ │ │ │ ├── submission.entity.ts │ │ │ │ └── competition.entity.ts │ │ │ ├── interfaces │ │ │ │ └── coderun-response.interface.ts │ │ │ ├── dtos │ │ │ │ ├── message-queue-item.dto.ts │ │ │ │ └── score-result.dto.ts │ │ │ └── score.module.ts │ │ ├── main.ts │ │ ├── app.service.ts │ │ └── app.controller.ts │ ├── nest-cli.json │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── Dockerfile │ ├── .gitignore │ ├── tsconfig.json │ └── .eslintrc.js ├── algo-with-me-api │ ├── .dockerignore │ ├── src │ │ ├── exception │ │ │ ├── exception.enum.ts │ │ │ ├── service.exception.ts │ │ │ └── service.exception.filter.ts │ │ ├── auth │ │ │ ├── dto │ │ │ │ └── auth.token.payload.dto.ts │ │ │ ├── auth.module.ts │ │ │ ├── services │ │ │ │ └── auth.service.ts │ │ │ └── controllers │ │ │ │ └── auth.controller.ts │ │ ├── competition │ │ │ ├── competition.enums.ts │ │ │ ├── dto │ │ │ │ ├── problem.simple.response.dto.ts │ │ │ │ ├── competition.is.joinable.dto.ts │ │ │ │ ├── score-result.dto.ts │ │ │ │ ├── create-submission.dto.ts │ │ │ │ ├── create-problem.dto.ts │ │ │ │ ├── problem.response.dto.ts │ │ │ │ └── competition.problem.response.dto.ts │ │ │ ├── tem.consumer.ts │ │ │ └── entities │ │ │ │ ├── competition.problem.entity.ts │ │ │ │ ├── competition.participant.entity.ts │ │ │ │ ├── problem.entity.ts │ │ │ │ ├── submission.entity.ts │ │ │ │ └── competition.entity.ts │ │ ├── user │ │ │ ├── decorators │ │ │ │ └── user.decorators.ts │ │ │ ├── dto │ │ │ │ └── user.response.dto.ts │ │ │ ├── user.module.ts │ │ │ ├── services │ │ │ │ └── user.service.ts │ │ │ └── entities │ │ │ │ └── user.entity.ts │ │ ├── dashboard │ │ │ ├── entities │ │ │ │ └── dashboard.entity.ts │ │ │ └── dashboard.module.ts │ │ ├── log │ │ │ └── logger.middleware.ts │ │ └── main.ts │ ├── tsconfig.build.json │ ├── .prettierrc │ ├── nest-cli.json │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── Dockerfile │ ├── .gitignore │ ├── tsconfig.json │ └── .eslintrc.js ├── algo-with-me-docker │ ├── build.sh │ ├── docker-sh │ │ ├── createNetwork.sh │ │ ├── prune.sh │ │ ├── stop.sh │ │ └── run.sh │ ├── .dockerignore │ ├── node-sh │ │ ├── time │ │ ├── runJs.sh │ │ └── run.sh │ ├── .prettierrc │ ├── package.json │ ├── Dockerfile │ ├── tsconfig.json │ ├── src │ │ └── app.js │ └── .eslintrc.js ├── push-all ├── build-api ├── build-score ├── build-docker ├── run-all ├── run-all-background ├── run-api ├── run-score ├── run-docker └── build-all ├── fe ├── _redirects ├── .npmrc ├── src │ ├── hooks │ │ ├── competitionDetail │ │ │ ├── types.ts │ │ │ ├── index.ts │ │ │ └── useCompetitionRerenderState.ts │ │ ├── simulation │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── competition │ │ │ ├── index.ts │ │ │ └── useCompetition.ts │ │ ├── problem │ │ │ ├── index.ts │ │ │ ├── useProblemList.ts │ │ │ ├── useCompetitionProblemList.ts │ │ │ └── useCompetitionProblem.ts │ │ ├── dashboard │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── useDashboardRenderState.ts │ │ │ └── useRemainingTimeCounter.ts │ │ ├── submission │ │ │ └── useSubmitSolution.ts │ │ └── login │ │ │ └── useAuth.ts │ ├── vite-env.d.ts │ ├── utils │ │ ├── copy │ │ │ ├── index.ts │ │ │ └── __tests__ │ │ │ │ └── copy.spec.ts │ │ ├── localStorage │ │ │ └── index.ts │ │ ├── unit │ │ │ ├── index.ts │ │ │ └── __tests__ │ │ │ │ └── byteToKb.spec.ts │ │ ├── api │ │ │ └── index.ts │ │ ├── array │ │ │ ├── index.ts │ │ │ └── __tests__ │ │ │ │ └── range.spec.ts │ │ ├── secToTime.ts │ │ ├── type │ │ │ ├── __tests__ │ │ │ │ ├── isNil.spec.ts │ │ │ │ ├── isNumber.ts │ │ │ │ └── isFunction.spec.ts │ │ │ └── index.ts │ │ ├── date │ │ │ ├── __tests__ │ │ │ │ ├── formatDate.spec.ts │ │ │ │ └── toLocalDate.spec.ts │ │ │ └── index.ts │ │ ├── observer │ │ │ ├── index.ts │ │ │ └── __tests__ │ │ │ │ └── observer.spec.ts │ │ └── socket │ │ │ └── index.ts │ ├── components │ │ ├── Common │ │ │ ├── Card │ │ │ │ ├── index.ts │ │ │ │ └── Card.tsx │ │ │ ├── Text │ │ │ │ └── index.ts │ │ │ ├── Modal │ │ │ │ ├── index.ts │ │ │ │ ├── ModalContext.ts │ │ │ │ └── ModalProvider.tsx │ │ │ ├── HStack │ │ │ │ ├── index.ts │ │ │ │ └── HStack.tsx │ │ │ ├── VStack │ │ │ │ ├── index.ts │ │ │ │ └── VStack.tsx │ │ │ ├── Space.tsx │ │ │ ├── Logo.tsx │ │ │ ├── Socket │ │ │ │ ├── SocketContext.ts │ │ │ │ └── SocketProvider.tsx │ │ │ ├── index.ts │ │ │ ├── Loading.tsx │ │ │ ├── Link.tsx │ │ │ ├── Chip.tsx │ │ │ └── BreadCrumb.tsx │ │ ├── Layout │ │ │ ├── index.ts │ │ │ ├── CompetitionPageLayout.tsx │ │ │ └── PageLayout.tsx │ │ ├── types.ts │ │ ├── CompetitionDetail │ │ │ ├── styles │ │ │ │ └── styles.ts │ │ │ ├── CompetitionDetailInfo.tsx │ │ │ ├── Buttons │ │ │ │ ├── JoinCompetitionButton.tsx │ │ │ │ └── EnterCompetitionButton.tsx │ │ │ └── CompetitionDetailContent.tsx │ │ ├── Dashboard │ │ │ ├── Buttons │ │ │ │ └── OpenDashboardModalButton.tsx │ │ │ ├── DashboardStatus.tsx │ │ │ └── DashboardLoading.tsx │ │ ├── Auth │ │ │ ├── AuthContext.ts │ │ │ └── AuthProvider.tsx │ │ ├── Main │ │ │ └── Buttons │ │ │ │ ├── ViewDashboardButton.tsx │ │ │ │ └── GoToCreateCompetitionLink.tsx │ │ ├── Submission │ │ │ ├── Connecting.tsx │ │ │ ├── types.ts │ │ │ ├── SubmissionResult.tsx │ │ │ ├── ResultTotalInfo.tsx │ │ │ └── SubmissionButton.tsx │ │ ├── Simulation │ │ │ └── SimulationExecButton.tsx │ │ ├── Competition │ │ │ ├── CompetitionHeader.tsx │ │ │ └── CompetitionProblemSelector.tsx │ │ ├── Problem │ │ │ ├── ProblemViewer.tsx │ │ │ ├── ProblemHeader.tsx │ │ │ ├── SelectableProblemList.tsx │ │ │ └── SelectedProblemList.tsx │ │ ├── UserValidator │ │ │ └── index.tsx │ │ ├── Editor │ │ │ └── Editor.tsx │ │ └── Login │ │ │ └── index.tsx │ ├── App.tsx │ ├── apis │ │ ├── competitionList │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── auth │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── dashboard │ │ │ ├── type.ts │ │ │ └── index.ts │ │ ├── joinCompetition │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── competitions │ │ │ ├── types.ts │ │ │ └── index.ts │ │ └── problems │ │ │ ├── types.ts │ │ │ └── index.ts │ ├── __mocks__ │ │ ├── index.ts │ │ └── algoWithMeApi.ts │ ├── constants │ │ └── index.ts │ ├── types │ │ └── index.ts │ ├── modules │ │ └── evaluator │ │ │ ├── createEvalMessage.ts │ │ │ ├── types.ts │ │ │ ├── createEvaluator.ts │ │ │ ├── eval.worker.ts │ │ │ └── index.ts │ ├── main.tsx │ ├── index.css │ ├── pages │ │ ├── CompetitionDetailPage.tsx │ │ └── LoginPage.tsx │ └── router.tsx ├── netlify.toml ├── public │ ├── algo.ico │ ├── algo.png │ └── vite.svg ├── styled-system │ ├── css │ │ ├── index.mjs │ │ ├── index.d.ts │ │ ├── sva.d.ts │ │ ├── cva.d.ts │ │ ├── cx.d.ts │ │ ├── cx.mjs │ │ ├── css.d.ts │ │ └── sva.mjs │ ├── types │ │ ├── parts.d.ts │ │ ├── index.d.ts │ │ └── global.d.ts │ ├── tokens │ │ ├── index.d.ts │ │ ├── keyframes.css │ │ └── index.css │ ├── patterns │ │ ├── box.mjs │ │ ├── visually-hidden.mjs │ │ ├── link-box.mjs │ │ ├── container.mjs │ │ ├── center.mjs │ │ ├── spacer.mjs │ │ ├── hstack.mjs │ │ ├── vstack.mjs │ │ ├── square.mjs │ │ ├── stack.mjs │ │ ├── circle.mjs │ │ ├── bleed.mjs │ │ ├── index.d.ts │ │ ├── wrap.mjs │ │ ├── index.mjs │ │ ├── link-overlay.mjs │ │ ├── box.d.ts │ │ ├── flex.mjs │ │ ├── link-box.d.ts │ │ ├── circle.d.ts │ │ ├── container.d.ts │ │ ├── square.d.ts │ │ ├── center.d.ts │ │ ├── spacer.d.ts │ │ ├── link-overlay.d.ts │ │ ├── bleed.d.ts │ │ ├── hstack.d.ts │ │ ├── vstack.d.ts │ │ ├── visually-hidden.d.ts │ │ ├── aspect-ratio.d.ts │ │ ├── grid-item.mjs │ │ ├── stack.d.ts │ │ ├── wrap.d.ts │ │ ├── grid.d.ts │ │ ├── divider.mjs │ │ ├── grid.mjs │ │ ├── divider.d.ts │ │ ├── grid-item.d.ts │ │ ├── flex.d.ts │ │ ├── aspect-ratio.mjs │ │ ├── float.d.ts │ │ └── float.mjs │ └── global.css ├── postcss.config.cjs ├── .prettierrc ├── tsconfig.node.json ├── .gitignore ├── index.html ├── vite.config.ts ├── tsconfig.json ├── .eslintrc.cjs └── README.md ├── img ├── semina.jpg ├── algowithme.png ├── go_to_docs.png ├── go_to_service.png └── mini_develop_semina.jpg └── .github ├── ISSUE_TEMPLATE ├── 🚀-be-feature.md ├── 🚀-fe-feature.md ├── 🛠-be-fix.md └── 🛠-fe-fix.md └── workflows └── main.yml /be/.gitignore: -------------------------------------------------------------------------------- 1 | ./.env 2 | /.env 3 | -------------------------------------------------------------------------------- /fe/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /fe/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /fe/src/hooks/competitionDetail/types.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /be/algo-with-me-score/.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .env -------------------------------------------------------------------------------- /be/algo-with-me-api/.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .env 4 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/build.sh: -------------------------------------------------------------------------------- 1 | sudo docker build . -t algo-with-me-docker:latest -------------------------------------------------------------------------------- /fe/src/hooks/competitionDetail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCompetitionRerenderState'; 2 | -------------------------------------------------------------------------------- /fe/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 5 | -------------------------------------------------------------------------------- /fe/src/hooks/simulation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useSimulation'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /img/semina.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/img/semina.jpg -------------------------------------------------------------------------------- /be/algo-with-me-docker/docker-sh/createNetwork.sh: -------------------------------------------------------------------------------- 1 | sudo docker network create --driver bridge isolatedNetwork -------------------------------------------------------------------------------- /fe/public/algo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/fe/public/algo.ico -------------------------------------------------------------------------------- /fe/public/algo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/fe/public/algo.png -------------------------------------------------------------------------------- /img/algowithme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/img/algowithme.png -------------------------------------------------------------------------------- /img/go_to_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/img/go_to_docs.png -------------------------------------------------------------------------------- /fe/src/hooks/competition/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCompetitionForm'; 2 | export * from './useCompetition'; 3 | -------------------------------------------------------------------------------- /fe/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line spaced-comment 2 | /// 3 | -------------------------------------------------------------------------------- /img/go_to_service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/img/go_to_service.png -------------------------------------------------------------------------------- /fe/src/utils/copy/index.ts: -------------------------------------------------------------------------------- 1 | export function deepCopy(value: T): T { 2 | return structuredClone(value); 3 | } 4 | -------------------------------------------------------------------------------- /fe/src/components/Common/Card/index.ts: -------------------------------------------------------------------------------- 1 | export { Card } from './Card'; 2 | export type { Props as CardProps } from './Card'; 3 | -------------------------------------------------------------------------------- /fe/src/components/Common/Text/index.ts: -------------------------------------------------------------------------------- 1 | export { Text } from './Text'; 2 | export type { Props as TextProps } from './Text'; 3 | -------------------------------------------------------------------------------- /img/mini_develop_semina.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/img/mini_develop_semina.jpg -------------------------------------------------------------------------------- /fe/src/components/Common/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export { Modal } from './Modal'; 2 | export type { Props as ModalProps } from './Modal'; 3 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/.dockerignore: -------------------------------------------------------------------------------- 1 | node-modules/ 2 | dist/ 3 | build.sh 4 | .eslintrc.js 5 | .prettierrc 6 | .gitignore 7 | tsconfig.json -------------------------------------------------------------------------------- /fe/src/components/Common/HStack/index.ts: -------------------------------------------------------------------------------- 1 | export { HStack } from './HStack'; 2 | export type { Props as HStackProps } from './HStack'; 3 | -------------------------------------------------------------------------------- /fe/src/components/Common/VStack/index.ts: -------------------------------------------------------------------------------- 1 | export { VStack } from './VStack'; 2 | export type { Props as VStackProps } from './VStack'; 3 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/node-sh/time: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/web12-algo-with-me/HEAD/be/algo-with-me-docker/node-sh/time -------------------------------------------------------------------------------- /be/algo-with-me-api/src/exception/exception.enum.ts: -------------------------------------------------------------------------------- 1 | export const ERROR_CODE = { 2 | NOT_FOUND: 404, 3 | BAD_REQUEST: 400, 4 | } as const; 5 | -------------------------------------------------------------------------------- /be/push-all: -------------------------------------------------------------------------------- 1 | docker login 2 | docker push yechan/algo-with-me-api 3 | docker push yechan/algo-with-me-score 4 | docker push yechan/algo-with-me-docker -------------------------------------------------------------------------------- /fe/styled-system/css/index.mjs: -------------------------------------------------------------------------------- 1 | export * from './css.mjs'; 2 | export * from './cx.mjs'; 3 | export * from './cva.mjs'; 4 | export * from './sva.mjs'; -------------------------------------------------------------------------------- /be/algo-with-me-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /fe/src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { CompetitionPageLayout } from './CompetitionPageLayout'; 2 | export { PageLayout } from './PageLayout'; 3 | -------------------------------------------------------------------------------- /fe/src/components/types.ts: -------------------------------------------------------------------------------- 1 | export const THEME = ['success', 'danger', 'warning', 'info'] as const; 2 | export type Theme = (typeof THEME)[number]; 3 | -------------------------------------------------------------------------------- /be/algo-with-me-score/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /fe/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | plugins: { 4 | '@pandacss/dev/postcss': {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /fe/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /fe/src/utils/localStorage/index.ts: -------------------------------------------------------------------------------- 1 | export function save(key: string, origin: unknown) { 2 | localStorage.setItem(key, JSON.stringify(origin)); 3 | } 4 | -------------------------------------------------------------------------------- /fe/src/utils/unit/index.ts: -------------------------------------------------------------------------------- 1 | const KB_BY_BYTES = 1024; 2 | 3 | export function byteToKB(byte: number) { 4 | return Math.floor(byte / KB_BY_BYTES); 5 | } 6 | -------------------------------------------------------------------------------- /fe/styled-system/css/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export * from './css'; 3 | export * from './cx'; 4 | export * from './cva'; 5 | export * from './sva'; -------------------------------------------------------------------------------- /fe/src/hooks/problem/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCompetitionProblemList'; 2 | export * from './useCompetitionProblem'; 3 | export * from './useProblemList'; 4 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/docker-sh/prune.sh: -------------------------------------------------------------------------------- 1 | sudo docker container stop algo-with-me-docker 2 | sudo docker container prune --force 3 | sudo docker image prune --force 4 | -------------------------------------------------------------------------------- /fe/styled-system/css/sva.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SlotRecipeCreatorFn } from '../types/recipe'; 3 | 4 | export declare const sva: SlotRecipeCreatorFn -------------------------------------------------------------------------------- /fe/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /fe/styled-system/types/parts.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export interface Part { 3 | selector: string 4 | } 5 | 6 | export interface Parts { 7 | [key: string]: Part 8 | } 9 | -------------------------------------------------------------------------------- /be/algo-with-me-api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /be/algo-with-me-score/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /fe/src/apis/competitionList/types.ts: -------------------------------------------------------------------------------- 1 | export type Competition = { 2 | id: number; 3 | name: string; 4 | startsAt: string; 5 | endsAt: string; 6 | maxParticipants: number; 7 | }; 8 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/docker-sh/stop.sh: -------------------------------------------------------------------------------- 1 | echo "stopped container: " 2 | sudo docker container stop algo-with-me-docker 3 | echo "removed container: " 4 | sudo docker container rm algo-with-me-docker -------------------------------------------------------------------------------- /fe/src/hooks/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './useParticipantDashboard'; 3 | export * from './useDashboardRenderState'; 4 | export * from './useRemainingTimeCounter'; 5 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "express": "^4.18.2" 5 | }, 6 | "devDependencies": { 7 | "@types/express": "^4.17.21" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/auth/dto/auth.token.payload.dto.ts: -------------------------------------------------------------------------------- 1 | export class AuthTokenPayloadDto { 2 | sub: string; 3 | 4 | nickname: string; 5 | 6 | iat: number; 7 | 8 | exp: number; 9 | } 10 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/competition.enums.ts: -------------------------------------------------------------------------------- 1 | export const RESULT = { 2 | PROGRESS: '처리중', 3 | CORRECT: '정답입니다', 4 | WRONG: '오답입니다', 5 | TIMEOUT: '시간초과', 6 | OOM: '메모리초과', 7 | } as const; 8 | -------------------------------------------------------------------------------- /fe/src/__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | import { algoWithMeApi } from './algoWithMeApi'; 2 | import { setupWorker } from 'msw/browser'; 3 | 4 | const worker = setupWorker(...algoWithMeApi); 5 | 6 | export default worker; 7 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/competition.enums.ts: -------------------------------------------------------------------------------- 1 | export const RESULT = { 2 | PROGRESS: '처리중', 3 | CORRECT: '정답입니다', 4 | WRONG: '오답입니다', 5 | TIMEOUT: '시간초과', 6 | OOM: '메모리초과', 7 | } as const; 8 | -------------------------------------------------------------------------------- /fe/src/apis/auth/types.ts: -------------------------------------------------------------------------------- 1 | interface TokenValidResponse { 2 | id: number; 3 | email: string; 4 | nickname: string; 5 | createdAt: string; 6 | updatedAt: string; 7 | } 8 | export { type TokenValidResponse }; 9 | -------------------------------------------------------------------------------- /fe/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const SITE = { 2 | NAME: 'Algo With Me', 3 | PAGE_DESCRIPTION: '누구나 만들고 참여할 수 있는 알고리즘 대회', 4 | }; 5 | 6 | export const ROUTE = { 7 | DASHBOARD: '/competition/dashboard', 8 | }; 9 | -------------------------------------------------------------------------------- /fe/src/components/Common/Space.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | export function Space() { 4 | return
; 5 | } 6 | 7 | const style = css({ 8 | flexGrow: '1', 9 | }); 10 | -------------------------------------------------------------------------------- /fe/styled-system/css/cva.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { RecipeCreatorFn } from '../types/recipe'; 3 | 4 | export declare const cva: RecipeCreatorFn 5 | 6 | export type { RecipeVariantProps } from '../types/recipe'; -------------------------------------------------------------------------------- /fe/styled-system/css/cx.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | type Argument = string | boolean | null | undefined 3 | 4 | /** Conditionally join classNames into a single string */ 5 | export declare function cx(...args: Argument[]): string -------------------------------------------------------------------------------- /be/algo-with-me-api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /be/algo-with-me-score/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fe/src/apis/dashboard/type.ts: -------------------------------------------------------------------------------- 1 | import type { Dashboard } from '@/hooks/dashboard'; 2 | 3 | export type DashboardData = { 4 | competitionId: number; 5 | email: string; 6 | }; 7 | 8 | export type FetchDashboardResponse = Dashboard; 9 | -------------------------------------------------------------------------------- /fe/src/utils/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosError } from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: import.meta.env.VITE_API_URL, 5 | }); 6 | 7 | export default api; 8 | export { AxiosError as NetworkError }; 9 | -------------------------------------------------------------------------------- /fe/src/utils/array/index.ts: -------------------------------------------------------------------------------- 1 | export const range = (start: number, end: number, step: number = 1) => { 2 | const length = Math.floor((end - start) / step); 3 | 4 | return Array.from({ length }, (_, i) => start + step * i); 5 | }; 6 | -------------------------------------------------------------------------------- /fe/styled-system/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import './global.d.ts' 3 | export * from './conditions'; 4 | export * from './pattern'; 5 | export * from './recipe'; 6 | export * from './system-types'; 7 | export * from './style-props'; -------------------------------------------------------------------------------- /be/build-api: -------------------------------------------------------------------------------- 1 | docker compose down 2 | docker container stop algo-with-me-api 2>/dev/null 3 | docker container rm algo-with-me-api 2>/dev/null 4 | docker image rm algo-with-me-api 2>/dev/null 5 | cd algo-with-me-api/ 6 | docker build -t algo-with-me-api . -------------------------------------------------------------------------------- /fe/src/apis/joinCompetition/types.ts: -------------------------------------------------------------------------------- 1 | export type CompetitionApiData = { 2 | id: number; 3 | token: string | null; 4 | }; 5 | 6 | export type FetchIsCompetitionJoinableResponse = { 7 | isJoinable: boolean; 8 | message: string; 9 | }; 10 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/interfaces/coderun-response.interface.ts: -------------------------------------------------------------------------------- 1 | interface ICoderunResponse { 2 | result: 'SUCCESS' | string; 3 | competitionId: number; 4 | userId: number; 5 | problemId: number; 6 | } 7 | 8 | export default ICoderunResponse; 9 | -------------------------------------------------------------------------------- /be/algo-with-me-api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /be/build-score: -------------------------------------------------------------------------------- 1 | docker compose down 2 | docker container stop algo-with-me-score 2>/dev/null 3 | docker container rm algo-with-me-score 2>/dev/null 4 | docker image rm algo-with-me-score 2>/dev/null 5 | cd algo-with-me-score/ 6 | docker build -t algo-with-me-score . -------------------------------------------------------------------------------- /be/algo-with-me-score/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /be/build-docker: -------------------------------------------------------------------------------- 1 | docker compose down 2 | docker container stop algo-with-me-docker 2>/dev/null 3 | docker container rm algo-with-me-docker 2>/dev/null 4 | docker image rm algo-with-me-docker 2>/dev/null 5 | cd algo-with-me-docker/ 6 | docker build -t algo-with-me-docker . -------------------------------------------------------------------------------- /fe/styled-system/tokens/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Token } from './tokens'; 3 | 4 | export declare const token: { 5 | (path: Token, fallback?: string): string 6 | var: (path: Token, fallback?: string) => string 7 | } 8 | 9 | export * from './tokens'; -------------------------------------------------------------------------------- /fe/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /fe/src/types/index.ts: -------------------------------------------------------------------------------- 1 | type Dictionary = { [key: string]: unknown }; 2 | 3 | type JSONType = 4 | | string 5 | | number 6 | | boolean 7 | | null 8 | | undefined 9 | | JSONType[] 10 | | { [key: string]: JSONType }; 11 | 12 | export type { Dictionary, JSONType }; 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🚀-be-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 BE Feature" 3 | about: 새 기능 (BE) 4 | title: "\U0001F680 " 5 | labels: BE 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 구현해야하는 기능 11 | > 이 기능을 구현하는데 필요한 TODO 리스트를 명시합니다. 12 | 13 | ## 관련 링크 14 | > 회의록, 관련 기술 링크, 테크 스팩 등을 명시합니다. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🚀-fe-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 FE Feature" 3 | about: 새 기능 (FE) 4 | title: "\U0001F680 " 5 | labels: FE 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 구현해야하는 기능 11 | > 이 기능을 구현하는데 필요한 TODO 리스트를 명시합니다. 12 | 13 | ## 관련 링크 14 | > 회의록, 관련 기술 링크, 테크 스팩 등을 명시합니다. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🛠-be-fix.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F6E0 BE Fix" 3 | about: 버그 수정 (BE) 4 | title: "\U0001F6E0 " 5 | labels: BE 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 버그 설명 11 | 12 | ## 예상하는 해결 방법 13 | 14 | ## 버그 재현 방법 15 | > 스크린 샷 등을 첨부해 버그를 어떻게 재현하는지 설명해 주세요 16 | 17 | ## 관련 링크 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🛠-fe-fix.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F6E0 FE Fix" 3 | about: 버그 수정 (FE) 4 | title: "\U0001F6E0 " 5 | labels: FE 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 버그 설명 11 | 12 | ## 예상하는 해결 방법 13 | 14 | ## 버그 재현 방법 15 | > 스크린 샷 등을 첨부해 버그를 어떻게 재현하는지 설명해 주세요 16 | 17 | ## 관련 링크 18 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/dtos/message-queue-item.dto.ts: -------------------------------------------------------------------------------- 1 | export class MessageQueueItemDto { 2 | constructor(submissionId: number, socketId: string) { 3 | this.submissionId = submissionId; 4 | this.socketId = socketId; 5 | } 6 | 7 | submissionId: number; 8 | socketId: string; 9 | } 10 | -------------------------------------------------------------------------------- /fe/styled-system/css/cx.mjs: -------------------------------------------------------------------------------- 1 | function cx() { 2 | let str = '', 3 | i = 0, 4 | arg 5 | 6 | for (; i < arguments.length; ) { 7 | if ((arg = arguments[i++]) && typeof arg === 'string') { 8 | str && (str += ' ') 9 | str += arg 10 | } 11 | } 12 | return str 13 | } 14 | 15 | export { cx } -------------------------------------------------------------------------------- /fe/src/components/CompetitionDetail/styles/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | const buttonContainerStyle = css({ 4 | justifyContent: 'flex-end', 5 | gap: '10px', 6 | marginTop: '19px', 7 | alignItems: 'center', 8 | marginBottom: '1rem', 9 | }); 10 | 11 | export { buttonContainerStyle }; 12 | -------------------------------------------------------------------------------- /be/algo-with-me-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | RUN npm install -g pnpm 4 | 5 | RUN groupadd -g 1001 be \ 6 | && useradd -r -m -u 1001 -g be be 7 | USER be 8 | 9 | WORKDIR /algo-with-me-api 10 | COPY --chown=be:be ./ ./ 11 | 12 | RUN pnpm install 13 | 14 | EXPOSE 3000 15 | 16 | CMD ["pnpm", "run", "start:dev"] 17 | -------------------------------------------------------------------------------- /be/algo-with-me-score/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | RUN npm install -g pnpm 4 | 5 | RUN groupadd -g 1001 be \ 6 | && useradd -r -m -u 1001 -g be be 7 | USER be 8 | 9 | WORKDIR /algo-with-me-score 10 | COPY --chown=be:be ./ ./ 11 | 12 | RUN pnpm install 13 | 14 | EXPOSE 4000-4999 15 | 16 | CMD ["pnpm", "run", "start:dev"] 17 | -------------------------------------------------------------------------------- /fe/src/modules/evaluator/createEvalMessage.ts: -------------------------------------------------------------------------------- 1 | import type { EvalMessage } from './types'; 2 | 3 | export default function createEvalMessage( 4 | id: string | number, 5 | code: string, 6 | param: string, 7 | ): EvalMessage { 8 | return { 9 | type: 'EVAL', 10 | clientId: id, 11 | code, 12 | param, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /be/run-all: -------------------------------------------------------------------------------- 1 | docker container stop algo-with-me-api 2>/dev/null 2 | docker container rm algo-with-me-api 2>/dev/null 3 | docker container stop algo-with-me-score 2>/dev/null 4 | docker container rm algo-with-me-score 2>/dev/null 5 | docker container stop algo-with-me-docker 2>/dev/null 6 | docker container rm algo-with-me-docker 2>/dev/null 7 | docker compose up -------------------------------------------------------------------------------- /fe/src/components/Common/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { isNumber } from '@/utils/type'; 2 | 3 | interface Props { 4 | size: number | `${string}px`; 5 | } 6 | 7 | export function Logo({ size }: Props) { 8 | const logoSize = isNumber(size) ? `${size}px` : size; 9 | return logo; 10 | } 11 | -------------------------------------------------------------------------------- /fe/src/hooks/simulation/types.ts: -------------------------------------------------------------------------------- 1 | export type SimulationInput = { 2 | id: number; 3 | input: string; 4 | expected: string; 5 | changable: boolean; 6 | }; 7 | 8 | export type SimulationResult = { 9 | id: number; 10 | isDone: boolean; 11 | input: string; 12 | output: unknown; 13 | expected: string; 14 | logs: string[]; 15 | }; 16 | -------------------------------------------------------------------------------- /fe/src/components/Common/Modal/ModalContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface ModalContextProps { 4 | isOpen: boolean; 5 | close: () => void; 6 | open: () => void; 7 | } 8 | 9 | export const ModalContext = createContext({ 10 | isOpen: false, 11 | close: () => {}, 12 | open: () => {}, 13 | }); 14 | -------------------------------------------------------------------------------- /fe/styled-system/css/css.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject } from '../types/index'; 3 | 4 | interface CssFunction { 5 | (...styles: Array): string 6 | raw: (...styles: Array) => SystemStyleObject 7 | } 8 | 9 | export declare const css: CssFunction; -------------------------------------------------------------------------------- /be/run-all-background: -------------------------------------------------------------------------------- 1 | docker container stop algo-with-me-api 2>/dev/null 2 | docker container rm algo-with-me-api 2>/dev/null 3 | docker container stop algo-with-me-score 2>/dev/null 4 | docker container rm algo-with-me-score 2>/dev/null 5 | docker container stop algo-with-me-docker 2>/dev/null 6 | docker container rm algo-with-me-docker 2>/dev/null 7 | docker compose up -d -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/problem.simple.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ProblemSimpleResponseDto { 4 | constructor(id: number, title: string) { 5 | this.id = id; 6 | this.title = title; 7 | } 8 | 9 | @ApiProperty() 10 | id: number; 11 | 12 | @ApiProperty() 13 | title: string; 14 | } 15 | -------------------------------------------------------------------------------- /fe/src/components/Common/Socket/SocketContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | import type { Socket } from '@/utils/socket'; 4 | 5 | export interface SocketContextProps { 6 | socket: Socket | null; 7 | isConnected: boolean; 8 | } 9 | 10 | export const SocketContext = createContext({ 11 | socket: null, 12 | isConnected: false, 13 | }); 14 | -------------------------------------------------------------------------------- /fe/src/components/Dashboard/Buttons/OpenDashboardModalButton.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import { Button } from '@/components/Common'; 4 | 5 | interface Props extends ButtonHTMLAttributes {} 6 | 7 | export default function OpenDashboardModalButton(props: Props) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/box.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const boxConfig = { 5 | transform(props) { 6 | return props; 7 | }} 8 | 9 | export const getBoxStyle = (styles = {}) => boxConfig.transform(styles, { map: mapObject }) 10 | 11 | export const box = (styles) => css(getBoxStyle(styles)) 12 | box.raw = getBoxStyle -------------------------------------------------------------------------------- /fe/src/utils/unit/__tests__/byteToKb.spec.ts: -------------------------------------------------------------------------------- 1 | import { byteToKB } from '../index'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('byteToKB', () => { 5 | it('byte를 KB로 변환한다.', () => { 6 | expect(byteToKB(0)).toBe(0); 7 | expect(byteToKB(1024)).toBe(1); 8 | expect(byteToKB(2048)).toBe(2); 9 | expect(byteToKB(1024 * 1024)).toBe(1024); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/user/decorators/user.decorators.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | 3 | import { User } from '../entities/user.entity'; 4 | 5 | export const AuthUser = createParamDecorator((data: string, ctx: ExecutionContext) => { 6 | const user: User = ctx.switchToHttp().getRequest().user; 7 | if (!user) return; 8 | return user; 9 | }); 10 | -------------------------------------------------------------------------------- /fe/src/utils/secToTime.ts: -------------------------------------------------------------------------------- 1 | export default function secToTime(sec: number) { 2 | const days = Math.floor(sec / (1000 * 60 * 60 * 24)); 3 | const hours = Math.floor((sec % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 4 | const minutes = Math.floor((sec % (1000 * 60 * 60)) / (1000 * 60)); 5 | const seconds = Math.floor((sec % (1000 * 60)) / 1000); 6 | 7 | return { days, hours, minutes, seconds }; 8 | } 9 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/user/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class UserResponseDto { 4 | constructor(email: string, nickname: string) { 5 | this.email = email; 6 | this.nickname = nickname; 7 | } 8 | 9 | @ApiProperty({ description: '이메일' }) 10 | email: string; 11 | 12 | @ApiProperty({ description: '닉네임' }) 13 | nickname: string; 14 | } 15 | -------------------------------------------------------------------------------- /fe/src/hooks/dashboard/types.ts: -------------------------------------------------------------------------------- 1 | import { CompetitionId } from '@/apis/competitions'; 2 | 3 | export type Rank = { 4 | rank: number; 5 | email: string; 6 | score: number; 7 | problemDict: { 8 | [key: number]: number | null; 9 | }; 10 | }; 11 | 12 | export type Dashboard = { 13 | competitionId: CompetitionId; 14 | totalProblemCount: number; 15 | rankings: Rank[]; 16 | myRanking: Rank; 17 | }; 18 | -------------------------------------------------------------------------------- /fe/src/apis/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | import api from '@/utils/api'; 2 | 3 | import { DashboardData, FetchDashboardResponse } from './type'; 4 | 5 | export const getDashboardData = async ({ competitionId, email }: DashboardData) => { 6 | const response = await api.get(`/dashboards/${competitionId}`, { 7 | params: { 8 | email, 9 | }, 10 | }); 11 | 12 | return response.data; 13 | }; 14 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { User } from './entities/user.entity'; 5 | import { UserService } from './services/user.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | providers: [UserService], 10 | exports: [UserService], 11 | }) 12 | export class UserModule {} 13 | -------------------------------------------------------------------------------- /fe/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # environment 27 | .env 28 | -------------------------------------------------------------------------------- /fe/src/components/Auth/AuthContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface AuthContextProps { 4 | isLoggedin: boolean; 5 | email: string; 6 | login: (email: string) => void; 7 | logout: () => void; 8 | } 9 | 10 | const AuthContext = createContext({ 11 | isLoggedin: false, 12 | email: '', 13 | login: () => {}, 14 | logout: () => {}, 15 | }); 16 | 17 | export default AuthContext; 18 | -------------------------------------------------------------------------------- /fe/src/modules/evaluator/types.ts: -------------------------------------------------------------------------------- 1 | export type EvalMessage = { 2 | type: 'EVAL'; 3 | clientId: number | string; 4 | code: string; 5 | param: string; 6 | }; 7 | 8 | export type EvalResult = { 9 | error?: { 10 | name: string; 11 | message: string; 12 | stack: string; 13 | }; 14 | result: unknown; 15 | logs: string[]; 16 | }; 17 | 18 | export type TaskEndMessage = EvalResult & { task: EvalMessage | null }; 19 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 3 | 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); 9 | await app.listen(4000 + parseInt(process.env.SCORE_SERVER_ID)); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /fe/src/apis/competitionList/index.ts: -------------------------------------------------------------------------------- 1 | import api from '@/utils/api'; 2 | 3 | import { Competition } from './types'; 4 | 5 | export const fetchCompetitionList = async (): Promise => { 6 | try { 7 | const response = await api.get('/competitions'); 8 | return response.data; 9 | } catch (error) { 10 | console.error('Error fetching competitions:', (error as Error).message); 11 | throw error; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /fe/src/components/Common/index.ts: -------------------------------------------------------------------------------- 1 | export { Input } from './Input'; 2 | export * from './Modal'; 3 | export * from './Space'; 4 | export * from './HStack'; 5 | export * from './VStack'; 6 | export * from './Button'; 7 | export * from './Text'; 8 | export * from './Link'; 9 | export * from './Chip'; 10 | export * from './Icon'; 11 | export * from './BreadCrumb'; 12 | export * from './Logo'; 13 | export * from './Card'; 14 | export * from './Loading'; 15 | -------------------------------------------------------------------------------- /be/run-api: -------------------------------------------------------------------------------- 1 | docker container stop algo-with-me-api 2>/dev/null 2 | docker container rm algo-with-me-api 2>/dev/null 3 | docker run -d -p 3000:3000 \ 4 | -v /home/be/algo-with-me/problems/:/algo-with-me/problems/ \ 5 | -v /home/be/algo-with-me/submissions/:/algo-with-me/submissions/ \ 6 | -v /home/be/algo-with-me/testcases/:/algo-with-me/testcases/ \ 7 | --env-file ./algo-with-me-api/.env \ 8 | --user 1001:1001 \ 9 | --name algo-with-me-api \ 10 | algo-with-me-api -------------------------------------------------------------------------------- /fe/src/utils/array/__tests__/range.spec.ts: -------------------------------------------------------------------------------- 1 | import { range } from '..'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('range', () => { 5 | it.each([ 6 | [0, 0, 1, []], 7 | [0, 5, 1, [0, 1, 2, 3, 4]], 8 | [1, 3, 1, [1, 2]], 9 | [0, 10, 2, [0, 2, 4, 6, 8]], 10 | ])('%s ~ %s 까지 %s(default 1) 만큼 증가하는 배열을 반환한다.', (start, end, gap, expected) => { 11 | expect(range(start, end, gap)).toEqual(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /be/run-score: -------------------------------------------------------------------------------- 1 | docker container stop algo-with-me-score 2>/dev/null 2 | docker container rm algo-with-me-score 2>/dev/null 3 | docker run -d -p 4000:4000 \ 4 | -v /home/be/algo-with-me/problems/:/algo-with-me/problems/ \ 5 | -v /home/be/algo-with-me/submissions/:/algo-with-me/submissions/ \ 6 | -v /home/be/algo-with-me/testcases/:/algo-with-me/testcases/ \ 7 | --env-file ./algo-with-me-score/.env \ 8 | --user 1001:1001 \ 9 | --name algo-with-me-score \ 10 | algo-with-me-score -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/competition.is.joinable.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class IsJoinableDto { 4 | constructor(isJoinable: boolean, message?: string) { 5 | this.isJoinable = isJoinable; 6 | this.message = message; 7 | } 8 | 9 | @ApiProperty({ description: 'true 면 입장 가능, false 면 입장 불가능' }) 10 | isJoinable: boolean; 11 | 12 | @ApiProperty({ description: '입장 불가능일 경우 사유' }) 13 | message?: string; 14 | } 15 | -------------------------------------------------------------------------------- /fe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Algo With Me 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectQueue } from '@nestjs/bull'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { Queue } from 'bull'; 4 | 5 | import { MessageQueueItemDto } from './score/dtos/message-queue-item.dto'; 6 | 7 | @Injectable() 8 | export class AppService { 9 | constructor(@InjectQueue('submission') private testQueue: Queue) {} 10 | 11 | async addMessageQueue(item: MessageQueueItemDto) { 12 | return await this.testQueue.add(item); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fe/src/modules/evaluator/createEvaluator.ts: -------------------------------------------------------------------------------- 1 | import EvalWorker from './eval.worker?worker'; 2 | import { EvalMessage } from './types'; 3 | 4 | export type Evaluator = { 5 | isIdle: boolean; 6 | worker: Worker; 7 | currentTask: EvalMessage | null; 8 | }; 9 | 10 | const createEvaluator = () => { 11 | const evaluator: Evaluator = { 12 | isIdle: true, 13 | worker: new EvalWorker(), 14 | currentTask: null, 15 | }; 16 | 17 | return evaluator; 18 | }; 19 | 20 | export default createEvaluator; 21 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/docker-sh/run.sh: -------------------------------------------------------------------------------- 1 | sudo docker run -d \ 2 | -p 5000:5000 \ 3 | -e COMPETITION_ID=$1 \ 4 | -e USER_ID=$2 \ 5 | -e PROBLEM_ID=$3 \ 6 | -e TESTCASE_ID=$4 \ 7 | -v $HOME/algo-with-me/problems:/algo-with-me/problems \ 8 | -v $HOME/algo-with-me/testcases:/algo-with-me/testcases \ 9 | -v $HOME/algo-with-me/submissions:/algo-with-me/submissions \ 10 | --user "$(id -u)":"$(id -g)" \ 11 | --name algo-with-me-docker \ 12 | algo-with-me-docker:latest 13 | 14 | #--network none \ 15 | #--network isolatedNetwork \ -------------------------------------------------------------------------------- /fe/styled-system/patterns/visually-hidden.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const visuallyHiddenConfig = { 5 | transform(props) { 6 | return { 7 | srOnly: true, 8 | ...props 9 | }; 10 | }} 11 | 12 | export const getVisuallyHiddenStyle = (styles = {}) => visuallyHiddenConfig.transform(styles, { map: mapObject }) 13 | 14 | export const visuallyHidden = (styles) => css(getVisuallyHiddenStyle(styles)) 15 | visuallyHidden.raw = getVisuallyHiddenStyle -------------------------------------------------------------------------------- /be/run-docker: -------------------------------------------------------------------------------- 1 | docker container stop algo-with-me-docker 2>/dev/null 2 | docker container rm algo-with-me-docker 2>/dev/null 3 | docker run -d \ 4 | -p 5000:5000 \ 5 | -e COMPETITION_ID=$1 \ 6 | -e USER_ID=$2 \ 7 | -e PROBLEM_ID=$3 \ 8 | -e TESTCASE_ID=$4 \ 9 | -v /home/be/algo-with-me/problems:/algo-with-me/problems \ 10 | -v /home/be/algo-with-me/testcases:/algo-with-me/testcases \ 11 | -v /home/be/algo-with-me/submissions:/algo-with-me/submissions \ 12 | --user 1001:1001 \ 13 | --name algo-with-me-docker \ 14 | algo-with-me-docker -------------------------------------------------------------------------------- /fe/src/components/Common/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | export interface Props extends HTMLAttributes {} 6 | 7 | export function Card({ className, children, ...props }: Props) { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | 15 | const style = css({ 16 | padding: '1rem', 17 | background: 'surface.alt', 18 | borderRadius: '0.5rem', 19 | }); 20 | -------------------------------------------------------------------------------- /fe/src/modules/evaluator/eval.worker.ts: -------------------------------------------------------------------------------- 1 | import * as QuickJS from './quickjs'; 2 | import type { EvalMessage } from './types'; 3 | 4 | type MessageEvent = { 5 | data: EvalMessage; 6 | }; 7 | 8 | self.addEventListener('message', async function (e: MessageEvent) { 9 | const message = e.data; 10 | 11 | try { 12 | const { code, param } = message; 13 | const result = await QuickJS.evaluate(code, param); 14 | 15 | self.postMessage(result); 16 | } catch (err) { 17 | self.postMessage(err); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /fe/src/components/Layout/CompetitionPageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | interface Props extends HTMLAttributes {} 6 | 7 | export function CompetitionPageLayout({ children, className, ...props }: Props) { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | 15 | const style = css({ 16 | width: '100%', 17 | color: 'text', 18 | background: 'background', 19 | }); 20 | -------------------------------------------------------------------------------- /fe/src/components/Layout/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | interface Props extends HTMLAttributes {} 6 | 7 | export function PageLayout({ children, className, ...props }: Props) { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | 15 | const style = css({ 16 | background: 'transparent', 17 | width: '100%', 18 | color: 'text', 19 | paddingBottom: '300px', 20 | }); 21 | -------------------------------------------------------------------------------- /fe/src/utils/type/__tests__/isNil.spec.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from '../index'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('isNil', () => { 5 | it('null 또는 undefined라면 true를 반환한다.', () => { 6 | expect(isNil(null)).toBe(true); 7 | expect(isNil(undefined)).toBe(true); 8 | }); 9 | 10 | it.each([[''], ['a'], [+0], [-0], [0], [1], [Number(0)], [String('')], [true], [false], [{}]])( 11 | 'isNil(%s) -> false', 12 | (input) => { 13 | expect(isNil(input)).toBe(false); 14 | }, 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/dashboard/entities/dashboard.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | import { Competition } from '@src/competition/entities/competition.entity'; 4 | 5 | @Entity() 6 | export class Dashboard { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column() 11 | competitionId: number; 12 | 13 | @ManyToOne(() => Competition, (competition) => competition.dashboards) 14 | competition: Competition; 15 | 16 | @Column('jsonb') 17 | result: object; 18 | } 19 | -------------------------------------------------------------------------------- /fe/src/components/Main/Buttons/ViewDashboardButton.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | import type { CompetitionId } from '@/apis/competitions'; 4 | import { Button } from '@/components/Common'; 5 | 6 | interface Props { 7 | competitionId: CompetitionId; 8 | } 9 | 10 | export default function ViewDashboardButton({ competitionId }: Props) { 11 | const dashboardLink = `/competition/dashboard/${competitionId}`; 12 | 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /fe/src/components/Submission/Connecting.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { Loading } from '@/components/Common'; 4 | 5 | interface Props { 6 | isConnected: boolean; 7 | } 8 | 9 | export default function Connecting(props: Props) { 10 | if (props.isConnected) return null; 11 | 12 | return ( 13 |
14 | 연결 중... 15 | 16 |
17 | ); 18 | } 19 | 20 | const rowStyle = css({ 21 | display: 'flex', 22 | gap: '0.5rem', 23 | }); 24 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=20.9.0 2 | ARG ALPINE_VERSION=3.18 3 | ARG PORT=5000 4 | 5 | FROM node:${NODE_VERSION}-bookworm AS base 6 | 7 | RUN npm install -g pnpm 8 | 9 | RUN groupadd -g 1001 be \ 10 | && useradd -r -m -u 1001 -g be be 11 | USER be 12 | 13 | CMD "mkdir -p /algo-with-me" 14 | WORKDIR /algo-with-me 15 | COPY --chmod=555 --chown=be:be ./node-sh /algo-with-me/node-sh 16 | COPY --chown=be:be . /algo-with-me 17 | 18 | RUN pnpm install 19 | 20 | SHELL ["/bin/bash", "-ec"] 21 | 22 | EXPOSE ${PORT} 23 | 24 | CMD node src/app.js 25 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/link-box.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const linkBoxConfig = { 5 | transform(props) { 6 | return { 7 | position: "relative", 8 | "& :where(a, abbr)": { 9 | position: "relative", 10 | zIndex: "1" 11 | }, 12 | ...props 13 | }; 14 | }} 15 | 16 | export const getLinkBoxStyle = (styles = {}) => linkBoxConfig.transform(styles, { map: mapObject }) 17 | 18 | export const linkBox = (styles) => css(getLinkBoxStyle(styles)) 19 | linkBox.raw = getLinkBoxStyle -------------------------------------------------------------------------------- /be/algo-with-me-score/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | 3 | import { AppService } from './app.service'; 4 | import { MessageQueueItemDto } from './score/dtos/message-queue-item.dto'; 5 | 6 | @Controller() 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | 10 | @Post() 11 | addMessage(@Body('submissionId') submissionId: number, @Body('socketId') socketId: string) { 12 | return this.appService.addMessageQueue(new MessageQueueItemDto(submissionId, socketId)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/container.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const containerConfig = { 5 | transform(props) { 6 | return { 7 | position: "relative", 8 | maxWidth: "8xl", 9 | mx: "auto", 10 | px: { base: "4", md: "6", lg: "8" }, 11 | ...props 12 | }; 13 | }} 14 | 15 | export const getContainerStyle = (styles = {}) => containerConfig.transform(styles, { map: mapObject }) 16 | 17 | export const container = (styles) => css(getContainerStyle(styles)) 18 | container.raw = getContainerStyle -------------------------------------------------------------------------------- /fe/src/components/Common/HStack/HStack.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | export interface Props extends HTMLAttributes { 6 | as?: React.ElementType; 7 | } 8 | 9 | export function HStack({ children, className, as = 'div', ...props }: Props) { 10 | const As = as; 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | 19 | const rowListStyle = css({ 20 | display: 'flex', 21 | flexDirection: 'column', 22 | }); 23 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/center.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const centerConfig = { 5 | transform(props) { 6 | const { inline, ...rest } = props; 7 | return { 8 | display: inline ? "inline-flex" : "flex", 9 | alignItems: "center", 10 | justifyContent: "center", 11 | ...rest 12 | }; 13 | }} 14 | 15 | export const getCenterStyle = (styles = {}) => centerConfig.transform(styles, { map: mapObject }) 16 | 17 | export const center = (styles) => css(getCenterStyle(styles)) 18 | center.raw = getCenterStyle -------------------------------------------------------------------------------- /be/algo-with-me-api/src/exception/service.exception.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_CODE } from './exception.enum'; 2 | 3 | export const NotFoundException = (message?: string) => { 4 | return new ServiceException(ERROR_CODE.NOT_FOUND, message); 5 | }; 6 | 7 | export const BadRequestException = (message?: string) => { 8 | return new ServiceException(ERROR_CODE.BAD_REQUEST, message); 9 | }; 10 | 11 | export class ServiceException extends Error { 12 | errorCode: number; 13 | constructor(errorCode: number, message?: string) { 14 | super(message); 15 | this.errorCode = errorCode; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fe/src/hooks/problem/useProblemList.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import type { ProblemInfo } from '@/apis/problems'; 4 | import { fetchProblemList } from '@/apis/problems'; 5 | 6 | export function useProblemList() { 7 | const [problemList, setProblemList] = useState([]); 8 | 9 | async function updateProblemList() { 10 | const problems = await fetchProblemList(); 11 | 12 | setProblemList(problems); 13 | } 14 | 15 | useEffect(() => { 16 | updateProblemList(); 17 | }, []); 18 | 19 | return { 20 | problemList, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /be/algo-with-me-score/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | .env -------------------------------------------------------------------------------- /fe/styled-system/patterns/spacer.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const spacerConfig = { 5 | transform(props, { map }) { 6 | const { size, ...rest } = props; 7 | return { 8 | alignSelf: "stretch", 9 | justifySelf: "stretch", 10 | flex: map(size, (v) => v == null ? "1" : `0 0 ${v}`), 11 | ...rest 12 | }; 13 | }} 14 | 15 | export const getSpacerStyle = (styles = {}) => spacerConfig.transform(styles, { map: mapObject }) 16 | 17 | export const spacer = (styles) => css(getSpacerStyle(styles)) 18 | spacer.raw = getSpacerStyle -------------------------------------------------------------------------------- /be/build-all: -------------------------------------------------------------------------------- 1 | docker compose down 2>/dev/null 2 | docker container stop algo-with-me-api 2>/dev/null 3 | docker container stop algo-with-me-score 2>/dev/null 4 | docker container stop algo-with-me-docker 2>/dev/null 5 | docker image rm algo-with-me-api 2>/dev/null 6 | docker image rm algo-with-me-score 2>/dev/null 7 | docker image rm algo-with-me-docker 2>/dev/null 8 | 9 | cd algo-with-me-api/ 10 | docker build -t algo-with-me-api . 11 | cd .. 12 | 13 | cd algo-with-me-score/ 14 | docker build -t algo-with-me-score . 15 | cd .. 16 | 17 | cd algo-with-me-docker/ 18 | docker build -t algo-with-me-docker . 19 | cd .. -------------------------------------------------------------------------------- /be/algo-with-me-api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # nest 환경 변수 39 | .env -------------------------------------------------------------------------------- /fe/src/utils/date/__tests__/formatDate.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from '../index'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('formatDate', () => { 5 | it('YYYY-MM-DDThh:mm 형식의 문자열을 반환한다.', () => { 6 | const now = new Date(2000, 1, 1, 13, 1, 1); 7 | const dateStr = formatDate(now, 'YYYY-MM-DDThh:mm'); 8 | 9 | expect(dateStr).toBe('2000-02-01T04:01'); 10 | }); 11 | 12 | it('일치하는 형식이 없으면 빈 문자열을 반환한다.', () => { 13 | const now = new Date(2000, 1, 1, 13, 1, 1); 14 | const dateStr = formatDate(now, '??'); 15 | 16 | expect(dateStr).toBe(''); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /fe/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | import { RouterProvider } from 'react-router-dom'; 6 | 7 | import AuthProvider from './components/Auth/AuthProvider'; 8 | import { Modal } from './components/Common'; 9 | import router from './router'; 10 | 11 | ReactDOM.createRoot(document.getElementById('root')!).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/hstack.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const hstackConfig = { 5 | transform(props) { 6 | const { justify, gap = "10px", ...rest } = props; 7 | return { 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: justify, 11 | gap, 12 | flexDirection: "row", 13 | ...rest 14 | }; 15 | }} 16 | 17 | export const getHstackStyle = (styles = {}) => hstackConfig.transform(styles, { map: mapObject }) 18 | 19 | export const hstack = (styles) => css(getHstackStyle(styles)) 20 | hstack.raw = getHstackStyle -------------------------------------------------------------------------------- /fe/styled-system/patterns/vstack.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const vstackConfig = { 5 | transform(props) { 6 | const { justify, gap = "10px", ...rest } = props; 7 | return { 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: justify, 11 | gap, 12 | flexDirection: "column", 13 | ...rest 14 | }; 15 | }} 16 | 17 | export const getVstackStyle = (styles = {}) => vstackConfig.transform(styles, { map: mapObject }) 18 | 19 | export const vstack = (styles) => css(getVstackStyle(styles)) 20 | vstack.raw = getVstackStyle -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: be-deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - be-dev 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: SSH-be-deploy 13 | uses: appleboy/ssh-action@master 14 | with: 15 | host: ${{ secrets.NCP_HOST }} 16 | username: ${{ secrets.NCP_USERNAME }} 17 | password: ${{ secrets.NCP_PASSWORD }} 18 | port: ${{ secrets.NCP_PORT }} 19 | script: | 20 | cd /home/be/web12-algo-with-me/ 21 | git pull 22 | cd be 23 | ./build-all 24 | ./run-all-background 25 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/log/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | @Injectable() 5 | export class LoggerMiddleware implements NestMiddleware { 6 | logger = new Logger('HTTP'); 7 | 8 | use(request: Request, response: Response, next: NextFunction): void { 9 | const { method, originalUrl } = request; 10 | 11 | response.on('finish', () => { 12 | const { statusCode } = response; 13 | this.logger.debug(`${method} ${originalUrl} ${statusCode}`); 14 | }); 15 | next(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/square.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const squareConfig = { 5 | transform(props) { 6 | const { size, ...rest } = props; 7 | return { 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: "center", 11 | flex: "0 0 auto", 12 | width: size, 13 | height: size, 14 | ...rest 15 | }; 16 | }} 17 | 18 | export const getSquareStyle = (styles = {}) => squareConfig.transform(styles, { map: mapObject }) 19 | 20 | export const square = (styles) => css(getSquareStyle(styles)) 21 | square.raw = getSquareStyle -------------------------------------------------------------------------------- /fe/styled-system/patterns/stack.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const stackConfig = { 5 | transform(props) { 6 | const { align, justify, direction = "column", gap = "10px", ...rest } = props; 7 | return { 8 | display: "flex", 9 | flexDirection: direction, 10 | alignItems: align, 11 | justifyContent: justify, 12 | gap, 13 | ...rest 14 | }; 15 | }} 16 | 17 | export const getStackStyle = (styles = {}) => stackConfig.transform(styles, { map: mapObject }) 18 | 19 | export const stack = (styles) => css(getStackStyle(styles)) 20 | stack.raw = getStackStyle -------------------------------------------------------------------------------- /fe/src/utils/observer/index.ts: -------------------------------------------------------------------------------- 1 | export type Listener = (result: T) => void; 2 | 3 | export interface Observer { 4 | notify: (data: T) => void; 5 | subscribe: (listener: Listener) => () => void; 6 | } 7 | 8 | export function createObserver(listeners: Listener[] = []): Observer { 9 | function notify(data: T) { 10 | listeners.forEach((l) => l(data)); 11 | } 12 | function subscribe(listener: Listener) { 13 | listeners.push(listener); 14 | 15 | return () => { 16 | listeners = listeners.filter((l) => l !== listener); 17 | }; 18 | } 19 | 20 | return { 21 | subscribe, 22 | notify, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /fe/src/apis/auth/index.ts: -------------------------------------------------------------------------------- 1 | import api, { type NetworkError } from '@/utils/api'; 2 | 3 | import { type TokenValidResponse } from './types'; 4 | 5 | const AUTH_TEST_PATH = '/auths/tests'; 6 | 7 | export const fetchTokenValid = async (token: string): Promise => { 8 | try { 9 | const response = await api.get(AUTH_TEST_PATH, { 10 | headers: { Authorization: `Bearer ${token}` }, 11 | }); 12 | return await response.data; 13 | } catch (e) { 14 | const error = e as NetworkError; 15 | console.error('토큰 유효성 확인 실패:', error.message); 16 | throw error; 17 | } 18 | }; 19 | 20 | export { type TokenValidResponse }; 21 | -------------------------------------------------------------------------------- /fe/src/components/Simulation/SimulationExecButton.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | 3 | import { Button } from '../Common'; 4 | 5 | interface Props extends HTMLAttributes { 6 | isRunning: boolean; 7 | onExec: () => void; 8 | onCancel: () => void; 9 | } 10 | 11 | export const SimulationExecButton = ({ isRunning, onExec, onCancel }: Props) => { 12 | const RUN_SIMULATION = '테스트 실행'; 13 | const CANCEL_SIMULATION = '실행 취소'; 14 | 15 | if (isRunning) { 16 | return ; 17 | } else { 18 | return ; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/circle.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const circleConfig = { 5 | transform(props) { 6 | const { size, ...rest } = props; 7 | return { 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: "center", 11 | flex: "0 0 auto", 12 | width: size, 13 | height: size, 14 | borderRadius: "9999px", 15 | ...rest 16 | }; 17 | }} 18 | 19 | export const getCircleStyle = (styles = {}) => circleConfig.transform(styles, { map: mapObject }) 20 | 21 | export const circle = (styles) => css(getCircleStyle(styles)) 22 | circle.raw = getCircleStyle -------------------------------------------------------------------------------- /fe/src/components/Competition/CompetitionHeader.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { VStack, type VStackProps } from '../Common'; 4 | 5 | interface Props extends VStackProps {} 6 | 7 | export default function CompetitionHeader({ className, children, ...props }: Props) { 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | } 14 | 15 | const headerStyle = css({ 16 | height: '4rem', 17 | paddingY: '0.5rem', 18 | borderBottom: '1px solid', 19 | borderColor: 'border', 20 | gap: '1rem', 21 | placeItems: 'center', 22 | }); 23 | -------------------------------------------------------------------------------- /fe/src/components/Common/Modal/ModalProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { useState } from 'react'; 3 | 4 | import { ModalContext } from './ModalContext'; 5 | 6 | export function ModalProvider({ children }: { children: ReactNode }) { 7 | const [isOpen, setIsOpen] = useState(false); 8 | const close = () => { 9 | setIsOpen(false); 10 | }; 11 | const open = () => { 12 | setIsOpen(true); 13 | }; 14 | 15 | return ( 16 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/bleed.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const bleedConfig = { 5 | transform(props) { 6 | const { inline = "0", block = "0", ...rest } = props; 7 | return { 8 | "--bleed-x": `spacing.${inline}`, 9 | "--bleed-y": `spacing.${block}`, 10 | marginInline: "calc(var(--bleed-x, 0) * -1)", 11 | marginBlock: "calc(var(--bleed-y, 0) * -1)", 12 | ...rest 13 | }; 14 | }} 15 | 16 | export const getBleedStyle = (styles = {}) => bleedConfig.transform(styles, { map: mapObject }) 17 | 18 | export const bleed = (styles) => css(getBleedStyle(styles)) 19 | bleed.raw = getBleedStyle -------------------------------------------------------------------------------- /fe/styled-system/patterns/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export * from './box'; 3 | export * from './flex'; 4 | export * from './stack'; 5 | export * from './vstack'; 6 | export * from './hstack'; 7 | export * from './spacer'; 8 | export * from './square'; 9 | export * from './circle'; 10 | export * from './center'; 11 | export * from './link-box'; 12 | export * from './link-overlay'; 13 | export * from './aspect-ratio'; 14 | export * from './grid'; 15 | export * from './grid-item'; 16 | export * from './wrap'; 17 | export * from './container'; 18 | export * from './divider'; 19 | export * from './float'; 20 | export * from './bleed'; 21 | export * from './visually-hidden'; -------------------------------------------------------------------------------- /fe/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc'; 2 | import path from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: [ 8 | { 9 | find: '@', 10 | replacement: path.resolve(__dirname, 'src'), 11 | }, 12 | { 13 | find: '@style', 14 | replacement: path.resolve(__dirname, 'styled-system'), 15 | }, 16 | ], 17 | }, 18 | build: { 19 | rollupOptions: { 20 | output: { 21 | inlineDynamicImports: true, 22 | }, 23 | }, 24 | }, 25 | worker: { 26 | format: 'es', 27 | }, 28 | plugins: [react()], 29 | }); 30 | -------------------------------------------------------------------------------- /fe/src/components/Problem/ProblemViewer.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | import Markdown from './Markdown'; 6 | 7 | interface Props extends HTMLAttributes { 8 | content: string; 9 | } 10 | 11 | export default function ProblemViewer({ content, className, ...props }: Props) { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | 19 | const style = css({ 20 | backgroundColor: 'surface', 21 | color: 'text', 22 | padding: '1rem', 23 | overflow: 'auto', 24 | whiteSpace: 'pre-wrap', 25 | }); 26 | -------------------------------------------------------------------------------- /fe/src/components/Submission/types.ts: -------------------------------------------------------------------------------- 1 | import { ProblemId } from '@/apis/problems'; 2 | 3 | export const SUBMIT_STATE = { 4 | notSubmitted: 0, 5 | loading: 1, 6 | submitted: 2, 7 | } as const; 8 | 9 | export type SubmitState = (typeof SUBMIT_STATE)[keyof typeof SUBMIT_STATE]; 10 | 11 | export type ScoreStart = { 12 | message: string; 13 | testcaseNum: number; 14 | }; 15 | 16 | export type ScoreResult = { 17 | problemId: ProblemId; 18 | testcaseId: number; 19 | timeUsage: number; 20 | memoryUsage: number; 21 | result: string; 22 | }; 23 | 24 | export type SubmitResult = { 25 | testcaseId: number; 26 | submitState: SubmitState; 27 | score?: ScoreResult; 28 | }; 29 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/wrap.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const wrapConfig = { 5 | transform(props) { 6 | const { columnGap, rowGap, gap = columnGap || rowGap ? void 0 : "10px", align, justify, ...rest } = props; 7 | return { 8 | display: "flex", 9 | flexWrap: "wrap", 10 | alignItems: align, 11 | justifyContent: justify, 12 | gap, 13 | columnGap, 14 | rowGap, 15 | ...rest 16 | }; 17 | }} 18 | 19 | export const getWrapStyle = (styles = {}) => wrapConfig.transform(styles, { map: mapObject }) 20 | 21 | export const wrap = (styles) => css(getWrapStyle(styles)) 22 | wrap.raw = getWrapStyle -------------------------------------------------------------------------------- /fe/src/utils/copy/__tests__/copy.spec.ts: -------------------------------------------------------------------------------- 1 | import { deepCopy } from '../index'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('deepCopy', () => { 5 | it('deepCopy는 객체를 깊은 복사한다.', () => { 6 | const obj = { 7 | inner: { 8 | a: 1, 9 | }, 10 | }; 11 | expect(deepCopy(obj)).not.equal(obj); 12 | expect(deepCopy(obj).inner).not.equal(obj.inner); 13 | }); 14 | 15 | it('deepCopy는 배열을 깊은 복사한다.', () => { 16 | const obj = { inner: { a: 1 } }; 17 | const arr = [obj]; 18 | expect(deepCopy(arr)).not.equal(arr); 19 | expect(deepCopy(arr)[0]).not.equal(arr[0]); 20 | expect(deepCopy(arr)[0].inner).not.equal(obj.inner); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /fe/src/components/Auth/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import AuthContext from './AuthContext'; 4 | 5 | const AuthProvider = ({ children }: { children: React.ReactNode }) => { 6 | function logout() { 7 | setIsLogined(false); 8 | setEmail(''); 9 | } 10 | function login(email: string) { 11 | setIsLogined(true); 12 | setEmail(email); 13 | } 14 | 15 | const [isLoggedin, setIsLogined] = useState(false); 16 | const [email, setEmail] = useState(''); 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default AuthProvider; 25 | -------------------------------------------------------------------------------- /fe/src/components/Submission/SubmissionResult.tsx: -------------------------------------------------------------------------------- 1 | import { cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | import Score from './Score'; 6 | import type { SubmitResult } from './types'; 7 | 8 | interface Props extends HTMLAttributes { 9 | submitResults: SubmitResult[]; 10 | } 11 | 12 | export function SubmissionResult({ className, submitResults = [], ...props }: Props) { 13 | return ( 14 |
15 | {submitResults.map(({ score, submitState, testcaseId }, index) => ( 16 | 17 | ))} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /fe/src/utils/type/__tests__/isNumber.ts: -------------------------------------------------------------------------------- 1 | import { isNumber } from '../index'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('isNumber', () => { 5 | it('입력값이 숫자라면 true를 반환한다.', () => { 6 | expect(isNumber(0)).toBe(true); 7 | expect(isNumber(1)).toBe(true); 8 | expect(isNumber(+1)).toBe(true); 9 | expect(isNumber(-1)).toBe(true); 10 | expect(isNumber(-0)).toBe(true); 11 | expect(isNumber(+0)).toBe(true); 12 | expect(isNumber(Number(0))).toBe(true); 13 | }); 14 | 15 | it.each([[''], ['0'], [String('')], [true], [false], [{}], [() => {}]])( 16 | 'isNumber(%s) -> false', 17 | (input) => { 18 | expect(isNumber(input)).toBe(false); 19 | }, 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/index.mjs: -------------------------------------------------------------------------------- 1 | export * from './box.mjs'; 2 | export * from './flex.mjs'; 3 | export * from './stack.mjs'; 4 | export * from './vstack.mjs'; 5 | export * from './hstack.mjs'; 6 | export * from './spacer.mjs'; 7 | export * from './square.mjs'; 8 | export * from './circle.mjs'; 9 | export * from './center.mjs'; 10 | export * from './link-box.mjs'; 11 | export * from './link-overlay.mjs'; 12 | export * from './aspect-ratio.mjs'; 13 | export * from './grid.mjs'; 14 | export * from './grid-item.mjs'; 15 | export * from './wrap.mjs'; 16 | export * from './container.mjs'; 17 | export * from './divider.mjs'; 18 | export * from './float.mjs'; 19 | export * from './bleed.mjs'; 20 | export * from './visually-hidden.mjs'; -------------------------------------------------------------------------------- /fe/styled-system/patterns/link-overlay.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const linkOverlayConfig = { 5 | transform(props) { 6 | return { 7 | position: "static", 8 | _before: { 9 | content: '""', 10 | display: "block", 11 | position: "absolute", 12 | cursor: "inherit", 13 | inset: "0", 14 | zIndex: "0", 15 | ...props["_before"] 16 | }, 17 | ...props 18 | }; 19 | }} 20 | 21 | export const getLinkOverlayStyle = (styles = {}) => linkOverlayConfig.transform(styles, { map: mapObject }) 22 | 23 | export const linkOverlay = (styles) => css(getLinkOverlayStyle(styles)) 24 | linkOverlay.raw = getLinkOverlayStyle -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/tem.consumer.ts: -------------------------------------------------------------------------------- 1 | // import { OnQueueCompleted, Process, Processor } from '@nestjs/bull'; 2 | // import { Job } from 'bull'; 3 | 4 | // @Processor(process.env.REDIS_MESSAGE_QUEUE_NAME) 5 | // export class SubmissionConsumer { 6 | // @Process() 7 | // async transcode(job: Job) { 8 | // console.log(job.data); 9 | // for (let i = 0; i < 10; i++) { 10 | // console.log(i); 11 | // } 12 | // return { good: 'good' }; 13 | // } 14 | 15 | // @OnQueueCompleted() 16 | // onCompleted(job: Job, result: any) { 17 | // console.log('job done'); 18 | // console.log(result); 19 | // // redis 에서 데이터 삭제 20 | // job.remove(); 21 | // } 22 | // } 23 | -------------------------------------------------------------------------------- /be/algo-with-me-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "paths": { 21 | "@src/*": ["./src/*"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/box.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface BoxProperties { 9 | 10 | } 11 | 12 | 13 | interface BoxStyles extends BoxProperties, DistributiveOmit {} 14 | 15 | interface BoxPatternFn { 16 | (styles?: BoxStyles): string 17 | raw: (styles?: BoxStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const box: BoxPatternFn; 22 | -------------------------------------------------------------------------------- /fe/styled-system/tokens/keyframes.css: -------------------------------------------------------------------------------- 1 | @layer tokens { 2 | @keyframes spin { 3 | to { 4 | transform: rotate(360deg) 5 | } 6 | } 7 | @keyframes ping { 8 | 75%, 100% { 9 | transform: scale(2); 10 | opacity: 0 11 | } 12 | } 13 | @keyframes pulse { 14 | 50% { 15 | opacity: .5 16 | } 17 | } 18 | @keyframes bounce { 19 | 0%, 100% { 20 | transform: translateY(-25%); 21 | animation-timing-function: cubic-bezier(0.8,0,1,1) 22 | } 23 | 50% { 24 | transform: none; 25 | animation-timing-function: cubic-bezier(0,0,0.2,1) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /be/algo-with-me-api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /be/algo-with-me-score/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /fe/src/components/UserValidator/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import { SocketContext } from '../Common/Socket/SocketContext'; 5 | 6 | export function UserValidator() { 7 | const { socket, isConnected } = useContext(SocketContext); 8 | 9 | const navigate = useNavigate(); 10 | 11 | const handleMessage = useCallback(() => { 12 | alert('유효하지 않은 접근입니다.'); 13 | navigate('/'); 14 | }, []); 15 | 16 | useEffect(() => { 17 | if (!socket) return; 18 | 19 | if (!socket?.hasListeners('message')) { 20 | socket.on('message', handleMessage); 21 | } 22 | }, [socket, isConnected]); 23 | return <>; 24 | } 25 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/flex.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const flexConfig = { 5 | transform(props) { 6 | const { direction, align, justify, wrap: wrap2, basis, grow, shrink, ...rest } = props; 7 | return { 8 | display: "flex", 9 | flexDirection: direction, 10 | alignItems: align, 11 | justifyContent: justify, 12 | flexWrap: wrap2, 13 | flexBasis: basis, 14 | flexGrow: grow, 15 | flexShrink: shrink, 16 | ...rest 17 | }; 18 | }} 19 | 20 | export const getFlexStyle = (styles = {}) => flexConfig.transform(styles, { map: mapObject }) 21 | 22 | export const flex = (styles) => css(getFlexStyle(styles)) 23 | flex.raw = getFlexStyle -------------------------------------------------------------------------------- /be/algo-with-me-score/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | // "paths": { 21 | // "@src/*": ["./src/*"], 22 | // } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fe/src/hooks/problem/useCompetitionProblemList.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import type { ProblemInfo } from '@/apis/problems'; 4 | import { fetchCompetitionProblemList } from '@/apis/problems'; 5 | 6 | export function useCompetitionProblemList(competitionId: number) { 7 | const [problemList, setProblemList] = useState([]); 8 | 9 | async function updateCompetitionProblemList(competitionId: number) { 10 | const problemList = await fetchCompetitionProblemList(competitionId); 11 | setProblemList(problemList); 12 | } 13 | 14 | useEffect(() => { 15 | updateCompetitionProblemList(competitionId); 16 | }, [competitionId]); 17 | 18 | return { 19 | problemList, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /fe/src/apis/competitions/types.ts: -------------------------------------------------------------------------------- 1 | import type { ProblemId } from '../problems'; 2 | 3 | export type CompetitionId = number; 4 | 5 | export type CompetitionForm = { 6 | name: string; 7 | detail: string; 8 | maxParticipants: number; 9 | startsAt: string; 10 | endsAt: string; 11 | problemIds: ProblemId[]; 12 | }; 13 | 14 | export type CompetitionInfo = { 15 | id: CompetitionId; 16 | host: string | null; 17 | participants: string[]; 18 | name: string; 19 | detail: string; 20 | maxParticipants: number; 21 | startsAt: string; 22 | endsAt: string; 23 | createdAt: string; 24 | updatedAt: string; 25 | }; 26 | 27 | export type FetchCompetitionResponse = CompetitionInfo; 28 | export type CreateCompetitionResponse = CompetitionInfo; 29 | -------------------------------------------------------------------------------- /fe/src/utils/date/__tests__/toLocalDate.spec.ts: -------------------------------------------------------------------------------- 1 | import { toLocalDate } from '../index'; 2 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 3 | 4 | describe('toLocalDate', () => { 5 | beforeEach(() => { 6 | const spyFn = vi.spyOn(Date.prototype, 'getTimezoneOffset'); 7 | spyFn.mockReturnValue(-9 * 60); // kr 시간 차이만큼 timezoneOffset 설정 8 | }); 9 | afterEach(() => { 10 | vi.clearAllMocks(); 11 | }); 12 | 13 | it('입력받은 Date를 현재 지역 기준 Date로 변환한다.', () => { 14 | const date = new Date(2000, 1, 1, 13, 1, 1); // 2000년 2월 1일 13시 1분 1초 15 | const localDate = toLocalDate(date); 16 | // ISOString이 9시간 차이나는 것이 아니라 설정한 시간으로 나오게 됨 17 | expect(localDate.toISOString()).toBe('2000-02-01T13:01:01.000Z'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/link-box.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface LinkBoxProperties { 9 | 10 | } 11 | 12 | 13 | interface LinkBoxStyles extends LinkBoxProperties, DistributiveOmit {} 14 | 15 | interface LinkBoxPatternFn { 16 | (styles?: LinkBoxStyles): string 17 | raw: (styles?: LinkBoxStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const linkBox: LinkBoxPatternFn; 22 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/exception/service.exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | 4 | import { ServiceException } from './service.exception'; 5 | 6 | @Catch(ServiceException) 7 | export class ServiceExceptionFilter implements ExceptionFilter { 8 | catch(exception: ServiceException, host: ArgumentsHost) { 9 | const ctx = host.switchToHttp(); 10 | const request = ctx.getRequest(); 11 | const response = ctx.getResponse(); 12 | const status = exception.errorCode; 13 | 14 | response.status(status).json({ 15 | statusCode: status, 16 | message: exception.message, 17 | path: request.url, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/circle.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface CircleProperties { 9 | size?: PropertyValue<'width'> 10 | } 11 | 12 | 13 | interface CircleStyles extends CircleProperties, DistributiveOmit {} 14 | 15 | interface CirclePatternFn { 16 | (styles?: CircleStyles): string 17 | raw: (styles?: CircleStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const circle: CirclePatternFn; 22 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/container.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface ContainerProperties { 9 | 10 | } 11 | 12 | 13 | interface ContainerStyles extends ContainerProperties, DistributiveOmit {} 14 | 15 | interface ContainerPatternFn { 16 | (styles?: ContainerStyles): string 17 | raw: (styles?: ContainerStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const container: ContainerPatternFn; 22 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/square.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface SquareProperties { 9 | size?: PropertyValue<'width'> 10 | } 11 | 12 | 13 | interface SquareStyles extends SquareProperties, DistributiveOmit {} 14 | 15 | interface SquarePatternFn { 16 | (styles?: SquareStyles): string 17 | raw: (styles?: SquareStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const square: SquarePatternFn; 22 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/center.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface CenterProperties { 9 | inline?: ConditionalValue 10 | } 11 | 12 | 13 | interface CenterStyles extends CenterProperties, DistributiveOmit {} 14 | 15 | interface CenterPatternFn { 16 | (styles?: CenterStyles): string 17 | raw: (styles?: CenterStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const center: CenterPatternFn; 22 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/spacer.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface SpacerProperties { 9 | size?: ConditionalValue 10 | } 11 | 12 | 13 | interface SpacerStyles extends SpacerProperties, DistributiveOmit {} 14 | 15 | interface SpacerPatternFn { 16 | (styles?: SpacerStyles): string 17 | raw: (styles?: SpacerStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const spacer: SpacerPatternFn; 22 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/score-result.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEnum, IsNotEmpty } from 'class-validator'; 3 | 4 | import { RESULT } from '../competition.enums'; 5 | 6 | export class ScoreResultDto { 7 | @ApiProperty() 8 | @IsNotEmpty() 9 | submissionId: number; 10 | 11 | @ApiProperty() 12 | @IsNotEmpty() 13 | testcaseId: number; 14 | 15 | @ApiProperty() 16 | @IsNotEmpty() 17 | socketId: string; 18 | 19 | @ApiProperty() 20 | @IsEnum(RESULT) 21 | result: string; 22 | 23 | @ApiProperty() 24 | stdout: string; 25 | 26 | @ApiProperty() 27 | stderr: string; 28 | 29 | @ApiProperty({ description: 'ms' }) 30 | timeUsage: number; 31 | 32 | @ApiProperty({ description: 'KB' }) 33 | memoryUsage: number; 34 | } 35 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/create-submission.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | import { Problem } from '../entities/problem.entity'; 4 | import { Submission } from '../entities/submission.entity'; 5 | 6 | import { User } from '@src/user/entities/user.entity'; 7 | 8 | export class CreateSubmissionDto { 9 | @IsNotEmpty() 10 | problemId: number; 11 | 12 | @IsNotEmpty() 13 | competitionId: number; 14 | 15 | @IsNotEmpty() 16 | code: string; 17 | 18 | toEntity(problem: Problem, user: User): Submission { 19 | const submission = new Submission(); 20 | submission.code = this.code; 21 | submission.competitionId = this.competitionId; 22 | submission.problem = problem; 23 | submission.user = user; 24 | return submission; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fe/src/components/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { javascript } from '@codemirror/lang-javascript'; 2 | import { vscodeDark } from '@uiw/codemirror-theme-vscode'; 3 | import CodeMirror from '@uiw/react-codemirror'; 4 | 5 | interface Props { 6 | code: string; 7 | onChangeCode: (newCode: string) => void; 8 | height?: string; 9 | width?: string; 10 | } 11 | 12 | const Editor = (props: Props) => { 13 | return ( 14 | { 22 | props.onChangeCode(value); 23 | }} 24 | /> 25 | ); 26 | }; 27 | 28 | export default Editor; 29 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/link-overlay.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface LinkOverlayProperties { 9 | 10 | } 11 | 12 | 13 | interface LinkOverlayStyles extends LinkOverlayProperties, DistributiveOmit {} 14 | 15 | interface LinkOverlayPatternFn { 16 | (styles?: LinkOverlayStyles): string 17 | raw: (styles?: LinkOverlayStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const linkOverlay: LinkOverlayPatternFn; 22 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/competition.problem.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | import { Competition } from './competition.entity'; 4 | import { Problem } from './problem.entity'; 5 | 6 | @Entity() 7 | export class CompetitionProblem { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | competitionId: number; 13 | 14 | @ManyToOne(() => Competition, (competition) => competition.competitionProblems, { 15 | nullable: false, 16 | }) 17 | competition: Competition; 18 | 19 | @Column() 20 | problemId: number; 21 | 22 | @ManyToOne(() => Problem, (problem) => problem.competitionProblems, { nullable: false }) 23 | problem: Problem; 24 | 25 | @CreateDateColumn() 26 | createdAt: Date; 27 | } 28 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/bleed.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface BleedProperties { 9 | inline?: PropertyValue<'marginInline'> 10 | block?: PropertyValue<'marginBlock'> 11 | } 12 | 13 | 14 | interface BleedStyles extends BleedProperties, DistributiveOmit {} 15 | 16 | interface BleedPatternFn { 17 | (styles?: BleedStyles): string 18 | raw: (styles?: BleedStyles) => SystemStyleObject 19 | } 20 | 21 | 22 | export declare const bleed: BleedPatternFn; 23 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/entities/competition.problem.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | import { Competition } from './competition.entity'; 4 | import { Problem } from './problem.entity'; 5 | 6 | @Entity() 7 | export class CompetitionProblem { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | competitionId: number; 13 | 14 | @ManyToOne(() => Competition, (competition) => competition.competitionProblems, { 15 | nullable: false, 16 | }) 17 | competition: Competition; 18 | 19 | @Column() 20 | problemId: number; 21 | 22 | @ManyToOne(() => Problem, (problem) => problem.competitionProblems, { nullable: false }) 23 | problem: Problem; 24 | 25 | @CreateDateColumn() 26 | createdAt: Date; 27 | } 28 | -------------------------------------------------------------------------------- /fe/src/utils/type/__tests__/isFunction.spec.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../index'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('isFunction', () => { 5 | it('함수라면 true를 반환한다.', () => { 6 | expect(isFunction(() => {})).toBe(true); 7 | expect(isFunction(async () => {})).toBe(true); 8 | expect(isFunction(function () {})).toBe(true); 9 | expect(isFunction(function abc() {})).toBe(true); 10 | expect( 11 | isFunction(function abc(arg1: number, arg2: string) { 12 | return arg1 + arg2; 13 | }), 14 | ).toBe(true); 15 | }); 16 | 17 | it.each([[''], ['a'], [+0], [-0], [0], [1], [Number(0)], [String('')], [true], [false], [{}]])( 18 | 'isFunction(%s) -> false', 19 | (input) => { 20 | expect(isFunction(input)).toBe(false); 21 | }, 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/hstack.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface HstackProperties { 9 | justify?: PropertyValue<'justifyContent'> 10 | gap?: PropertyValue<'gap'> 11 | } 12 | 13 | 14 | interface HstackStyles extends HstackProperties, DistributiveOmit {} 15 | 16 | interface HstackPatternFn { 17 | (styles?: HstackStyles): string 18 | raw: (styles?: HstackStyles) => SystemStyleObject 19 | } 20 | 21 | 22 | export declare const hstack: HstackPatternFn; 23 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/vstack.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface VstackProperties { 9 | justify?: PropertyValue<'justifyContent'> 10 | gap?: PropertyValue<'gap'> 11 | } 12 | 13 | 14 | interface VstackStyles extends VstackProperties, DistributiveOmit {} 15 | 16 | interface VstackPatternFn { 17 | (styles?: VstackStyles): string 18 | raw: (styles?: VstackStyles) => SystemStyleObject 19 | } 20 | 21 | 22 | export declare const vstack: VstackPatternFn; 23 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/visually-hidden.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface VisuallyHiddenProperties { 9 | 10 | } 11 | 12 | 13 | interface VisuallyHiddenStyles extends VisuallyHiddenProperties, DistributiveOmit {} 14 | 15 | interface VisuallyHiddenPatternFn { 16 | (styles?: VisuallyHiddenStyles): string 17 | raw: (styles?: VisuallyHiddenStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const visuallyHidden: VisuallyHiddenPatternFn; 22 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/aspect-ratio.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface AspectRatioProperties { 9 | ratio?: ConditionalValue 10 | } 11 | 12 | 13 | interface AspectRatioStyles extends AspectRatioProperties, DistributiveOmit {} 14 | 15 | interface AspectRatioPatternFn { 16 | (styles?: AspectRatioStyles): string 17 | raw: (styles?: AspectRatioStyles) => SystemStyleObject 18 | } 19 | 20 | 21 | export declare const aspectRatio: AspectRatioPatternFn; 22 | -------------------------------------------------------------------------------- /fe/src/components/Submission/ResultTotalInfo.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import Loading from './Loading'; 4 | 5 | interface Props { 6 | isAllTestDone: boolean; 7 | } 8 | 9 | export default function ResultTotallInfo({ isAllTestDone }: Props) { 10 | // Todo 계산 로직은 웹소켓 논의 후에 추가 11 | return ( 12 |
13 |
채점 결과
14 | {isAllTestDone ? ( 15 | <> 16 |

정확성: 40.0

17 |

합계: 40.0 / 100.0

18 | 19 | ) : ( 20 | 21 | )} 22 |
23 | ); 24 | } 25 | const titleStyle = css({ 26 | margin: '0.5rem 0', 27 | }); 28 | 29 | const detailTextStyle = css({ 30 | fontSize: '0.8rem', 31 | color: 'lightgray', 32 | }); 33 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/grid-item.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const gridItemConfig = { 5 | transform(props, { map }) { 6 | const { colSpan, rowSpan, colStart, rowStart, colEnd, rowEnd, ...rest } = props; 7 | const spanFn = (v) => v === "auto" ? v : `span ${v}`; 8 | return { 9 | gridColumn: colSpan != null ? map(colSpan, spanFn) : void 0, 10 | gridRow: rowSpan != null ? map(rowSpan, spanFn) : void 0, 11 | gridColumnStart: colStart, 12 | gridColumnEnd: colEnd, 13 | gridRowStart: rowStart, 14 | gridRowEnd: rowEnd, 15 | ...rest 16 | }; 17 | }} 18 | 19 | export const getGridItemStyle = (styles = {}) => gridItemConfig.transform(styles, { map: mapObject }) 20 | 21 | export const gridItem = (styles) => css(getGridItemStyle(styles)) 22 | gridItem.raw = getGridItemStyle -------------------------------------------------------------------------------- /fe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": ["./src/*"], 24 | "@style/*": ["./styled-system/*"] 25 | } 26 | }, 27 | "include": ["src", "styled-system"], 28 | "references": [{ "path": "./tsconfig.node.json" }], 29 | "ts-node": { 30 | "esm": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "esModuleInterop": true, // https://stackoverflow.com/questions/63744824/getting-express-default-is-not-a-function-error-when-i-run-node-server-in-a-co 21 | "paths": { 22 | "@src/*": ["./src/*"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/node-sh/runJs.sh: -------------------------------------------------------------------------------- 1 | # PARAM 2 | # $1 COMPETITION_ID 3 | # $2 USER_ID 4 | # $3 PROBLEM_ID 5 | # $4 TESTCASE_ID 6 | 7 | # DESCRIPTION 8 | # node로 제출한 파일을 실행한다. 9 | # stdout, stderr는 각각 STDOUT_FILE, STDERR_FILE에 기록된다. 10 | # RESULT_FILE에는 제출한 파일의 solution() 값이 기록되는데, shell script 상에서는 할 수 없어 템플릿 코드에서 기록해주어야 한다. 11 | # node 실행할 때 첫번째 인자로 RESULT_FILE의 파일 경로를 입력해준다. 12 | 13 | SUBMISSION_JS_FILE="/algo-with-me/submissions/$1/$2/$3.js" 14 | STDOUT_FILE="/algo-with-me/submissions/$1/$2/$3.$4.stdout" 15 | STDERR_FILE="/algo-with-me/submissions/$1/$2/$3.$4.stderr" 16 | RESULT_FILE="/algo-with-me/submissions/$1/$2/$3.$4.result" 17 | TIME_FILE="/algo-with-me/submissions/$1/$2/$3.$4.time" 18 | TESTCASE_INPUTFILE="/algo-with-me/testcases/$3/secrets/$4.in" 19 | 20 | node "$SUBMISSION_JS_FILE" "$TESTCASE_INPUTFILE" "$RESULT_FILE" "$TIME_FILE" 1> "$STDOUT_FILE" 2> "$STDERR_FILE" 21 | 22 | exit 0 23 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/dtos/score-result.dto.ts: -------------------------------------------------------------------------------- 1 | export class ScoreResultDto { 2 | constructor( 3 | submissionId: number, 4 | testcaseId: number, 5 | socketId: string, 6 | result: '처리중' | '정답입니다' | '오답입니다' | '시간초과' | '메모리초과', 7 | stdout: string, 8 | stderr: string, 9 | timeUsage: number, 10 | memoryUsage: number, 11 | ) { 12 | this.submissionId = submissionId; 13 | this.testcaseId = testcaseId; 14 | this.socketId = socketId; 15 | this.result = result; 16 | this.stdout = stdout; 17 | this.stderr = stderr; 18 | this.timeUsage = timeUsage; 19 | this.memoryUsage = memoryUsage; 20 | } 21 | 22 | submissionId: number; 23 | testcaseId: number; 24 | socketId: string; 25 | result: '처리중' | '정답입니다' | '오답입니다' | '시간초과' | '메모리초과'; 26 | stdout: string; 27 | stderr: string; 28 | timeUsage: number; 29 | memoryUsage: number; 30 | } 31 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/stack.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface StackProperties { 9 | align?: PropertyValue<'alignItems'> 10 | justify?: PropertyValue<'justifyContent'> 11 | direction?: PropertyValue<'flexDirection'> 12 | gap?: PropertyValue<'gap'> 13 | } 14 | 15 | 16 | interface StackStyles extends StackProperties, DistributiveOmit {} 17 | 18 | interface StackPatternFn { 19 | (styles?: StackStyles): string 20 | raw: (styles?: StackStyles) => SystemStyleObject 21 | } 22 | 23 | 24 | export declare const stack: StackPatternFn; 25 | -------------------------------------------------------------------------------- /fe/src/components/Main/Buttons/GoToCreateCompetitionLink.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { Button, Link, Text } from '@/components/Common'; 4 | import useAuth from '@/hooks/login/useAuth'; 5 | 6 | export default function GoToCreateCompetitionLink() { 7 | const { isLoggedin } = useAuth(); 8 | 9 | const handleNavigate = () => { 10 | if (!isLoggedin) { 11 | alert('로그인이 필요합니다.'); 12 | } 13 | }; 14 | 15 | return ( 16 | 17 | 22 | 23 | ); 24 | } 25 | 26 | const buttonStyle = css({ 27 | width: '120px', 28 | }); 29 | 30 | const LinkTextStyle = css({ 31 | color: 'text', 32 | }); 33 | -------------------------------------------------------------------------------- /fe/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'google', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['simple-import-sort', 'react-refresh'], 13 | rules: { 14 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 15 | 'object-curly-spacing': 'off', 16 | 'require-jsdoc': 'off', 17 | 'max-len': 'off', 18 | 'operator-linebreak': 'off', 19 | indent: 'off', 20 | 'space-before-function-paren': 'off', 21 | 'simple-import-sort/imports': [ 22 | 'error', 23 | { 24 | groups: [['^.+\\.s?css$', '^@style(/.*|$)'], ['^react'], ['^@(/.*|$)']], 25 | }, 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /fe/src/components/Common/Loading.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | size: string; 3 | color: string; 4 | } 5 | 6 | export function Loading({ size, color }: Props) { 7 | return ( 8 | 15 | 24 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/wrap.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface WrapProperties { 9 | gap?: PropertyValue<'gap'> 10 | rowGap?: PropertyValue<'gap'> 11 | columnGap?: PropertyValue<'gap'> 12 | align?: PropertyValue<'alignItems'> 13 | justify?: PropertyValue<'justifyContent'> 14 | } 15 | 16 | 17 | interface WrapStyles extends WrapProperties, DistributiveOmit {} 18 | 19 | interface WrapPatternFn { 20 | (styles?: WrapStyles): string 21 | raw: (styles?: WrapStyles) => SystemStyleObject 22 | } 23 | 24 | 25 | export declare const wrap: WrapPatternFn; 26 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/competition.participant.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | Unique, 8 | } from 'typeorm'; 9 | 10 | import { Competition } from './competition.entity'; 11 | import { User } from './user.entity'; 12 | 13 | @Entity() 14 | @Unique('unique_participant', ['user', 'competition']) 15 | export class CompetitionParticipant { 16 | @PrimaryGeneratedColumn() 17 | id: number; 18 | 19 | @Column() 20 | userId: number; 21 | 22 | @ManyToOne(() => User, (user) => user.competitionParticipant, { nullable: false }) 23 | user: User; 24 | 25 | @Column() 26 | competitionId: number; 27 | 28 | @ManyToOne(() => Competition, (competition) => competition.competitionParticipants, { 29 | nullable: false, 30 | }) 31 | competition: Competition; 32 | 33 | @CreateDateColumn() 34 | createdAt: Date; 35 | } 36 | -------------------------------------------------------------------------------- /fe/src/components/CompetitionDetail/CompetitionDetailInfo.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | import { CompetitionInfo } from '@/apis/competitions'; 6 | 7 | import { Text } from '../Common'; 8 | 9 | interface Props extends HTMLAttributes { 10 | competition: CompetitionInfo; 11 | } 12 | 13 | export default function CompetitionDetailInfo({ competition, className, ...props }: Props) { 14 | return ( 15 |
16 |
17 | 18 | {competition.name} 19 | 20 |
21 | 22 | {competition.detail} 23 | 24 |
25 | ); 26 | } 27 | 28 | const infoContainerStyle = css({ 29 | display: 'flex', 30 | flexDirection: 'column', 31 | gap: '16px', 32 | }); 33 | -------------------------------------------------------------------------------- /fe/src/components/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { Button } from '@/components/Common'; 4 | 5 | interface Props { 6 | onClickLogin: () => void; 7 | } 8 | 9 | export default function Login({ onClickLogin }: Props) { 10 | return ( 11 |
12 |
Algo With Me
13 | 14 |
15 | ); 16 | } 17 | 18 | const loginWrapperStyle = css({ 19 | border: '1px solid white', 20 | borderRadius: '10px', 21 | width: '100%', 22 | height: '100%', 23 | maxWidth: '500px', 24 | maxHeight: '200px', 25 | display: 'flex', 26 | flexDirection: 'column', 27 | justifyContent: 'space-between', 28 | }); 29 | 30 | const loginHeaderStyle = css({ 31 | fontSize: '3rem', 32 | fontWeight: 'bold', 33 | textAlign: 'center', 34 | padding: '1rem', 35 | }); 36 | -------------------------------------------------------------------------------- /fe/styled-system/global.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | :root { 3 | --made-with-panda: '🐼' 4 | } 5 | 6 | *, *::before, *::after, ::backdrop { 7 | --blur: ; 8 | --brightness: ; 9 | --contrast: ; 10 | --grayscale: ; 11 | --hue-rotate: ; 12 | --invert: ; 13 | --saturate: ; 14 | --sepia: ; 15 | --drop-shadow: ; 16 | --backdrop-blur: ; 17 | --backdrop-brightness: ; 18 | --backdrop-contrast: ; 19 | --backdrop-grayscale: ; 20 | --backdrop-hue-rotate: ; 21 | --backdrop-invert: ; 22 | --backdrop-opacity: ; 23 | --backdrop-saturate: ; 24 | --backdrop-sepia: ; 25 | --scroll-snap-strictness: proximity; 26 | --border-spacing-x: 0; 27 | --border-spacing-y: 0; 28 | --translate-x: 0; 29 | --translate-y: 0; 30 | --rotate: 0; 31 | --skew-x: 0; 32 | --skew-y: 0; 33 | --scale-x: 1; 34 | --scale-y: 1 35 | } 36 | } -------------------------------------------------------------------------------- /fe/styled-system/patterns/grid.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface GridProperties { 9 | gap?: PropertyValue<'gap'> 10 | columnGap?: PropertyValue<'gap'> 11 | rowGap?: PropertyValue<'gap'> 12 | columns?: ConditionalValue 13 | minChildWidth?: ConditionalValue 14 | } 15 | 16 | 17 | interface GridStyles extends GridProperties, DistributiveOmit {} 18 | 19 | interface GridPatternFn { 20 | (styles?: GridStyles): string 21 | raw: (styles?: GridStyles) => SystemStyleObject 22 | } 23 | 24 | 25 | export declare const grid: GridPatternFn; 26 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/entities/competition.participant.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | Unique, 8 | } from 'typeorm'; 9 | 10 | import { Competition } from './competition.entity'; 11 | 12 | import { User } from '@src/user/entities/user.entity'; 13 | 14 | @Entity() 15 | @Unique('unique_participant', ['user', 'competition']) 16 | export class CompetitionParticipant { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column() 21 | userId: number; 22 | 23 | @ManyToOne(() => User, (user) => user.competitionParticipant, { nullable: false }) 24 | user: User; 25 | 26 | @Column() 27 | competitionId: number; 28 | 29 | @ManyToOne(() => Competition, (competition) => competition.competitionParticipants, { 30 | nullable: false, 31 | }) 32 | competition: Competition; 33 | 34 | @CreateDateColumn() 35 | createdAt: Date; 36 | } 37 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/divider.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const dividerConfig = { 5 | transform(props, { map }) { 6 | const { orientation = "horizontal", thickness = "1px", color, ...rest } = props; 7 | return { 8 | "--thickness": thickness, 9 | width: map(orientation, (v) => v === "vertical" ? void 0 : "100%"), 10 | height: map(orientation, (v) => v === "horizontal" ? void 0 : "100%"), 11 | borderBlockEndWidth: map(orientation, (v) => v === "horizontal" ? "var(--thickness)" : void 0), 12 | borderInlineEndWidth: map(orientation, (v) => v === "vertical" ? "var(--thickness)" : void 0), 13 | borderColor: color, 14 | ...rest 15 | }; 16 | }} 17 | 18 | export const getDividerStyle = (styles = {}) => dividerConfig.transform(styles, { map: mapObject }) 19 | 20 | export const divider = (styles) => css(getDividerStyle(styles)) 21 | divider.raw = getDividerStyle -------------------------------------------------------------------------------- /fe/src/utils/type/index.ts: -------------------------------------------------------------------------------- 1 | export type Nil = undefined | null; 2 | type Fn = (...args: unknown[]) => unknown; 3 | 4 | export const isNil = (type: unknown): type is Nil => { 5 | if (Object.is(type, null)) return true; 6 | if (Object.is(type, undefined)) return true; 7 | 8 | return false; 9 | }; 10 | 11 | export const isDictionary = (obj: unknown): obj is Record => { 12 | // TODO 테스트 코드 작성하기 13 | 14 | if (obj === null || obj === undefined) return false; 15 | if (Array.isArray(obj)) return false; 16 | if (obj instanceof Function) return false; 17 | if (typeof obj !== 'object') return false; 18 | 19 | return true; 20 | }; 21 | 22 | export const isFunction = (type: unknown): type is Fn => { 23 | if (type instanceof Function) return true; 24 | 25 | return false; 26 | }; 27 | 28 | export const isNumber = (type: unknown): type is number => { 29 | if (typeof type === 'number') return true; 30 | 31 | return false; 32 | }; 33 | -------------------------------------------------------------------------------- /fe/src/components/Dashboard/DashboardStatus.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { HStack, Text } from '../Common'; 4 | 5 | interface Props { 6 | competitionName: string; 7 | competitionStatusText: string; 8 | } 9 | 10 | export default function DashboardStatus({ competitionName, competitionStatusText }: Props) { 11 | return ( 12 | 13 | 14 | {competitionName} 15 | 16 | 17 | {competitionStatusText} 18 | 19 | 20 | ); 21 | } 22 | 23 | const textContainerStyle = css({ 24 | textAlign: 'center', 25 | gap: '10px', 26 | marginTop: '56px', 27 | marginBottom: '47px', 28 | }); 29 | 30 | const competitionNameStyle = css({ 31 | display: 'flex', 32 | height: '116px', 33 | flexDirection: 'column', 34 | justifyContent: 'center', 35 | }); 36 | -------------------------------------------------------------------------------- /fe/src/hooks/competitionDetail/useCompetitionRerenderState.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const TIME_INTERVAL = 1000; 4 | 5 | export function useCompetitionRerender(startsAt: Date, endsAt: Date) { 6 | const [shouldRerenderDuring, setShouldRerenderDuring] = useState(false); 7 | const [shouldRerenderAfter, setShouldRerenderAfter] = useState(false); 8 | 9 | useEffect(() => { 10 | const intervalId = setInterval(() => { 11 | const currentDate = new Date(); 12 | if (currentDate >= startsAt && !shouldRerenderDuring) { 13 | setShouldRerenderDuring(true); 14 | } 15 | 16 | if (currentDate >= endsAt && !shouldRerenderAfter) { 17 | setShouldRerenderAfter(true); 18 | } 19 | }, TIME_INTERVAL); 20 | 21 | return () => clearInterval(intervalId); 22 | }, [startsAt, endsAt, shouldRerenderDuring, shouldRerenderAfter]); 23 | 24 | return { shouldRerenderDuring, shouldRerenderAfter }; 25 | } 26 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/grid.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const gridConfig = { 5 | transform(props, { map }) { 6 | const regex = /\d+(cm|in|pt|em|px|rem|vh|vmax|vmin|vw|ch|lh|%)$/; 7 | const { columnGap, rowGap, gap = columnGap || rowGap ? void 0 : "10px", columns, minChildWidth, ...rest } = props; 8 | const getValue = (v) => regex.test(v) ? v : `token(sizes.${v}, ${v})`; 9 | return { 10 | display: "grid", 11 | gridTemplateColumns: columns != null ? map(columns, (v) => `repeat(${v}, minmax(0, 1fr))`) : minChildWidth != null ? map(minChildWidth, (v) => `repeat(auto-fit, minmax(${getValue(v)}, 1fr))`) : void 0, 12 | gap, 13 | columnGap, 14 | rowGap, 15 | ...rest 16 | }; 17 | }} 18 | 19 | export const getGridStyle = (styles = {}) => gridConfig.transform(styles, { map: mapObject }) 20 | 21 | export const grid = (styles) => css(getGridStyle(styles)) 22 | grid.raw = getGridStyle -------------------------------------------------------------------------------- /fe/styled-system/patterns/divider.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface DividerProperties { 9 | orientation?: ConditionalValue<"horizontal" | "vertical"> 10 | thickness?: ConditionalValue 11 | color?: ConditionalValue 12 | } 13 | 14 | 15 | interface DividerStyles extends DividerProperties, DistributiveOmit {} 16 | 17 | interface DividerPatternFn { 18 | (styles?: DividerStyles): string 19 | raw: (styles?: DividerStyles) => SystemStyleObject 20 | } 21 | 22 | 23 | export declare const divider: DividerPatternFn; 24 | -------------------------------------------------------------------------------- /fe/src/components/Problem/ProblemHeader.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | import type { ProblemInfo } from '@/apis/problems'; 6 | import { Text } from '@/components/Common'; 7 | 8 | interface Props extends HTMLAttributes { 9 | problem: ProblemInfo; 10 | } 11 | 12 | export function ProblemHeader({ problem, className, ...props }: Props) { 13 | return ( 14 |
15 | 16 | {problem.title} 17 | 18 |
19 | ); 20 | } 21 | 22 | const style = css({ 23 | borderBottom: '1px solid', 24 | borderColor: 'border', 25 | }); 26 | 27 | const problemTitleStyle = css({ 28 | display: 'inline-block', 29 | paddingY: '1rem', 30 | paddingX: '1.25rem', 31 | marginLeft: '4rem', 32 | color: 'brand', 33 | borderBottom: '2px solid', 34 | borderColor: 'brand', 35 | }); 36 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { DashboardController } from './dashboard.controller'; 5 | import { DashboardGateway } from './dashboard.gateway'; 6 | import { DashboardService } from './dashboard.service'; 7 | import { Dashboard } from './entities/dashboard.entity'; 8 | 9 | import { CompetitionModule } from '@src/competition/competition.module'; 10 | import { Competition } from '@src/competition/entities/competition.entity'; 11 | import { CompetitionProblem } from '@src/competition/entities/competition.problem.entity'; 12 | 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([Dashboard, CompetitionProblem, Competition]), 16 | forwardRef(() => CompetitionModule), 17 | ], 18 | providers: [DashboardGateway, DashboardService], 19 | controllers: [DashboardController], 20 | exports: [DashboardService], 21 | }) 22 | export class DashboardModule {} 23 | -------------------------------------------------------------------------------- /fe/src/components/Common/Link.tsx: -------------------------------------------------------------------------------- 1 | import { cva, cx } from '@style/css'; 2 | 3 | import type { RefAttributes } from 'react'; 4 | import type { LinkProps } from 'react-router-dom'; 5 | import { Link as _Link } from 'react-router-dom'; 6 | 7 | import { Text } from './Text'; 8 | 9 | interface Props extends LinkProps, RefAttributes { 10 | underline?: boolean; 11 | } 12 | 13 | export function Link({ className, children, underline = true, ...props }: Props) { 14 | return ( 15 | <_Link className={cx(className, style({ underline }))} {...props}> 16 | {children} 17 | 18 | ); 19 | } 20 | 21 | const style = cva({ 22 | base: { 23 | color: 'brand', 24 | _hover: { 25 | color: 'brand.alt', 26 | }, 27 | }, 28 | variants: { 29 | underline: { 30 | true: { 31 | textDecoration: 'underline', 32 | }, 33 | false: { 34 | textDecoration: 'none', 35 | }, 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/grid-item.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface GridItemProperties { 9 | colSpan?: ConditionalValue 10 | rowSpan?: ConditionalValue 11 | colStart?: ConditionalValue 12 | rowStart?: ConditionalValue 13 | colEnd?: ConditionalValue 14 | rowEnd?: ConditionalValue 15 | } 16 | 17 | 18 | interface GridItemStyles extends GridItemProperties, DistributiveOmit {} 19 | 20 | interface GridItemPatternFn { 21 | (styles?: GridItemStyles): string 22 | raw: (styles?: GridItemStyles) => SystemStyleObject 23 | } 24 | 25 | 26 | export declare const gridItem: GridItemPatternFn; 27 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 5 | 6 | import { AppModule } from '@src/app.module'; 7 | 8 | function setSwagger(app: INestApplication) { 9 | const config = new DocumentBuilder() 10 | .setTitle('algo-with-me-api') 11 | .setDescription('algo with me API description') 12 | .setVersion('1.0') 13 | .addBearerAuth() 14 | .build(); 15 | 16 | const document = SwaggerModule.createDocument(app, config); 17 | SwaggerModule.setup('api', app, document); 18 | } 19 | 20 | async function bootstrap() { 21 | const app = await NestFactory.create(AppModule, { cors: true }); 22 | // app.useGlobalFilters(new ServiceExceptionFilter()); 23 | app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); 24 | setSwagger(app); 25 | await app.listen(3000); 26 | } 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/flex.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface FlexProperties { 9 | align?: PropertyValue<'alignItems'> 10 | justify?: PropertyValue<'justifyContent'> 11 | direction?: PropertyValue<'flexDirection'> 12 | wrap?: PropertyValue<'flexWrap'> 13 | basis?: PropertyValue<'flexBasis'> 14 | grow?: PropertyValue<'flexGrow'> 15 | shrink?: PropertyValue<'flexShrink'> 16 | } 17 | 18 | 19 | interface FlexStyles extends FlexProperties, DistributiveOmit {} 20 | 21 | interface FlexPatternFn { 22 | (styles?: FlexStyles): string 23 | raw: (styles?: FlexStyles) => SystemStyleObject 24 | } 25 | 26 | 27 | export declare const flex: FlexPatternFn; 28 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | 5 | import { GithubStrategy, JWTStrategy } from './auth.strategy'; 6 | import { AuthController } from './controllers/auth.controller'; 7 | import { AuthService } from './services/auth.service'; 8 | 9 | import { UserModule } from '@src/user/user.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | UserModule, 14 | JwtModule.registerAsync({ 15 | useFactory: async (configService: ConfigService) => { 16 | return { 17 | signOptions: { expiresIn: configService.get('JWT_ACCESSTOKEN_EXPIRE_TIME') }, 18 | secret: configService.get('JWT_SECRET'), 19 | }; 20 | }, 21 | inject: [ConfigService], 22 | }), 23 | ], 24 | controllers: [AuthController], 25 | providers: [GithubStrategy, AuthService, JWTStrategy], 26 | exports: [AuthService], 27 | }) 28 | export class AuthModule {} 29 | -------------------------------------------------------------------------------- /fe/src/utils/socket/index.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from '@/utils/type'; 2 | 3 | import type { ManagerOptions, SocketOptions } from 'socket.io-client'; 4 | import { io, type Socket } from 'socket.io-client'; 5 | 6 | const SOCKET_URL = import.meta.env.VITE_API_URL; 7 | 8 | export type ConnectOptions = Partial; 9 | 10 | export function createSocketInstance(url: string, opts: ConnectOptions = {}) { 11 | return io(`${SOCKET_URL}${url}`, opts); 12 | } 13 | 14 | type SocketDict = Record; 15 | const socketDict: SocketDict = {}; 16 | 17 | export function connect(url: string, opts: ConnectOptions = {}) { 18 | if (!isNil(socketDict[url])) return socketDict[url] as Socket; 19 | socketDict[url] = createSocketInstance(url, opts); 20 | 21 | return socketDict[url] as Socket; 22 | } 23 | 24 | export function disconnect(url: string) { 25 | if (isNil(socketDict[url])) return; 26 | socketDict[url]?.disconnect(); 27 | socketDict[url] = undefined; 28 | } 29 | 30 | export type { Socket }; 31 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/aspect-ratio.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const aspectRatioConfig = { 5 | transform(props, { map }) { 6 | const { ratio = 4 / 3, ...rest } = props; 7 | return { 8 | position: "relative", 9 | _before: { 10 | content: `""`, 11 | display: "block", 12 | height: "0", 13 | paddingBottom: map(ratio, (r) => `${1 / r * 100}%`) 14 | }, 15 | "&>*": { 16 | display: "flex", 17 | justifyContent: "center", 18 | alignItems: "center", 19 | overflow: "hidden", 20 | position: "absolute", 21 | inset: "0", 22 | width: "100%", 23 | height: "100%" 24 | }, 25 | "&>img, &>video": { 26 | objectFit: "cover" 27 | }, 28 | ...rest 29 | }; 30 | }} 31 | 32 | export const getAspectRatioStyle = (styles = {}) => aspectRatioConfig.transform(styles, { map: mapObject }) 33 | 34 | export const aspectRatio = (styles) => css(getAspectRatioStyle(styles)) 35 | aspectRatio.raw = getAspectRatioStyle -------------------------------------------------------------------------------- /fe/src/__mocks__/algoWithMeApi.ts: -------------------------------------------------------------------------------- 1 | import algoWithMeApiData from '@/__mocks__/algoWithMeApiData.json'; 2 | 3 | import { delay, http, HttpResponse } from 'msw'; 4 | 5 | interface PostLoginReqBody { 6 | testcaseId: string; 7 | solutionCode: string; 8 | } 9 | 10 | export const algoWithMeApi = [ 11 | // TODO API url을 어떻게 설정할 지 다음주에 백엔드와 논의 필요. 12 | http.post('/algo-with-me-api', async ({ request }) => { 13 | const { solutionCode, testcaseId } = (await request.json()) as PostLoginReqBody; 14 | // 아래 로직은 websocket을 msw로 구현하는 것이 어렵거나 불가능해서 임시로 넣은 로직 15 | // client가 testcaseId에 해당하는 값을 post로 보내면 16 | // msw로 랜덤한 delay를 준 후에 response로 해당 testcaseId에 해당하는 mockSolutionResult를 보내줌 17 | 18 | // 해당 테스트 케이스에 대한 정답 여부 19 | const filterData = algoWithMeApiData.find((result) => result.testcaseId === Number(testcaseId)); 20 | const delayTime = Math.floor(Math.random() * 10000); 21 | 22 | console.log(solutionCode, '해당 코드 푸는 중...'); 23 | await delay(delayTime); 24 | return HttpResponse.json(filterData); 25 | }), 26 | ]; 27 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/create-problem.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | 4 | import { Problem } from '../entities/problem.entity'; 5 | 6 | export class CreateProblemDto { 7 | @ApiProperty() 8 | @IsNotEmpty() 9 | title: string; 10 | 11 | @ApiProperty() 12 | @IsNotEmpty() 13 | timeLimit: number; 14 | 15 | @ApiProperty() 16 | @IsNotEmpty() 17 | memoryLimit: number; 18 | 19 | @ApiProperty() 20 | @IsNotEmpty() 21 | testcaseNum: number; 22 | 23 | @ApiProperty() 24 | @IsNotEmpty() 25 | frameCode: string; 26 | 27 | @ApiProperty() 28 | @IsNotEmpty() 29 | solutionCode: string; 30 | 31 | toEntity(): Problem { 32 | const problem = new Problem(); 33 | problem.title = this.title; 34 | problem.timeLimit = this.timeLimit; 35 | problem.memoryLimit = this.memoryLimit; 36 | problem.testcaseNum = this.testcaseNum; 37 | problem.frameCode = this.frameCode; 38 | problem.solutionCode = this.solutionCode; 39 | return problem; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /fe/src/hooks/dashboard/useDashboardRenderState.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const TIME_INTERVAL = 1000; 4 | const ADDITIONAL_BUFFER_TIME = 3 * 1000; 5 | 6 | export function useDashboardRerenderState(endsAt: Date, bufferTimeAfterCompetitionEnd: Date) { 7 | const [shouldRenderLoading, setShouldRenderLoading] = useState(false); 8 | 9 | useEffect(() => { 10 | const intervalId = setInterval(() => { 11 | const currentDate = new Date(); 12 | 13 | if ( 14 | currentDate >= endsAt && 15 | currentDate < new Date(bufferTimeAfterCompetitionEnd.getTime() + ADDITIONAL_BUFFER_TIME) 16 | ) { 17 | setShouldRenderLoading(true); 18 | } 19 | 20 | if ( 21 | currentDate >= new Date(bufferTimeAfterCompetitionEnd.getTime() + ADDITIONAL_BUFFER_TIME) 22 | ) { 23 | setShouldRenderLoading(false); 24 | } 25 | }, TIME_INTERVAL); 26 | 27 | return () => clearInterval(intervalId); 28 | }, [endsAt, shouldRenderLoading, bufferTimeAfterCompetitionEnd]); 29 | 30 | return shouldRenderLoading; 31 | } 32 | -------------------------------------------------------------------------------- /fe/styled-system/css/sva.mjs: -------------------------------------------------------------------------------- 1 | import { getSlotRecipes, splitProps } from '../helpers.mjs'; 2 | import { cva } from './cva.mjs'; 3 | 4 | export function sva(config) { 5 | const slots = Object.entries(getSlotRecipes(config)).map(([slot, slotCva]) => [slot, cva(slotCva)]) 6 | 7 | function svaFn(props) { 8 | const result = slots.map(([slot, cvaFn]) => [slot, cvaFn(props)]) 9 | return Object.fromEntries(result) 10 | } 11 | 12 | function raw(props) { 13 | const result = slots.map(([slot, cvaFn]) => [slot, cvaFn.raw(props)]) 14 | return Object.fromEntries(result) 15 | } 16 | 17 | const variants = config.variants ?? {}; 18 | const variantKeys = Object.keys(variants); 19 | 20 | function splitVariantProps(props) { 21 | return splitProps(props, variantKeys); 22 | } 23 | 24 | const variantMap = Object.fromEntries( 25 | Object.entries(variants).map(([key, value]) => [key, Object.keys(value)]) 26 | ); 27 | 28 | return Object.assign(svaFn, { 29 | __cva__: false, 30 | raw, 31 | variantMap, 32 | variantKeys, 33 | splitVariantProps, 34 | }) 35 | } -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/problem.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | OneToMany, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { CompetitionProblem } from './competition.problem.entity'; 11 | import { Submission } from './submission.entity'; 12 | 13 | @Entity() 14 | export class Problem { 15 | @PrimaryGeneratedColumn() 16 | id: number; 17 | 18 | @Column() 19 | title: string; 20 | 21 | @Column() 22 | timeLimit: number; 23 | 24 | @Column() 25 | memoryLimit: number; 26 | 27 | @Column() 28 | testcaseNum: number; 29 | 30 | @Column('text') 31 | frameCode: string; 32 | 33 | @Column('text') 34 | solutionCode: string; 35 | 36 | @OneToMany(() => Submission, (submission) => submission.problem) 37 | submissions: Submission[]; 38 | 39 | @OneToMany(() => CompetitionProblem, (competitionProblem) => competitionProblem.problem) 40 | competitionProblems: CompetitionProblem[]; 41 | 42 | @CreateDateColumn() 43 | createdAt: Date; 44 | 45 | @UpdateDateColumn() 46 | updatedAt: Date; 47 | } 48 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/entities/problem.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | OneToMany, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { CompetitionProblem } from './competition.problem.entity'; 11 | import { Submission } from './submission.entity'; 12 | 13 | @Entity() 14 | export class Problem { 15 | @PrimaryGeneratedColumn() 16 | id: number; 17 | 18 | @Column() 19 | title: string; 20 | 21 | @Column() 22 | timeLimit: number; 23 | 24 | @Column() 25 | memoryLimit: number; 26 | 27 | @Column() 28 | testcaseNum: number; 29 | 30 | @Column('text') 31 | frameCode: string; 32 | 33 | @Column('text') 34 | solutionCode: string; 35 | 36 | @OneToMany(() => Submission, (submission) => submission.problem) 37 | submissions: Submission[]; 38 | 39 | @OneToMany(() => CompetitionProblem, (competitionProblem) => competitionProblem.problem) 40 | competitionProblems: CompetitionProblem[]; 41 | 42 | @CreateDateColumn() 43 | createdAt: Date; 44 | 45 | @UpdateDateColumn() 46 | updatedAt: Date; 47 | } 48 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/float.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SystemStyleObject, ConditionalValue } from '../types/index'; 3 | import type { Properties } from '../types/csstype'; 4 | import type { PropertyValue } from '../types/prop-type'; 5 | import type { DistributiveOmit } from '../types/system-types'; 6 | import type { Tokens } from '../tokens/index'; 7 | 8 | export interface FloatProperties { 9 | offsetX?: ConditionalValue 10 | offsetY?: ConditionalValue 11 | offset?: ConditionalValue 12 | placement?: ConditionalValue<"bottom-end" | "bottom-start" | "top-end" | "top-start" | "bottom-center" | "top-center" | "middle-center" | "middle-end" | "middle-start"> 13 | } 14 | 15 | 16 | interface FloatStyles extends FloatProperties, DistributiveOmit {} 17 | 18 | interface FloatPatternFn { 19 | (styles?: FloatStyles): string 20 | raw: (styles?: FloatStyles) => SystemStyleObject 21 | } 22 | 23 | 24 | export declare const float: FloatPatternFn; 25 | -------------------------------------------------------------------------------- /fe/src/hooks/dashboard/useRemainingTimeCounter.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const TIME_INTERVAL = 1000; 4 | 5 | export function useRemainingTimeCounter(endsAt: Date) { 6 | const [remainingTime, setRemainingTime] = useState(''); 7 | 8 | useEffect(() => { 9 | const endsAtDate = new Date(endsAt); 10 | 11 | const calculateRemainingTime = () => { 12 | const currentTime = new Date(); 13 | const timeDifference = endsAtDate.getTime() - currentTime.getTime(); 14 | 15 | if (timeDifference > 0) { 16 | const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60)); 17 | const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000); 18 | 19 | return `${minutes}분 ${seconds}초`; 20 | } 21 | 22 | return ''; 23 | }; 24 | 25 | const intervalId = setInterval(() => { 26 | const newRemainingTime = calculateRemainingTime(); 27 | setRemainingTime(newRemainingTime); 28 | }, TIME_INTERVAL); 29 | 30 | return () => clearInterval(intervalId); 31 | }, [endsAt]); 32 | 33 | return remainingTime; 34 | } 35 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/user/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | 5 | import { UserResponseDto } from '../dto/user.response.dto'; 6 | import { User } from '../entities/user.entity'; 7 | 8 | @Injectable() 9 | export class UserService { 10 | constructor(@InjectRepository(User) private readonly userRepository: Repository) {} 11 | 12 | async saveOrGetByEmail(email: string) { 13 | const user = await this.userRepository.findOneBy({ email }); 14 | if (!user) { 15 | const savedUser = await this.userRepository.save({ 16 | email: email, 17 | nickname: email, 18 | }); 19 | return new UserResponseDto(savedUser.email, savedUser.nickname); 20 | } 21 | return new UserResponseDto(user.email, user.nickname); 22 | } 23 | 24 | async getByEmail(email: string) { 25 | const user = await this.userRepository.findOneBy({ email }); 26 | if (!user) throw new NotFoundException(`${email} 에 해당하는 유저를 찾을 수 없습니다.`); 27 | return user; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fe/src/utils/observer/__tests__/observer.spec.ts: -------------------------------------------------------------------------------- 1 | import { createObserver } from '../index'; 2 | import { expect, it, vi } from 'vitest'; 3 | 4 | it('옵저버는 구독할 수 있으며, 발행시 발행 값을 받아볼 수 있다.', () => { 5 | const observer = createObserver(); 6 | const fakeFn = vi.fn(); 7 | observer.subscribe(fakeFn); 8 | 9 | observer.notify(1); 10 | 11 | expect(fakeFn).toBeCalledWith(1); 12 | }); 13 | 14 | it('여러 명의 구독자를 둘 수 있다.', () => { 15 | const observer = createObserver(); 16 | const fakeFn1 = vi.fn(); 17 | const fakeFn2 = vi.fn(); 18 | 19 | observer.subscribe(fakeFn1); 20 | observer.subscribe(fakeFn2); 21 | 22 | observer.notify(1); 23 | 24 | expect(fakeFn1).toBeCalledWith(1); 25 | expect(fakeFn2).toBeCalledWith(1); 26 | }); 27 | 28 | it('구독을 해지할 수 있다.', () => { 29 | const observer = createObserver(); 30 | const fakeFn1 = vi.fn(); 31 | const fakeFn2 = vi.fn(); 32 | 33 | const unsubscriber = observer.subscribe(fakeFn1); 34 | observer.subscribe(fakeFn2); 35 | 36 | unsubscriber(); 37 | observer.notify(1); 38 | 39 | expect(fakeFn1).not.toBeCalledWith(1); 40 | expect(fakeFn2).toBeCalledWith(1); 41 | }); 42 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { Competition } from './competition.entity'; 12 | import { CompetitionParticipant } from './competition.participant.entity'; 13 | import { Submission } from './submission.entity'; 14 | 15 | @Entity() 16 | export class User { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column() 21 | @IsEmail() 22 | email: string; 23 | 24 | @Column() 25 | nickname: string; 26 | 27 | @OneToMany(() => CompetitionParticipant, (competitionParticipant) => competitionParticipant.user) 28 | competitionParticipant: CompetitionParticipant[]; 29 | 30 | @OneToMany(() => Submission, (submission) => submission.user) 31 | submissions: Submission[]; 32 | 33 | @OneToMany(() => Competition, (competition) => competition.user) 34 | competitions: Competition[]; 35 | 36 | @CreateDateColumn() 37 | createdAt: Date; 38 | 39 | @UpdateDateColumn() 40 | updatedAt: Date; 41 | } 42 | -------------------------------------------------------------------------------- /fe/src/index.css: -------------------------------------------------------------------------------- 1 | @layer reset, base, tokens, recipes, utilities; 2 | 3 | @font-face { 4 | font-family: 'Pretendard-Regular'; 5 | src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') 6 | format('woff'); 7 | font-weight: 400; 8 | font-style: normal; 9 | } 10 | 11 | :root { 12 | font-family: 'Pretendard-Regular', Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 13 | line-height: 1.5; 14 | font-weight: 400; 15 | 16 | color-scheme: light dark; 17 | font-synthesis: none; 18 | text-rendering: optimizeLegibility; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-text-size-adjust: 100%; 22 | } 23 | 24 | html:has(dialog[open]) { 25 | overflow: hidden; 26 | } 27 | 28 | body { 29 | margin: 0; 30 | display: flex; 31 | place-items: center; 32 | min-width: 320px; 33 | min-height: 100vh; 34 | } 35 | 36 | h1 { 37 | font-size: 3.2em; 38 | line-height: 1.1; 39 | } 40 | 41 | #root { 42 | width: 100%; 43 | height: 100%; 44 | min-width: 100vw; 45 | min-height: 100vh; 46 | background-color: #263238; 47 | } 48 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/score.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { Competition } from './entities/competition.entity'; 5 | import { CompetitionParticipant } from './entities/competition.participant.entity'; 6 | import { CompetitionProblem } from './entities/competition.problem.entity'; 7 | import { Problem } from './entities/problem.entity'; 8 | import { Submission } from './entities/submission.entity'; 9 | import { User } from './entities/user.entity'; 10 | import { FetchService } from './services/fetch.service'; 11 | import { FilesystemService } from './services/filesystem.service'; 12 | import { SubmissionConsumer } from './services/score.consumer'; 13 | import { ScoreService } from './services/score.service'; 14 | 15 | @Module({ 16 | imports: [ 17 | TypeOrmModule.forFeature([ 18 | Competition, 19 | Submission, 20 | Problem, 21 | User, 22 | CompetitionProblem, 23 | CompetitionParticipant, 24 | ]), 25 | ], 26 | controllers: [], 27 | providers: [SubmissionConsumer, FilesystemService, ScoreService, FetchService], 28 | }) 29 | export class ScoreModule {} 30 | -------------------------------------------------------------------------------- /fe/src/pages/CompetitionDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { useParams } from 'react-router-dom'; 4 | 5 | import { CompetitionDetailContent } from '@/components/CompetitionDetail/CompetitionDetailContent'; 6 | import Header from '@/components/Header'; 7 | import { PageLayout } from '@/components/Layout/PageLayout'; 8 | import { useCompetition } from '@/hooks/competition'; 9 | 10 | export default function CompetitionDetailPage() { 11 | const { id } = useParams<{ id: string }>(); 12 | const competitionId: number = id ? parseInt(id, 10) : -1; 13 | const { competition } = useCompetition(competitionId); 14 | // 대회 상태에 따른 페이지를 구성하기 위해 현재 날짜, 시작 시간, 종료 시간을 가져옴 15 | 16 | return ( 17 | 18 |
19 | 26 | 27 | ); 28 | } 29 | 30 | const pageStyle = css({ 31 | display: 'flex', 32 | flexDirection: 'column', 33 | alignItems: 'center', 34 | gap: '136px', 35 | minHeight: '100vh', 36 | }); 37 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const child_process = require('node:child_process'); 3 | 4 | const app = express(); 5 | const PORT = 5000; 6 | const TIMEOUT_IN_MILLI = 10_000; 7 | 8 | app.post('/:competitionId/:userId/:problemId/:testcaseId', (req, res) => { 9 | const {competitionId, userId, problemId, testcaseId} = req.params; 10 | // const result = execute(`/algo-with-me/node-sh/run.sh ${competitionId} ${userId} ${problemId}`); // docker instance용 11 | // const result = execute(`./node-sh/run.sh ${competitionId} ${userId} ${problemId}`); // local test용 12 | 13 | const responseJson = { result: '', competitionId, userId, problemId }; 14 | const command = `/algo-with-me/node-sh/run.sh ${competitionId} ${userId} ${problemId} ${testcaseId}`; 15 | try { 16 | child_process.execSync(command, { timeout: TIMEOUT_IN_MILLI }); 17 | responseJson.result = 'SUCCESS'; 18 | } catch (error) { 19 | responseJson.result = error.code === 'ETIMEDOUT'? 'TIMEOUT' : error.code; 20 | } 21 | 22 | res.send(JSON.stringify(responseJson)); 23 | }) 24 | 25 | app.listen(PORT, () => { 26 | console.log(`[algo-with-me-docker] listening at port ${PORT}`); 27 | }) 28 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { Competition } from '@src/competition/entities/competition.entity'; 12 | import { CompetitionParticipant } from '@src/competition/entities/competition.participant.entity'; 13 | import { Submission } from '@src/competition/entities/submission.entity'; 14 | 15 | @Entity() 16 | export class User { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column() 21 | @IsEmail() 22 | email: string; 23 | 24 | @Column() 25 | nickname: string; 26 | 27 | @OneToMany(() => CompetitionParticipant, (competitionParticipant) => competitionParticipant.user) 28 | competitionParticipant: CompetitionParticipant[]; 29 | 30 | @OneToMany(() => Submission, (submission) => submission.user) 31 | submissions: Submission[]; 32 | 33 | @OneToMany(() => Competition, (competition) => competition.user) 34 | competitions: Competition[]; 35 | 36 | @CreateDateColumn() 37 | createdAt: Date; 38 | 39 | @UpdateDateColumn() 40 | updatedAt: Date; 41 | } 42 | -------------------------------------------------------------------------------- /fe/src/components/CompetitionDetail/Buttons/JoinCompetitionButton.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import { joinCompetition } from '@/apis/joinCompetition'; 4 | import type { CompetitionApiData } from '@/apis/joinCompetition/types'; 5 | import { Button } from '@/components/Common'; 6 | import useAuth from '@/hooks/login/useAuth'; 7 | 8 | const TOKEN_KEY = 'accessToken'; 9 | 10 | export default function JoinCompetitionButton(props: { id: number }) { 11 | const { isLoggedin } = useAuth(); 12 | const navigate = useNavigate(); 13 | 14 | const queryParams = new URLSearchParams(location.search); 15 | const token = queryParams.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY); 16 | 17 | const handleJoinClick = async () => { 18 | if (!isLoggedin) { 19 | alert('로그인이 필요합니다.'); 20 | navigate('/login'); 21 | return; 22 | } 23 | 24 | const result = await joinCompetition(competitionData); 25 | alert(result); 26 | navigate(0); 27 | }; 28 | const competitionData: CompetitionApiData = { 29 | id: props.id, 30 | token: token, 31 | }; 32 | 33 | return ( 34 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /fe/src/apis/problems/types.ts: -------------------------------------------------------------------------------- 1 | import type { SimulationInput } from '@/hooks/simulation'; 2 | 3 | export type ProblemId = number; 4 | export type Problem = { 5 | id: number; 6 | title: string; 7 | timeLimit: number; 8 | memoryLimit: number; 9 | content: string; 10 | createdAt: string; 11 | }; 12 | export type ProblemInfo = { 13 | id: ProblemId; 14 | title: string; 15 | }; 16 | interface TestcaseBaseDictionary { 17 | name: string; 18 | type: string; 19 | } 20 | 21 | type ValueType = 'string' | 'number' | 'boolean'; 22 | 23 | type InputType = ValueType | ValueType[] | ValueType[][]; 24 | 25 | interface TestcaseDataDictionary { 26 | input: InputType[]; 27 | output: ValueType; 28 | } 29 | 30 | export interface Testcase { 31 | data: TestcaseDataDictionary[]; 32 | input: TestcaseBaseDictionary[]; 33 | output: TestcaseBaseDictionary; 34 | } 35 | 36 | export type CompetitionProblem = { 37 | id: ProblemId; 38 | title: string; 39 | timeLimit: number; 40 | memoryLimit: number; 41 | content: string; 42 | solutionCode: string; 43 | testcases: SimulationInput[]; 44 | createdAt: string; 45 | }; 46 | 47 | export type FetchProblemListResponse = ProblemInfo[]; 48 | export type FetchCompetitionProblemResponse = CompetitionProblem; 49 | -------------------------------------------------------------------------------- /fe/src/components/Common/VStack/VStack.tsx: -------------------------------------------------------------------------------- 1 | import { css, cva, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | export interface Props extends HTMLAttributes { 6 | as?: React.ElementType; 7 | alignItems?: 'flexStart' | 'flexEnd' | 'baseline' | 'stretch' | 'center'; 8 | } 9 | 10 | export function VStack({ 11 | children, 12 | className, 13 | as = 'div', 14 | alignItems = 'flexStart', 15 | ...props 16 | }: Props) { 17 | const As = as; 18 | 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | 26 | const rowListStyle = css({ 27 | display: 'flex', 28 | }); 29 | 30 | const alignItemStyle = cva({ 31 | defaultVariants: { 32 | alignItems: 'flexStart', 33 | }, 34 | variants: { 35 | alignItems: { 36 | flexStart: { 37 | alignItems: 'flex-start', 38 | }, 39 | flexEnd: { 40 | alignItems: 'flex-end', 41 | }, 42 | baseline: { 43 | alignItems: 'baseline', 44 | }, 45 | stretch: { 46 | alignItems: 'stretch', 47 | }, 48 | center: { 49 | alignItems: 'center', 50 | }, 51 | }, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | 4 | import { AuthTokenPayloadDto } from '../dto/auth.token.payload.dto'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | constructor(private readonly jwtService: JwtService) {} 9 | 10 | async getGithubPrimaryEmail(accessToken: string): Promise { 11 | const res: Response = await fetch('https://api.github.com/user/emails', { 12 | method: 'GET', 13 | headers: { 14 | Authorization: `Bearer ${accessToken}`, 15 | }, 16 | }); 17 | 18 | // github에 primary로 등록된 이메일을 가져온다. 19 | const emails: object[] = await res.json(); 20 | const email = emails.find((element) => { 21 | if (element['primary']) return element; 22 | }); 23 | return email['email']; 24 | } 25 | 26 | verifyToken(token: string): AuthTokenPayloadDto { 27 | if (!token) throw new UnauthorizedException('토큰을 찾을 수 없습니다.'); 28 | const tokens = token.split(' '); 29 | if (tokens.length !== 2) throw new UnauthorizedException('토큰의 양식이 올바르지 않습니다.'); 30 | try { 31 | return this.jwtService.verify(tokens[1]); 32 | } catch { 33 | throw new UnauthorizedException('유효하지 않은 토큰입니다.'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /fe/src/components/Common/Chip.tsx: -------------------------------------------------------------------------------- 1 | import { css, cva, cx } from '@style/css'; 2 | 3 | import { Text, type TextProps } from './Text'; 4 | 5 | type Theme = 'success' | 'danger' | 'warning' | 'info'; 6 | 7 | interface Props extends TextProps { 8 | theme: Theme; 9 | } 10 | 11 | export function Chip({ className, children, theme = 'info', ...props }: Props) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | 19 | const style = css({ 20 | borderRadius: '9999px', 21 | paddingX: '1rem', 22 | paddingY: '0.25rem', 23 | }); 24 | 25 | const themeStyle = cva({ 26 | base: { 27 | border: '1px solid', 28 | }, 29 | variants: { 30 | theme: { 31 | success: { 32 | borderColor: 'alert.success !important', 33 | background: 'alert.success.dark', 34 | }, 35 | warning: { 36 | borderColor: 'alert.warning !important', 37 | background: 'alert.warning.dark', 38 | }, 39 | danger: { 40 | borderColor: 'alert.danger !important', 41 | background: 'alert.danger.dark', 42 | }, 43 | info: { 44 | borderColor: 'alert.info !important', 45 | background: 'alert.info.dark', 46 | }, 47 | }, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /fe/src/apis/competitions/index.ts: -------------------------------------------------------------------------------- 1 | import api from '@/utils/api'; 2 | 3 | import type { 4 | CompetitionForm, 5 | CompetitionInfo, 6 | CreateCompetitionResponse, 7 | FetchCompetitionResponse, 8 | } from './types'; 9 | 10 | export * from './types'; 11 | 12 | export async function fetchCompetition(competitionId: number): Promise { 13 | try { 14 | const { data } = await api.get(`/competitions/${competitionId}`); 15 | return data; 16 | } catch (err) { 17 | console.error('Error fetching competition data:', err); 18 | return null; 19 | } 20 | } 21 | 22 | export async function createCompetition( 23 | competitionForm: CompetitionForm, 24 | ): Promise { 25 | const { name, detail, maxParticipants, startsAt, endsAt, problemIds } = competitionForm; 26 | 27 | try { 28 | const form = { 29 | name, 30 | detail, 31 | maxParticipants, 32 | startsAt, 33 | endsAt, 34 | problemIds, 35 | }; 36 | const { data } = await api.post('/competitions', form, { 37 | headers: { 38 | Authorization: `Bearer ${localStorage.getItem('accessToken')}`, 39 | }, 40 | }); 41 | 42 | return data; 43 | } catch (err) { 44 | console.error(err); 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/auth/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Redirect, Req, UseGuards } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 5 | 6 | @ApiTags('인증(auths)') 7 | @Controller('auths') 8 | export class AuthController { 9 | constructor(private jwtService: JwtService) {} 10 | 11 | @Get('github') 12 | @ApiOperation({ 13 | summary: 'github 로그인/회원가입', 14 | description: 'github 로그인/회원가입 api 입니다.', 15 | }) 16 | @UseGuards(AuthGuard('github')) 17 | async login() {} 18 | 19 | @Get('github/callback') 20 | @UseGuards(AuthGuard('github')) 21 | @Redirect() 22 | @ApiOperation({ 23 | summary: '깃허브 인증 완료시 리다이렉트 되는 api', 24 | description: '깃허브 인증이 완료되면 리다이렉트 되는 api 입니다. accessToken을 반환합니다.', 25 | }) 26 | async authCallback(@Req() req) { 27 | const content = { 28 | sub: req.user.email, 29 | nickname: req.user.nickname, 30 | }; 31 | return { 32 | url: `https://www.algo-with-me.site?accessToken=${this.jwtService.sign( 33 | content, 34 | )}`, 35 | }; 36 | } 37 | 38 | // 인증 테스트 api 39 | @Get('/tests') 40 | @ApiBearerAuth() 41 | @UseGuards(AuthGuard('jwt')) 42 | test(@Req() req) { 43 | return req.user; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fe/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /fe/src/components/Competition/CompetitionProblemSelector.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | 5 | import { Button, HStack } from '../Common'; 6 | 7 | interface Props extends HTMLAttributes { 8 | problemIds: number[]; 9 | currentIndex: number; 10 | onChangeProblemIndex: (index: number) => void; 11 | } 12 | 13 | export default function CompetitionProblemSelector({ 14 | problemIds, 15 | currentIndex, 16 | onChangeProblemIndex, 17 | ...props 18 | }: Props) { 19 | function handleChangeProblemIndex(index: number) { 20 | onChangeProblemIndex(index); 21 | } 22 | 23 | return ( 24 | 25 | {problemIds.map((id: number, index: number) => ( 26 |
  • 27 | 34 |
  • 35 | ))} 36 |
    37 | ); 38 | } 39 | 40 | const listStyle = css({ 41 | listStyle: 'none', 42 | display: 'flex', 43 | flexDirection: 'column', 44 | gap: '1rem', 45 | placeItems: 'center', 46 | }); 47 | 48 | const buttonStyle = css({ 49 | width: '3rem', 50 | height: '3rem', 51 | }); 52 | -------------------------------------------------------------------------------- /fe/src/router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from 'react-router-dom'; 2 | 3 | import CompetitionPage from '@/pages/CompetitionPage'; 4 | import CreateCompetitionPage from '@/pages/CreateCompetitionPage'; 5 | import LoginPage from '@/pages/LoginPage'; 6 | import MainPage from '@/pages/MainPage'; 7 | import ProblemPage from '@/pages/ProblemPage'; 8 | 9 | import App from './App'; 10 | import CompetitionDetailPage from './pages/CompetitionDetailPage'; 11 | import DashboardPage from './pages/DashboardPage'; 12 | 13 | const router = createBrowserRouter([ 14 | { 15 | path: '/', 16 | element: , 17 | children: [ 18 | { 19 | index: true, 20 | element: , 21 | }, 22 | { 23 | path: '/competition/:id', 24 | element: , 25 | }, 26 | { 27 | path: '/problem/:id', 28 | element: , 29 | }, 30 | { 31 | path: '/competition/create', 32 | element: , 33 | }, 34 | { path: '/login', element: }, 35 | { 36 | path: '/competition/detail/:id', 37 | element: , 38 | }, 39 | { 40 | path: '/competition/dashboard/:id', 41 | element: , 42 | }, 43 | ], 44 | }, 45 | ]); 46 | 47 | export default router; 48 | -------------------------------------------------------------------------------- /fe/src/modules/evaluator/index.ts: -------------------------------------------------------------------------------- 1 | import { range } from '@/utils/array'; 2 | import { createObserver, type Listener } from '@/utils/observer'; 3 | 4 | import createEvalMessage from './createEvalMessage'; 5 | import createEvaluator from './createEvaluator'; 6 | import EvalTaskManager from './EvalTaskManager'; 7 | import type { EvalMessage, TaskEndMessage } from './types'; 8 | 9 | const TOTAL_WORKERS = 3; 10 | 11 | const taskEndNotifier = createObserver(); 12 | const evalWorkers = range(0, TOTAL_WORKERS).map(createEvaluator); 13 | const evalManager = new EvalTaskManager(taskEndNotifier, evalWorkers); 14 | 15 | function evaluate(tasks: EvalMessage[]) { 16 | if (evalManager.isWorking()) { 17 | return false; 18 | } 19 | 20 | evalManager.queueTasks(tasks); 21 | evalManager.deployTask(); 22 | 23 | return true; 24 | } 25 | 26 | function cancelEvaluation() { 27 | evalWorkers.forEach(({ worker }) => worker.terminate()); 28 | evalManager.cancelTasks(); 29 | 30 | const newEvalWorkers = range(0, TOTAL_WORKERS).map(createEvaluator); 31 | evalManager.setNewWorkers(newEvalWorkers); 32 | } 33 | 34 | function subscribe(listener: Listener) { 35 | return taskEndNotifier.subscribe(listener); 36 | } 37 | 38 | export default { 39 | evaluate, 40 | cancelEvaluation, 41 | subscribe, 42 | createEvalMessage, 43 | }; 44 | 45 | export * from './types'; 46 | -------------------------------------------------------------------------------- /fe/src/components/Common/BreadCrumb.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes } from 'react'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import { Icon, Text, VStack } from '.'; 7 | 8 | interface Props extends HTMLAttributes { 9 | crumbs: string[]; 10 | } 11 | 12 | export function BreadCrumb({ crumbs, className, ...props }: Props) { 13 | const lastIndex = crumbs.length - 1; 14 | 15 | return ( 16 | 37 | ); 38 | } 39 | 40 | const style = css({ 41 | gap: '0.25rem', 42 | }); 43 | 44 | const crumbStyle = css({ 45 | display: 'flex', 46 | placeItems: 'center', 47 | marginY: 'auto', 48 | marginX: '0', 49 | }); 50 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/submission.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { Competition } from './competition.entity'; 11 | import { RESULT } from './competition.enums'; 12 | import { Problem } from './problem.entity'; 13 | import { User } from './user.entity'; 14 | 15 | @Entity() 16 | export class Submission { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column('text') 21 | code: string; 22 | 23 | @Column({ 24 | type: 'enum', 25 | enum: RESULT, 26 | default: RESULT.PROGRESS, 27 | }) 28 | result: string; 29 | 30 | @Column('json', { nullable: true, default: [] }) 31 | detail: object[]; 32 | 33 | @Column() 34 | problemId: number; 35 | 36 | @ManyToOne(() => Problem, (problem) => problem.submissions, { nullable: false }) 37 | problem: Problem; 38 | 39 | @Column() 40 | competitionId: number; 41 | 42 | @ManyToOne(() => Competition, (competition) => competition.submissions, { nullable: false }) 43 | competition: Competition; 44 | 45 | @Column() 46 | userId: number; 47 | 48 | @ManyToOne(() => User, (user) => user.submissions, { nullable: false }) 49 | user: User; 50 | 51 | @CreateDateColumn() 52 | createdAt: Date; 53 | 54 | @UpdateDateColumn() 55 | updatedAt: Date; 56 | } 57 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/entities/submission.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { Competition } from './competition.entity'; 11 | import { Problem } from './problem.entity'; 12 | import { RESULT } from '../competition.enums'; 13 | 14 | import { User } from '@src/user/entities/user.entity'; 15 | 16 | @Entity() 17 | export class Submission { 18 | @PrimaryGeneratedColumn() 19 | id: number; 20 | 21 | @Column('text') 22 | code: string; 23 | 24 | @Column({ 25 | type: 'enum', 26 | enum: RESULT, 27 | default: RESULT.PROGRESS, 28 | }) 29 | result: string; 30 | 31 | @Column('jsonb', { nullable: true, default: [] }) 32 | detail: object[]; 33 | 34 | @Column() 35 | problemId: number; 36 | 37 | @ManyToOne(() => Problem, (problem) => problem.submissions, { nullable: false }) 38 | problem: Problem; 39 | 40 | @Column() 41 | competitionId: number; 42 | 43 | @ManyToOne(() => Competition, (competition) => competition.submissions, { nullable: false }) 44 | competition: Competition; 45 | 46 | @Column() 47 | userId: number; 48 | 49 | @ManyToOne(() => User, (user) => user.submissions, { nullable: false }) 50 | user: User; 51 | 52 | @CreateDateColumn() 53 | createdAt: Date; 54 | 55 | @UpdateDateColumn() 56 | updatedAt: Date; 57 | } 58 | -------------------------------------------------------------------------------- /fe/src/components/Dashboard/DashboardLoading.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { HStack, Loading, Text } from '@/components/Common'; 4 | import { useRemainingTimeCounter } from '@/hooks/dashboard'; 5 | 6 | import Header from '../Header'; 7 | import { PageLayout } from '../Layout/PageLayout'; 8 | 9 | interface Props { 10 | bufferTimeAfterCompetitionEnd: Date; 11 | } 12 | 13 | export default function DashboardLoading({ bufferTimeAfterCompetitionEnd }: Props) { 14 | const remainingTime = useRemainingTimeCounter(new Date(bufferTimeAfterCompetitionEnd)); 15 | 16 | return ( 17 | <> 18 |
    19 | 20 | 21 | 22 | 대회 종료 후 5분 뒤에 집계가 완료됩니다 23 | 24 | 25 | 남은 시간: {remainingTime} 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | const pageStyle = css({ 35 | minHeight: '100vh', 36 | display: 'flex', 37 | flexDirection: 'column', 38 | alignItems: 'center', 39 | justifyContent: 'center', 40 | }); 41 | 42 | const textContainerStyle = css({ 43 | alignItems: 'center', 44 | }); 45 | 46 | const textStyle = css({ 47 | marginBottom: '20px', 48 | }); 49 | -------------------------------------------------------------------------------- /fe/src/hooks/competition/useCompetition.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import type { CompetitionInfo } from '@/apis/competitions'; 4 | import { fetchCompetition } from '@/apis/competitions'; 5 | import type { ProblemId } from '@/apis/problems'; 6 | import { isNil } from '@/utils/type'; 7 | 8 | export type SubmissionForm = { 9 | competitionId: number; 10 | problemId: ProblemId; 11 | code: string; 12 | }; 13 | 14 | const notFoundCompetition: CompetitionInfo = { 15 | id: 0, 16 | host: null, 17 | participants: [], 18 | name: 'Competition Not Found', 19 | detail: 'Competition Not Found', 20 | maxParticipants: 0, 21 | startsAt: 'Competition Not Found', 22 | endsAt: 'Competition Not Found', 23 | createdAt: 'Competition Not Found', 24 | updatedAt: 'Competition Not Found', 25 | }; 26 | 27 | export const useCompetition = (competitionId: number) => { 28 | const [competition, setCompetition] = useState(notFoundCompetition); 29 | 30 | async function updateCompetition(competitionId: number) { 31 | const competition = await fetchCompetition(competitionId); 32 | if (isNil(competition)) { 33 | alert('대회 정보 패치에 실패했습니다.'); 34 | return; 35 | } 36 | 37 | setCompetition(competition); 38 | } 39 | 40 | useEffect(() => { 41 | updateCompetition(competitionId); 42 | }, [competitionId]); 43 | 44 | return { 45 | competition, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /fe/styled-system/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | import type * as Panda from '@pandacss/dev' 4 | import type { RecipeVariantRecord, RecipeConfig, SlotRecipeVariantRecord, SlotRecipeConfig } from './recipe'; 5 | import type { Parts } from './parts'; 6 | import type { PatternConfig, PatternProperties } from './pattern'; 7 | import type { GlobalStyleObject, SystemStyleObject } from './system-types'; 8 | import type { CompositionStyles } from './composition'; 9 | 10 | declare module '@pandacss/dev' { 11 | export function defineRecipe(config: RecipeConfig): Panda.RecipeConfig 12 | export function defineSlotRecipe>(config: SlotRecipeConfig): Panda.SlotRecipeConfig 13 | export function defineStyles(definition: SystemStyleObject): SystemStyleObject 14 | export function defineGlobalStyles(definition: GlobalStyleObject): Panda.GlobalStyleObject 15 | export function defineTextStyles(definition: CompositionStyles['textStyles']): Panda.TextStyles 16 | export function defineLayerStyles(definition: CompositionStyles['layerStyles']): Panda.LayerStyles 17 | export function definePattern(config: PatternConfig): Panda.PatternConfig 18 | export function defineParts(parts: T): (config: Partial>) => Partial> 19 | } -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/problem.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | import { Problem } from '../entities/problem.entity'; 4 | 5 | export class ProblemResponseDto { 6 | constructor( 7 | id: number, 8 | title: string, 9 | timeLimit: number, 10 | memoryLimit: number, 11 | content: string, 12 | testcaseNum: number, 13 | createdAt: Date, 14 | ) { 15 | this.id = id; 16 | this.title = title; 17 | this.timeLimit = timeLimit; 18 | this.memoryLimit = memoryLimit; 19 | this.content = content; 20 | this.testcaseNum = testcaseNum; 21 | this.createdAt = createdAt; 22 | } 23 | 24 | @ApiProperty() 25 | id: number; 26 | 27 | @ApiProperty() 28 | title: string; 29 | 30 | @ApiProperty({ description: '시간제한(ms)' }) 31 | timeLimit: number; 32 | 33 | @ApiProperty({ description: '메모리제한(KB)' }) 34 | memoryLimit: number; 35 | 36 | @ApiProperty({ description: '문제 내용' }) 37 | content: string; 38 | 39 | @ApiProperty({ description: '테스트케이스 개수' }) 40 | testcaseNum: number; 41 | 42 | @ApiProperty() 43 | createdAt: Date; 44 | 45 | static from(problem: Problem, content: string) { 46 | return new ProblemResponseDto( 47 | problem.id, 48 | problem.title, 49 | problem.timeLimit, 50 | problem.memoryLimit, 51 | content, 52 | problem.testcaseNum, 53 | problem.createdAt, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /fe/src/hooks/submission/useSubmitSolution.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { SUBMIT_STATE, type SubmitResult } from '@/components/Submission/types'; 4 | import { range } from '@/utils/array'; 5 | import type { Socket } from '@/utils/socket'; 6 | import { isNil } from '@/utils/type'; 7 | 8 | import type { SubmissionForm } from '../competition'; 9 | 10 | export function useSubmitSolution(socket: Socket | null) { 11 | const [scoreResults, setScoreResults] = useState([]); 12 | 13 | function submit(form: SubmissionForm) { 14 | if (isNil(socket)) return; 15 | 16 | socket.emit('submission', form); 17 | } 18 | 19 | function changeDoneScoreResult(doneResult: SubmitResult) { 20 | setScoreResults((results) => { 21 | return results.map((result) => { 22 | if (result.testcaseId === doneResult.testcaseId) { 23 | return doneResult; 24 | } 25 | return result; 26 | }); 27 | }); 28 | } 29 | 30 | function toEvaluatingState(testcaseNum: number) { 31 | setScoreResults( 32 | range(0, testcaseNum).map((_, index) => ({ 33 | testcaseId: index + 1, 34 | submitState: SUBMIT_STATE.loading, 35 | })), 36 | ); 37 | } 38 | 39 | function emptyResults() { 40 | setScoreResults([]); 41 | } 42 | 43 | return { 44 | submit, 45 | changeDoneScoreResult, 46 | toEvaluatingState, 47 | emptyResults, 48 | scoreResults, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /fe/src/pages/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import { Button, HStack, Logo } from '@/components/Common'; 4 | import { PageLayout } from '@/components/Layout'; 5 | 6 | const GITHUB_AUTH_URL = import.meta.env.VITE_GITHUB_AUTH_URL; 7 | 8 | export default function LoginPage() { 9 | const handleLogin = () => { 10 | try { 11 | window.location.href = GITHUB_AUTH_URL; 12 | } catch (e) { 13 | const error = e as Error; 14 | console.error(error.message); 15 | } 16 | }; 17 | 18 | return ( 19 | 20 | 21 | 22 |
    Algo With Me
    23 | 26 |
    27 |
    28 | ); 29 | } 30 | 31 | const style = css({ position: 'relative' }); 32 | 33 | const loginWrapperStyle = css({ 34 | position: 'absolute', 35 | left: '50%', 36 | transform: 'translateX(-50%)', 37 | top: '180px', 38 | width: '900px', 39 | margin: '0 auto', 40 | height: '100%', 41 | alignItems: 'center', 42 | }); 43 | 44 | const loginHeaderStyle = css({ 45 | fontSize: '3rem', 46 | fontWeight: 'bold', 47 | textAlign: 'center', 48 | padding: '1rem', 49 | }); 50 | 51 | const loginButtonStyle = css({ 52 | width: '300px', 53 | }); 54 | -------------------------------------------------------------------------------- /fe/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/node-sh/run.sh: -------------------------------------------------------------------------------- 1 | # PARAM 2 | # $1 COMPETITION_ID 3 | # $2 USER_ID 4 | # $3 PROBLEM_ID 5 | # $4 TESTCASE_ID 6 | 7 | # DESCRIPTION 8 | # SUBMISSION_JS_FILE에서 파일을 읽어, node로 실행한다. 9 | # DETAIL_FILE에는 사용한 시간(sec)과 최대 memory 메모리 사용량(KB)이 공백(' ')으로 분리되어 기록된다. 10 | 11 | mkdir -p "/algo-with-me/submissions/$1/$2/" || exit 1 12 | 13 | SUBMISSION_JS_FILE="/algo-with-me/submissions/$1/$2/$3.js" 14 | MEMORY_FILE="/algo-with-me/submissions/$1/$2/$3.$4.memory" 15 | 16 | # 제출된 js 파일이 있으면 node로 js 파일 실행 17 | # 주의: judge.sh와 run.sh는 execute 권한이 부여되어야 함 18 | if [ -f "$SUBMISSION_JS_FILE" ]; then 19 | echo "[algo-with-me-docker] run.sh: started running $SUBMISSION_JS_FILE. COMPETITION_ID=$1, USER_ID=$2, PROBLEM_ID=$3, TESTCASE_ID=$4" 20 | # -o FILE Write result to FILE 21 | # -f FMT Custom format 22 | # U Total number of CPU-seconds that the process used directly (in user mode), in seconds. 23 | # e Elapsed real (wall clock) time used by the process, in seconds. 24 | # M Maximum resident set size of the process during its lifetime, in Kilobytes. 25 | /algo-with-me/node-sh/time -o "$MEMORY_FILE" -f "%M" /algo-with-me/node-sh/runJs.sh "$1" "$2" "$3" "$4" || exit 2 26 | echo "[algo-with-me-docker] run.sh: successfully ran $SUBMISSION_JS_FILE. COMPETITION_ID=$1, USER_ID=$2, PROBLEM_ID=$3, TESTCASE_ID=$4" 27 | else 28 | echo "[algo-with-me-docker] run.sh: cannot find submitted js file $SUBMISSION_JS_FILE" 29 | exit 3 30 | fi 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/dto/competition.problem.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export type TestcaseParameterMetadata = { name: string; type: string }; 4 | export type TestcaseData = { input: any[]; output: any }; 5 | export interface ITestcases { 6 | input: TestcaseParameterMetadata[]; 7 | output: TestcaseParameterMetadata; 8 | data: TestcaseData[]; 9 | } 10 | 11 | export class CompetitionProblemResponseDto { 12 | constructor( 13 | id: number, 14 | title: string, 15 | timeLimit: number, 16 | memoryLimit: number, 17 | content: string, 18 | solutionCode: string, 19 | testcases: ITestcases, 20 | createdAt: Date, 21 | ) { 22 | this.id = id; 23 | this.title = title; 24 | this.timeLimit = timeLimit; 25 | this.memoryLimit = memoryLimit; 26 | this.content = content; 27 | this.solutionCode = solutionCode; 28 | this.testcases = testcases; 29 | this.createdAt = createdAt; 30 | } 31 | 32 | @ApiProperty() 33 | id: number; 34 | 35 | @ApiProperty() 36 | title: string; 37 | 38 | @ApiProperty({ description: '시간제한(ms)' }) 39 | timeLimit: number; 40 | 41 | @ApiProperty({ description: '메모리제한(KB)' }) 42 | memoryLimit: number; 43 | 44 | @ApiProperty({ description: '문제 내용' }) 45 | content: string; 46 | 47 | @ApiProperty({ description: '초기 코드' }) 48 | solutionCode: string; 49 | 50 | @ApiProperty({ description: '공개 테스트 케이스' }) 51 | testcases: ITestcases; 52 | 53 | @ApiProperty() 54 | createdAt: Date; 55 | } 56 | -------------------------------------------------------------------------------- /be/algo-with-me-score/src/score/entities/competition.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { CompetitionParticipant } from './competition.participant.entity'; 12 | import { CompetitionProblem } from './competition.problem.entity'; 13 | import { Submission } from './submission.entity'; 14 | import { User } from './user.entity'; 15 | 16 | @Entity() 17 | export class Competition { 18 | @PrimaryGeneratedColumn() 19 | id: number; 20 | 21 | @Column() 22 | name: string; 23 | 24 | @Column('text') 25 | detail: string; 26 | 27 | @Column() 28 | maxParticipants: number; 29 | 30 | @Column() 31 | startsAt: Date; 32 | 33 | @Column() 34 | endsAt: Date; 35 | 36 | @OneToMany(() => Submission, (submission) => submission.competition) 37 | submissions: Submission[]; 38 | 39 | @OneToMany(() => CompetitionProblem, (competitionProblem) => competitionProblem.competition) 40 | competitionProblems: CompetitionProblem[]; 41 | 42 | @OneToMany( 43 | () => CompetitionParticipant, 44 | (competitionParticipant) => competitionParticipant.competition, 45 | ) 46 | competitionParticipants: CompetitionParticipant[]; 47 | 48 | @Column() 49 | userId: number; 50 | 51 | @ManyToOne(() => User, (user) => user.competitions, { nullable: false }) 52 | user: User; 53 | 54 | @CreateDateColumn() 55 | createdAt: Date; 56 | 57 | @UpdateDateColumn() 58 | updatedAt: Date; 59 | } 60 | -------------------------------------------------------------------------------- /fe/src/components/Submission/SubmissionButton.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@style/css'; 2 | 3 | import { HTMLAttributes, useContext } from 'react'; 4 | 5 | import { CompetitionId } from '@/apis/competitions'; 6 | import type { ProblemId } from '@/apis/problems'; 7 | import type { SubmissionForm } from '@/hooks/competition'; 8 | import { isNil } from '@/utils/type'; 9 | 10 | import { Button } from '../Common'; 11 | import { SocketContext } from '../Common/Socket/SocketContext'; 12 | 13 | interface Props extends HTMLAttributes { 14 | code: string; 15 | problemId?: ProblemId; 16 | competitionId: CompetitionId; 17 | } 18 | 19 | export function SubmissionButton({ code, problemId, competitionId, className, ...props }: Props) { 20 | const { socket, isConnected } = useContext(SocketContext); 21 | 22 | function handleSubmitSolution() { 23 | if (isNil(problemId)) { 24 | console.error('존재하지 않는 문제입니다.'); 25 | return; 26 | } 27 | 28 | const form = { 29 | problemId, 30 | code, 31 | competitionId, 32 | } satisfies SubmissionForm; 33 | 34 | if (isNil(socket) || !isConnected) { 35 | alert('연결에 실패했습니다.'); 36 | return; 37 | } 38 | 39 | socket.emit('submission', form); 40 | } 41 | 42 | return ( 43 | 51 | ); 52 | } 53 | 54 | const style = css({ 55 | paddingX: '2rem', 56 | }); 57 | -------------------------------------------------------------------------------- /fe/src/hooks/problem/useCompetitionProblem.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import type { CompetitionProblem, ProblemId, Testcase } from '@/apis/problems'; 4 | import { fetchCompetitionProblem } from '@/apis/problems'; 5 | import { isNil } from '@/utils/type'; 6 | 7 | const notFoundProblem: CompetitionProblem = { 8 | id: 0, 9 | title: 'Problem Not Found', 10 | timeLimit: 0, 11 | memoryLimit: 0, 12 | content: 'The requested problem could not be found.', 13 | solutionCode: '', 14 | testcases: [], 15 | createdAt: new Date().toISOString(), 16 | }; 17 | 18 | export const useCompetitionProblem = (problemId: ProblemId) => { 19 | const [problem, setProblem] = useState(notFoundProblem); 20 | 21 | async function updateProblem(problemId: ProblemId) { 22 | if (problemId < 0) return; 23 | 24 | const problem = await fetchCompetitionProblem(problemId); 25 | 26 | if (isNil(problem)) { 27 | alert('문제를 가져오는데 실패했습니다.'); 28 | return; 29 | } 30 | 31 | const { data } = problem.testcases as unknown as Testcase; 32 | 33 | setProblem({ 34 | ...problem, 35 | testcases: data.map(({ input, output }, index) => ({ 36 | id: index + 1, 37 | input: input.map((el) => JSON.stringify(el)).join(','), 38 | expected: JSON.stringify(output), 39 | changable: false, 40 | })), 41 | }); 42 | } 43 | 44 | useEffect(() => { 45 | updateProblem(problemId); 46 | }, [problemId]); 47 | 48 | return { 49 | problem, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /fe/src/components/CompetitionDetail/Buttons/EnterCompetitionButton.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import { fetchIsJoinableCompetition } from '@/apis/joinCompetition'; 4 | import { Button } from '@/components/Common'; 5 | import useAuth from '@/hooks/login/useAuth'; 6 | 7 | interface Props { 8 | id: number; 9 | startsAt: Date; 10 | endsAt: Date; 11 | } 12 | 13 | export default function EnterCompetitionButton({ id, startsAt, endsAt }: Props) { 14 | const competitionLink = `/competition/${id}`; 15 | const { isLoggedin } = useAuth(); 16 | const navigate = useNavigate(); 17 | 18 | const handleNavigate = async () => { 19 | const currentTime = new Date(); 20 | 21 | if (!isLoggedin) { 22 | alert('로그인이 필요합니다.'); 23 | navigate('/login'); 24 | 25 | return; 26 | } 27 | 28 | if (currentTime < startsAt) { 29 | alert('아직 대회가 시작되지 않았습니다. 다시 시도해주세요'); 30 | navigate(0); 31 | 32 | return; 33 | } 34 | 35 | if (currentTime >= endsAt) { 36 | alert('해당 대회는 종료되었습니다.'); 37 | navigate(0); 38 | 39 | return; 40 | } 41 | 42 | const accessToken = localStorage.getItem('accessToken') ?? ''; 43 | 44 | const isJoinable = await fetchIsJoinableCompetition(id, accessToken); 45 | if (!isJoinable) { 46 | alert('대회에 참여할 수 없습니다.\n다음부터는 늦지 않게 대회 신청을 해주세요 :)'); 47 | return; 48 | } 49 | 50 | navigate(competitionLink); 51 | }; 52 | 53 | return ( 54 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /fe/src/components/CompetitionDetail/CompetitionDetailContent.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | 3 | import { CompetitionInfo } from '@/apis/competitions'; 4 | import AfterCompetition from '@/components/CompetitionDetail/AfterCompetition'; 5 | import BeforeCompetition from '@/components/CompetitionDetail/BeforeCompetition'; 6 | import DuringCompetition from '@/components/CompetitionDetail/DuringCompetition'; 7 | import { useCompetitionRerender } from '@/hooks/competitionDetail'; 8 | 9 | interface Props extends HTMLAttributes { 10 | competitionId: number; 11 | competition: CompetitionInfo; 12 | } 13 | 14 | export function CompetitionDetailContent({ 15 | competitionId, 16 | competition, 17 | className, 18 | ...props 19 | }: Props) { 20 | const currentDate = new Date(); 21 | const startsAt = new Date(competition.startsAt || ''); 22 | const endsAt = new Date(competition.endsAt || ''); 23 | 24 | const { shouldRerenderDuring, shouldRerenderAfter } = useCompetitionRerender(startsAt, endsAt); 25 | 26 | if ((shouldRerenderAfter && shouldRerenderDuring) || currentDate >= endsAt) { 27 | return ( 28 | 29 | ); 30 | } 31 | 32 | if (shouldRerenderDuring || currentDate >= startsAt) { 33 | return ( 34 | 35 | ); 36 | } 37 | 38 | return ; 39 | } 40 | -------------------------------------------------------------------------------- /fe/styled-system/tokens/index.css: -------------------------------------------------------------------------------- 1 | @layer tokens { 2 | :where(:root, :host) { 3 | --animations-spin: spin 1s linear infinite; 4 | --breakpoints-sm: 640px; 5 | --breakpoints-md: 768px; 6 | --breakpoints-lg: 1024px; 7 | --breakpoints-xl: 1280px; 8 | --breakpoints-2xl: 1536px; 9 | --sizes-breakpoint-sm: 640px; 10 | --sizes-breakpoint-md: 768px; 11 | --sizes-breakpoint-lg: 1024px; 12 | --sizes-breakpoint-xl: 1280px; 13 | --sizes-breakpoint-2xl: 1536px; 14 | --colors-background: #263238; 15 | --colors-alert-success: #82dd55; 16 | --colors-alert-success-dark: #355a23; 17 | --colors-alert-warning: #edb95e; 18 | --colors-alert-warning-dark: #8e6f3a; 19 | --colors-alert-danger: #e23636; 20 | --colors-alert-danger-dark: #751919; 21 | --colors-alert-info: #c8cdd0; 22 | --colors-alert-info-dark: #444749; 23 | --colors-brand: #ffa800; 24 | --colors-brand-alt: #ffbb36; 25 | --colors-surface: #37474f; 26 | --colors-surface-alt: #455a64; 27 | --colors-surface-light: #d9d9d9; 28 | --colors-text: #f5f5f5; 29 | --colors-text-light: #fafafa99; 30 | --colors-border: #455a64; 31 | --font-sizes-display-lg: 57px; 32 | --font-sizes-display-md: 45px; 33 | --font-sizes-display-sm: 36px; 34 | --font-sizes-title-lg: 22px; 35 | --font-sizes-title-md: 16px; 36 | --font-sizes-title-sm: 14px; 37 | --font-sizes-body-lg: 16px; 38 | --font-sizes-body-md: 14px; 39 | --font-sizes-body-sm: 12px; 40 | --font-sizes-label-lg: 14px; 41 | --font-sizes-label-md: 12px; 42 | --font-sizes-label-sm: 11px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fe/src/hooks/login/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | 4 | import { fetchTokenValid, type TokenValidResponse } from '@/apis/auth'; 5 | import AuthContext from '@/components/Auth/AuthContext'; 6 | 7 | const TOKEN_KEY = 'accessToken'; 8 | 9 | export default function useAuth() { 10 | const { isLoggedin, login, logout, email } = useContext(AuthContext); 11 | 12 | const location = useLocation(); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | if (isLoggedin) return; 17 | const queryParams = new URLSearchParams(location.search); 18 | const token = queryParams.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY); 19 | 20 | if (!token) return; 21 | evaluateToken(token); 22 | }, []); 23 | 24 | const evaluateToken = async (token: string) => { 25 | try { 26 | const info = await fetchTokenValid(token); 27 | saveAuthInfo(info, token); 28 | } catch (e) { 29 | removeAuthInfo(); 30 | } 31 | }; 32 | 33 | const saveAuthInfo = (info: TokenValidResponse, token: string) => { 34 | const { email } = info; 35 | 36 | localStorage.setItem(TOKEN_KEY, token); 37 | login(email); 38 | }; 39 | 40 | const removeAuthInfo = () => { 41 | localStorage.removeItem(TOKEN_KEY); 42 | logout(); 43 | }; 44 | 45 | const changeLoginInfo = () => { 46 | removeAuthInfo(); 47 | navigate('/login'); 48 | }; 49 | 50 | const changeLogoutInfo = () => { 51 | removeAuthInfo(); 52 | }; 53 | return { changeLoginInfo, changeLogoutInfo, isLoggedin, email }; 54 | } 55 | -------------------------------------------------------------------------------- /fe/src/apis/joinCompetition/index.ts: -------------------------------------------------------------------------------- 1 | import { CompetitionId } from '@/apis/competitions'; 2 | import api from '@/utils/api'; 3 | 4 | import type { CompetitionApiData } from './types'; 5 | import axios from 'axios'; 6 | 7 | const STATUS = { 8 | Forbidden: 403, 9 | BadRequest: 400, 10 | } as const; 11 | 12 | export async function joinCompetition(data: CompetitionApiData) { 13 | const { id, token } = data; 14 | 15 | try { 16 | await api.post( 17 | `/competitions/${id}/participations`, 18 | {}, 19 | { 20 | headers: { 21 | Authorization: `Bearer ${token}`, 22 | }, 23 | }, 24 | ); 25 | 26 | return '대회에 성공적으로 참여했습니다.'; 27 | } catch (error: unknown) { 28 | if (!axios.isAxiosError(error)) { 29 | return 'Unexpected error occurred'; 30 | } 31 | 32 | if (!error.response) { 33 | return 'Network error occurred'; 34 | } 35 | 36 | switch (error.response.status) { 37 | case STATUS.Forbidden: 38 | return '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; 39 | case STATUS.BadRequest: 40 | return '이미 참여한 대회입니다.'; 41 | default: 42 | return `HTTP Error ${error.response.status}`; 43 | } 44 | } 45 | } 46 | 47 | export async function fetchIsJoinableCompetition( 48 | competitionId: CompetitionId, 49 | token: string, 50 | ): Promise { 51 | try { 52 | const { data } = await api.get(`/competitions/validation/${competitionId}`, { 53 | headers: { 54 | Authorization: `Bearer ${token}`, 55 | }, 56 | }); 57 | 58 | return data.isJoinable; 59 | } catch (err) { 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /fe/src/components/Problem/SelectableProblemList.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import type { ProblemId, ProblemInfo } from '@/apis/problems'; 4 | 5 | import { Icon } from '../Common'; 6 | 7 | interface ProblemListProps { 8 | problemList: ProblemInfo[]; 9 | onSelectProblem: (problemId: ProblemId) => void; 10 | } 11 | 12 | export function SelectableProblemList({ problemList, onSelectProblem }: ProblemListProps) { 13 | function handleSelectProblem(id: ProblemId) { 14 | onSelectProblem(id); 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {problemList.map(({ id, title }) => ( 27 | 28 | 29 | 38 | 39 | ))} 40 | 41 |
    문제 이름문제 추가
    {title} 36 | handleSelectProblem(id)} /> 37 |
    42 | ); 43 | } 44 | 45 | const tableStyle = css({ 46 | width: '320px', 47 | padding: '24px 16px', 48 | tableLayout: 'fixed', 49 | }); 50 | 51 | const dividingStyle = css({ 52 | borderBottom: '1px solid', 53 | borderColor: 'border', 54 | }); 55 | -------------------------------------------------------------------------------- /fe/src/components/Problem/SelectedProblemList.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@style/css'; 2 | 3 | import type { ProblemId, ProblemInfo } from '@/apis/problems'; 4 | 5 | import { Icon } from '../Common'; 6 | 7 | interface SelectedProblemListProps { 8 | problemList: ProblemInfo[]; 9 | onCancelProblem: (problemId: ProblemId) => void; 10 | } 11 | 12 | export function SelectedProblemList({ problemList, onCancelProblem }: SelectedProblemListProps) { 13 | function handleCancelProblem(id: ProblemId) { 14 | onCancelProblem(id); 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {problemList.map(({ id, title }) => ( 27 | 28 | 29 | 38 | 39 | ))} 40 | 41 |
    문제 이름문제 삭제
    {title} 36 | handleCancelProblem(id)} /> 37 |
    42 | ); 43 | } 44 | 45 | const tableStyle = css({ 46 | width: '320px', 47 | padding: '1.5rem 1rem', 48 | tableLayout: 'fixed', 49 | }); 50 | 51 | const dividingStyle = css({ 52 | borderBottom: '1px solid', 53 | borderColor: 'border', 54 | }); 55 | -------------------------------------------------------------------------------- /fe/src/utils/date/index.ts: -------------------------------------------------------------------------------- 1 | const ONE_SEC_BY_MS = 1_000; 2 | const ONE_MIN_BY_MS = 60 * ONE_SEC_BY_MS; 3 | const ONE_MIN_BY_SEC = 60; 4 | const ONE_HOUR_BY_MIN = 60; 5 | const ONE_HOUR_BY_SEC = ONE_HOUR_BY_MIN * ONE_MIN_BY_SEC; 6 | 7 | export function toLocalDate(date: Date) { 8 | const localTimeOffset = date.getTimezoneOffset() * ONE_MIN_BY_MS; 9 | const localDate = new Date(date.getTime() - localTimeOffset); 10 | 11 | return localDate; 12 | } 13 | 14 | export const formatDate = (date: Date, form: string) => { 15 | if (form === 'YYYY-MM-DDThh:mm') { 16 | return date.toISOString().slice(0, 'YYYY-MM-DDThh:mm'.length); 17 | } 18 | 19 | if (form === 'YYYY. MM. DD. hh:mm') { 20 | return date.toLocaleString('ko-KR', { 21 | year: 'numeric', 22 | month: '2-digit', 23 | day: '2-digit', 24 | hour: '2-digit', 25 | minute: '2-digit', 26 | hour12: true, 27 | }); 28 | } 29 | 30 | if (form === 'hh:mm') { 31 | const hours = String(date.getHours()).padStart(2, '0'); 32 | const minutes = String(date.getMinutes()).padStart(2, '0'); 33 | return `${hours}:${minutes}`; 34 | } 35 | 36 | return ''; 37 | }; 38 | 39 | export const formatMilliSecond = (ms: number, form: string) => { 40 | const sec = Math.floor(ms / ONE_SEC_BY_MS); 41 | 42 | if (form === 'hh:mm:ss') { 43 | // 시간(초)을 'hh:mm:ss' 형식으로 변환 44 | const hours = Math.floor(sec / ONE_HOUR_BY_SEC); 45 | const minutes = Math.floor((sec % ONE_HOUR_BY_SEC) / ONE_MIN_BY_SEC); 46 | const seconds = sec % ONE_MIN_BY_SEC; 47 | return [hours, minutes, seconds].map((time) => String(time).padStart(2, '0')).join(':'); 48 | } 49 | return ''; 50 | }; 51 | -------------------------------------------------------------------------------- /fe/src/apis/problems/index.ts: -------------------------------------------------------------------------------- 1 | import api from '@/utils/api'; 2 | 3 | import type { 4 | CompetitionProblem, 5 | FetchCompetitionProblemResponse, 6 | FetchProblemListResponse, 7 | Problem, 8 | ProblemId, 9 | ProblemInfo, 10 | } from './types'; 11 | 12 | export * from './types'; 13 | 14 | export async function fetchProblemList(): Promise { 15 | try { 16 | const { data } = await api.get('/problems'); 17 | 18 | return data; 19 | } catch (err) { 20 | console.error(err); 21 | 22 | return []; 23 | } 24 | } 25 | 26 | export async function fetchCompetitionProblemList(competitionId: number): Promise { 27 | try { 28 | const { data } = await api.get( 29 | `/competitions/${competitionId}/problems`, 30 | ); 31 | 32 | return data; 33 | } catch (err) { 34 | console.error(err); 35 | 36 | return []; 37 | } 38 | } 39 | 40 | export async function fetchCompetitionProblem( 41 | problemId: ProblemId, 42 | ): Promise { 43 | try { 44 | const { data } = await api.get( 45 | `/competitions/problems/${problemId}/`, 46 | ); 47 | 48 | return data; 49 | } catch (err) { 50 | console.error(err); 51 | 52 | return null; 53 | } 54 | } 55 | 56 | export async function fetchProblemDetail(problemId: number) { 57 | try { 58 | const response = await api.get(`/problems/${problemId}`); 59 | const data = response.data; 60 | 61 | return data; 62 | } catch (error) { 63 | console.error('Error fetching problem:', (error as Error).message); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /be/algo-with-me-api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@typescript-eslint/recommended', 4 | // nestjs 스타일 가이드 5 | 'plugin:nestjs/recommended', 6 | // google 스타일 가이드 7 | // 'google', 8 | // import sort 관련 설정 9 | 'plugin:import/recommended', 10 | 'plugin:import/typescript', 11 | // prettier 12 | 'prettier', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: 'tsconfig.json', 17 | tsconfigRootDir: __dirname, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['@typescript-eslint/eslint-plugin', 'nestjs'], 21 | root: true, 22 | env: { 23 | node: true, 24 | jest: true, 25 | }, 26 | ignorePatterns: ['.eslintrc.js'], 27 | rules: { 28 | '@typescript-eslint/interface-name-prefix': 'off', 29 | '@typescript-eslint/explicit-function-return-type': 'off', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | // import sort 관련 설정 33 | 'import/order': [ 34 | 'error', 35 | { 36 | groups: ['external', 'builtin', ['parent', 'sibling'], 'internal'], 37 | pathGroups: [ 38 | { 39 | pattern: 'nest', 40 | group: 'external', 41 | position: 'before', 42 | }, 43 | ], 44 | alphabetize: { 45 | order: 'asc', 46 | caseInsensitive: true, 47 | }, 48 | 'newlines-between': 'always', 49 | }, 50 | ], 51 | // 52 | }, 53 | settings: { 54 | // import sort 관련 설정 55 | 'import/resolver': { 56 | typescript: {}, 57 | node: { 58 | paths: ['src'], 59 | }, 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /be/algo-with-me-docker/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@typescript-eslint/recommended', 4 | // nestjs 스타일 가이드 5 | 'plugin:nestjs/recommended', 6 | // google 스타일 가이드 7 | // 'google', 8 | // import sort 관련 설정 9 | 'plugin:import/recommended', 10 | 'plugin:import/typescript', 11 | // prettier 12 | 'prettier', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: 'tsconfig.json', 17 | tsconfigRootDir: __dirname, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['@typescript-eslint/eslint-plugin', 'nestjs'], 21 | root: true, 22 | env: { 23 | node: true, 24 | jest: true, 25 | }, 26 | ignorePatterns: ['.eslintrc.js'], 27 | rules: { 28 | '@typescript-eslint/interface-name-prefix': 'off', 29 | '@typescript-eslint/explicit-function-return-type': 'off', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | // import sort 관련 설정 33 | 'import/order': [ 34 | 'error', 35 | { 36 | groups: ['external', 'builtin', ['parent', 'sibling'], 'internal'], 37 | pathGroups: [ 38 | { 39 | pattern: 'nest', 40 | group: 'external', 41 | position: 'before', 42 | }, 43 | ], 44 | alphabetize: { 45 | order: 'asc', 46 | caseInsensitive: true, 47 | }, 48 | 'newlines-between': 'always', 49 | }, 50 | ], 51 | // 52 | }, 53 | settings: { 54 | // import sort 관련 설정 55 | 'import/resolver': { 56 | typescript: {}, 57 | node: { 58 | paths: ['src'], 59 | }, 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /be/algo-with-me-score/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@typescript-eslint/recommended', 4 | // nestjs 스타일 가이드 5 | 'plugin:nestjs/recommended', 6 | // google 스타일 가이드 7 | // 'google', 8 | // import sort 관련 설정 9 | 'plugin:import/recommended', 10 | 'plugin:import/typescript', 11 | // prettier 12 | 'prettier', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: 'tsconfig.json', 17 | tsconfigRootDir: __dirname, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['@typescript-eslint/eslint-plugin', 'nestjs'], 21 | root: true, 22 | env: { 23 | node: true, 24 | jest: true, 25 | }, 26 | ignorePatterns: ['.eslintrc.js'], 27 | rules: { 28 | '@typescript-eslint/interface-name-prefix': 'off', 29 | '@typescript-eslint/explicit-function-return-type': 'off', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | // import sort 관련 설정 33 | 'import/order': [ 34 | 'error', 35 | { 36 | groups: ['external', 'builtin', ['parent', 'sibling'], 'internal'], 37 | pathGroups: [ 38 | { 39 | pattern: 'nest', 40 | group: 'external', 41 | position: 'before', 42 | }, 43 | ], 44 | alphabetize: { 45 | order: 'asc', 46 | caseInsensitive: true, 47 | }, 48 | 'newlines-between': 'always', 49 | }, 50 | ], 51 | // 52 | }, 53 | settings: { 54 | // import sort 관련 설정 55 | 'import/resolver': { 56 | typescript: {}, 57 | node: { 58 | path: ['src'], 59 | }, 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /be/algo-with-me-api/src/competition/entities/competition.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { CompetitionParticipant } from './competition.participant.entity'; 12 | import { CompetitionProblem } from './competition.problem.entity'; 13 | import { Submission } from './submission.entity'; 14 | 15 | import { Dashboard } from '@src/dashboard/entities/dashboard.entity'; 16 | import { User } from '@src/user/entities/user.entity'; 17 | 18 | @Entity() 19 | export class Competition { 20 | @PrimaryGeneratedColumn() 21 | id: number; 22 | 23 | @Column() 24 | name: string; 25 | 26 | @Column('text') 27 | detail: string; 28 | 29 | @Column() 30 | maxParticipants: number; 31 | 32 | @Column() 33 | startsAt: Date; 34 | 35 | @Column() 36 | endsAt: Date; 37 | 38 | @OneToMany(() => Submission, (submission) => submission.competition) 39 | submissions: Submission[]; 40 | 41 | @OneToMany(() => CompetitionProblem, (competitionProblem) => competitionProblem.competition) 42 | competitionProblems: CompetitionProblem[]; 43 | 44 | @OneToMany( 45 | () => CompetitionParticipant, 46 | (competitionParticipant) => competitionParticipant.competition, 47 | ) 48 | competitionParticipants: CompetitionParticipant[]; 49 | 50 | @Column() 51 | userId: number; 52 | 53 | @ManyToOne(() => User, (user) => user.competitions, { nullable: false }) 54 | user: User; 55 | 56 | @OneToMany(() => Dashboard, (dashboard) => dashboard.competition) 57 | dashboards: Dashboard[]; 58 | 59 | @CreateDateColumn() 60 | createdAt: Date; 61 | 62 | @UpdateDateColumn() 63 | updatedAt: Date; 64 | } 65 | -------------------------------------------------------------------------------- /fe/src/components/Common/Socket/SocketProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useRef, useState } from 'react'; 2 | 3 | import { connect, disconnect } from '@/utils/socket'; 4 | import { isNil } from '@/utils/type'; 5 | 6 | import { SocketContext } from './SocketContext'; 7 | 8 | interface Props { 9 | namespace: string; 10 | query: Record; 11 | transports: string[]; 12 | token?: string; 13 | children: ReactNode; 14 | } 15 | 16 | export function SocketProvider({ 17 | namespace = '', 18 | transports = ['websocket'], 19 | query = {}, 20 | token = '', 21 | children, 22 | }: Props) { 23 | const [isConnected, setIsConnected] = useState(false); 24 | const socket = useRef( 25 | connect(`/${namespace}`, { 26 | transports, 27 | query, 28 | auth: { 29 | token: `Bearer ${token}`, 30 | }, 31 | }), 32 | ); 33 | 34 | const handleConnect = () => { 35 | setIsConnected(true); 36 | }; 37 | 38 | const handleDisconnect = () => { 39 | setIsConnected(false); 40 | }; 41 | 42 | useEffect(() => { 43 | if (!socket.current.hasListeners('connect')) { 44 | socket.current.on('connect', handleConnect); 45 | } 46 | if (!socket.current.hasListeners('disconnect')) { 47 | socket.current.on('disconnect', handleDisconnect); 48 | } 49 | }, [socket.current]); 50 | 51 | useEffect(() => { 52 | if (isNil(socket.current)) return; 53 | return () => { 54 | disconnect(`/${namespace}`); 55 | }; 56 | }, [socket]); 57 | 58 | return ( 59 | 65 | {children} 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /fe/styled-system/patterns/float.mjs: -------------------------------------------------------------------------------- 1 | import { mapObject } from '../helpers.mjs'; 2 | import { css } from '../css/index.mjs'; 3 | 4 | const floatConfig = { 5 | transform(props, { map }) { 6 | const { offset = "0", offsetX = offset, offsetY = offset, placement = "top-end", ...rest } = props; 7 | return { 8 | display: "inline-flex", 9 | justifyContent: "center", 10 | alignItems: "center", 11 | position: "absolute", 12 | insetBlockStart: map(placement, (v) => { 13 | const [side] = v.split("-"); 14 | const map2 = { top: offsetY, middle: "50%", bottom: "auto" }; 15 | return map2[side]; 16 | }), 17 | insetBlockEnd: map(placement, (v) => { 18 | const [side] = v.split("-"); 19 | const map2 = { top: "auto", middle: "50%", bottom: offsetY }; 20 | return map2[side]; 21 | }), 22 | insetInlineStart: map(placement, (v) => { 23 | const [, align] = v.split("-"); 24 | const map2 = { start: offsetX, center: "50%", end: "auto" }; 25 | return map2[align]; 26 | }), 27 | insetInlineEnd: map(placement, (v) => { 28 | const [, align] = v.split("-"); 29 | const map2 = { start: "auto", center: "50%", end: offsetX }; 30 | return map2[align]; 31 | }), 32 | translate: map(placement, (v) => { 33 | const [side, align] = v.split("-"); 34 | const mapX = { start: "-50%", center: "-50%", end: "50%" }; 35 | const mapY = { top: "-50%", middle: "-50%", bottom: "50%" }; 36 | return `${mapX[align]} ${mapY[side]}`; 37 | }), 38 | ...rest 39 | }; 40 | }} 41 | 42 | export const getFloatStyle = (styles = {}) => floatConfig.transform(styles, { map: mapObject }) 43 | 44 | export const float = (styles) => css(getFloatStyle(styles)) 45 | float.raw = getFloatStyle --------------------------------------------------------------------------------