├── .deploy └── values.yaml ├── .env.ci.example ├── .env.example ├── .eslintrc.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── deploy-codepocket-external.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-typescript.cjs ├── releases │ └── yarn-3.2.1.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ └── api.js │ └── package.json │ ├── integrations.yml │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── CONTRIBUTING.md ├── Dockerfile.compose ├── Dockerfile.kontrol ├── LICENSE ├── README.md ├── cli ├── .npmignore ├── README.md ├── bin │ └── index.ts ├── jest.config.js ├── lib │ ├── __mocks__ │ │ ├── handlers.ts │ │ ├── mockup.ts │ │ └── server.ts │ ├── api.ts │ ├── command │ │ ├── __test__ │ │ │ ├── delete.test.ts │ │ │ ├── list.test.ts │ │ │ ├── pull.test.ts │ │ │ └── push.test.ts │ │ ├── daangn.ts │ │ ├── delete.ts │ │ ├── list.ts │ │ ├── open.ts │ │ ├── pull.ts │ │ └── push.ts │ ├── env.ts │ ├── index.ts │ ├── setupTests.ts │ └── utils.ts ├── package.json └── tsconfig.json ├── client ├── .eslintrc.json ├── .gitignore ├── .storybook │ ├── main.js │ ├── preview-head.html │ └── preview.js ├── README.md ├── index.html ├── jest.config.js ├── package.json ├── src │ ├── App.tsx │ ├── __mocks__ │ │ ├── handlers.ts │ │ └── server.ts │ ├── auth │ │ ├── Auth.test.tsx │ │ ├── Auth.tsx │ │ ├── api.ts │ │ ├── components │ │ │ ├── SubTitle │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── useCreateUser.ts │ │ │ ├── useCustomAuth0.ts │ │ │ ├── useTyping.ts │ │ │ └── useVerifyUser.ts │ │ └── style.css.ts │ ├── detail │ │ ├── Detail.tsx │ │ ├── api.ts │ │ ├── components │ │ │ ├── ErrorModal │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── Sandpack │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── SandpackLoader │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── StoryConfirmModal │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ └── StoryNameList │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ ├── hooks │ │ │ ├── useCreateStory.ts │ │ │ ├── useDeleteStory.ts │ │ │ ├── useStory.ts │ │ │ ├── useStoryNames.ts │ │ │ └── useUpdateStory.ts │ │ ├── style.css.ts │ │ └── utils │ │ │ ├── filterObj.ts │ │ │ ├── getAllCodesFromSandpack.ts │ │ │ ├── parse.ts │ │ │ └── textGenerator.ts │ ├── env.d.ts │ ├── main.tsx │ ├── pocket │ │ ├── Pocket.tsx │ │ ├── api.ts │ │ ├── components │ │ │ ├── CodeBlockSkeleton │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── CodeList │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── Codeblock │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── DeleteModal │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── ErrorMessage │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── FloatingActionButton │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── Modal │ │ │ │ ├── CreateModal.tsx │ │ │ │ ├── EditModal.tsx │ │ │ │ ├── Template.tsx │ │ │ │ └── style.css.ts │ │ │ ├── MoreButton │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── PendingFallback │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── SearchHelpText │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── Searchbar │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── useCodes.ts │ │ │ ├── useCreateCode.ts │ │ │ ├── useDeleteCode.ts │ │ │ ├── useScrollDirection.ts │ │ │ ├── useScrollPosition.ts │ │ │ └── useUpdateCode.ts │ │ └── style.css.ts │ ├── routes.ts │ ├── setupTests.ts │ ├── shared │ │ ├── api.ts │ │ ├── components │ │ │ ├── Alert │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── AsyncBoundary.tsx │ │ │ ├── Icon │ │ │ │ ├── @types │ │ │ │ │ └── index.ts │ │ │ │ ├── Check.tsx │ │ │ │ ├── Clip.tsx │ │ │ │ ├── Close.tsx │ │ │ │ ├── Code.tsx │ │ │ │ ├── Delete.tsx │ │ │ │ ├── Edit.tsx │ │ │ │ ├── Information.tsx │ │ │ │ ├── LeftChevron.tsx │ │ │ │ ├── Profile.tsx │ │ │ │ ├── RightChevron.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── WarningFill.tsx │ │ │ │ └── index.tsx │ │ │ ├── IconButton │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── Modal │ │ │ │ ├── index.tsx │ │ │ │ └── style.css.ts │ │ │ ├── PrivateRoute.tsx │ │ │ └── index.tsx │ │ ├── constant.ts │ │ ├── contexts │ │ │ ├── GlobalModal.tsx │ │ │ └── ModalContext.tsx │ │ ├── hooks │ │ │ ├── useClipboard.ts │ │ │ ├── useCode.ts │ │ │ ├── useCustomInfiniteQuery.ts │ │ │ ├── useCustomMutation.ts │ │ │ ├── useCustomQuery.ts │ │ │ ├── useKeyboard.ts │ │ │ └── useModal.ts │ │ ├── lib │ │ │ └── axios.ts │ │ ├── styles │ │ │ ├── global.css.ts │ │ │ ├── keyframes.css.ts │ │ │ ├── media.css.ts │ │ │ ├── token.css.ts │ │ │ └── utils.css.ts │ │ └── utils │ │ │ ├── Providers.tsx │ │ │ ├── Transition.tsx │ │ │ ├── localStorage.ts │ │ │ └── test-utils.tsx │ ├── token │ │ ├── Token.tsx │ │ ├── hooks │ │ │ └── useSearch.ts │ │ └── style.css.ts │ └── vite-env.d.ts ├── styleMock.js ├── tsconfig.json └── vite.config.ts ├── core └── server │ ├── .gitignore │ ├── README.md │ ├── bsconfig.json │ ├── package.json │ ├── src │ ├── createCode.ts │ ├── createStory.ts │ ├── createUser.ts │ ├── deleteCode.ts │ ├── deleteCodeById.ts │ ├── deleteStory.ts │ ├── getCode.ts │ ├── getCodeAuthors.ts │ ├── getCodeNames.ts │ ├── getCodes.ts │ ├── getStoryCode.ts │ ├── getStoryNames.ts │ ├── hello.res │ ├── index.ts │ ├── pullCode.ts │ ├── pushCode.ts │ ├── slack │ │ ├── api.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── types │ │ └── index.ts │ ├── updateCode.ts │ ├── updateStory.ts │ └── verifyUser.ts │ └── tsconfig.json ├── docker-compose.yml ├── lerna.json ├── package.json ├── schema ├── .gitignore ├── README.md ├── compile.ts ├── json │ ├── createCodeRequest.json │ ├── createCodeResponse.json │ ├── createStoryRequest.json │ ├── createStoryResponse.json │ ├── createUserRequest.json │ ├── createUserResponse.json │ ├── deleteCodeByIdRequest.json │ ├── deleteCodeByIdResponse.json │ ├── deleteCodeRequest.json │ ├── deleteCodeResponse.json │ ├── deleteStoryRequest.json │ ├── deleteStoryResponse.json │ ├── getCodeAuthorsRequest.json │ ├── getCodeAuthorsResponse.json │ ├── getCodeNamesRequest.json │ ├── getCodeNamesResponse.json │ ├── getCodeRequest.json │ ├── getCodeResponse.json │ ├── getCodesRequest.json │ ├── getCodesResponse.json │ ├── getStoryCodeRequest.json │ ├── getStoryCodeResponse.json │ ├── getStoryNamesRequest.json │ ├── getStoryNamesResponse.json │ ├── jwtType.json │ ├── pullCodeRequest.json │ ├── pullCodeResponse.json │ ├── pushCodeRequest.json │ ├── pushCodeResponse.json │ ├── updateCodeRequest.json │ ├── updateCodeResponse.json │ ├── updateStoryRequest.json │ ├── updateStoryResponse.json │ ├── uploadSlackFileResponse.json │ ├── verifyUserRequest.json │ └── verifyUserResponse.json ├── package.json ├── template.ts ├── tsconfig.json └── utils.ts ├── server ├── API.md ├── README.md ├── package.json ├── src │ ├── @types │ │ └── global.d.ts │ ├── config.ts │ ├── constants.ts │ ├── dbModule │ │ ├── code.ts │ │ ├── index.ts │ │ ├── story.ts │ │ └── user.ts │ ├── index.ts │ ├── router.ts │ ├── schema.ts │ └── utils │ │ ├── env.ts │ │ ├── responseHandler.ts │ │ └── string.ts └── tsconfig.json ├── tsconfig.base.json └── yarn.lock /.deploy/values.yaml: -------------------------------------------------------------------------------- 1 | type: v1/appservice 2 | 3 | images: 4 | server: 5 | dockerfile: Dockerfile.kontrol 6 | 7 | configs: 8 | env: 9 | name: env 10 | 11 | services: 12 | app: 13 | class: k1.small 14 | image: server 15 | ports: 16 | - name: http 17 | port: 8080 18 | configs: 19 | - env 20 | 21 | routes: 22 | - name: backend 23 | protocol: http 24 | host: 25 | - scope: public 26 | backend: 27 | - port: http 28 | service: app 29 | -------------------------------------------------------------------------------- /.env.ci.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daangn/codepocket/8fcb5e8a7a43be0db6fee8faafb04c85088b5064/.env.ci.example -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_BASE_SERVER_URL= 2 | VITE_AUTH0_DOMAIN= 3 | VITE_AUTH0_CLIENT_ID= 4 | VITE_EMAIL_DOMAIN_NAME= 5 | 6 | SLACK_BOT_TOKEN= 7 | 8 | CODEPOCKET_CHANNEL_ID= 9 | 10 | CHAPTER_FRONTED_CHANNEL_ID= 11 | 12 | MONGO_DB_URI= 13 | 14 | MONGO_DB_NAME= 15 | 16 | BASE_SERVER_URL= 17 | 18 | BASE_WEB_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb-base", 8 | "plugin:prettier/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "json-format", 17 | "simple-import-sort" 18 | ], 19 | "rules": { 20 | "no-undef": 0, 21 | "import/no-unresolved": 0, 22 | "import/prefer-default-export": 0, 23 | "no-return-await": 0, 24 | "no-unused-vars": 0, 25 | "no-shadow": 0, 26 | "import/no-extraneous-dependencies": 0, 27 | "simple-import-sort/imports": "error", 28 | "simple-import-sort/exports": "error", 29 | "import/extensions": [ 30 | "error", 31 | "ignorePackages", 32 | { 33 | "js": "never", 34 | "jsx": "never", 35 | "ts": "never", 36 | "tsx": "never", 37 | "json": "never" 38 | } 39 | ] 40 | }, 41 | "settings": { 42 | "import/resolver": { 43 | "node": { 44 | "extensions": [ 45 | ".js", 46 | ".jsx", 47 | ".ts", 48 | ".tsx" 49 | ] 50 | } 51 | } 52 | }, 53 | "ignorePatterns": [ 54 | "**/dist/**/*", 55 | "**/bin/**/*" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | closes # 2 | -------------------------------------------------------------------------------- /.github/workflows/deploy-codepocket-external.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | paths: 6 | - 'schema/**' 7 | - 'client/**' 8 | 9 | name: codepocket pages 배포 (external) 10 | 11 | jobs: 12 | deploy-codepocket-external: 13 | name: codepocket pages 배포 (external) 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: '16.16.0' 22 | 23 | - name: 의존성을 설치해요 24 | if: steps.yarn-cache.outputs.cache-hit != 'true' 25 | run: yarn install 26 | 27 | - name: schema 프로젝트를 빌드해요 28 | working-directory: ./schema 29 | run: | 30 | yarn build 31 | 32 | - name: client 프로젝트를 빌드해요 33 | working-directory: ./client 34 | env: 35 | VITE_BASE_SERVER_URL: ${{ secrets.VITE_BASE_SERVER_URL }} 36 | VITE_AUTH0_DOMAIN: ${{ secrets.VITE_AUTH0_DOMAIN }} 37 | VITE_AUTH0_CLIENT_ID: ${{ secrets.VITE_AUTH0_CLIENT_ID }} 38 | VITE_EMAIL_DOMAIN_NAME: ${{ secrets.VITE_EMAIL_DOMAIN_NAME }} 39 | run: | 40 | yarn build 41 | 42 | - name: 서비스를 Cloudflare pages에 배포해요 43 | uses: cloudflare/wrangler-action@2.0.0 44 | with: 45 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 46 | apiToken: ${{ secrets.CF_API_TOKEN }} 47 | command: pages publish ./client/dist --project-name=codepocket 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | Lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: yarn 14 | run: yarn 15 | - name: check lint 16 | run: yarn lint 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | Test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: yarn 14 | run: yarn 15 | 16 | - name: set env var 17 | run: | 18 | echo "POCKET_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6InFrcmRtc3RscjMiLCJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJpYXQiOjE2NjE2OTQxMjV9.mZxJhIEcjtr-weC3Dmqjq5IWAt49DTONBLf2s-wr8K4" >> $GITHUB_ENV 19 | echo $POCKET_TOKEN 20 | 21 | - name: test all 22 | run: yarn test 23 | 24 | - name: if fail 25 | uses: actions/github-script@v6 26 | with: 27 | github-token: ${{github.token}} 28 | script: | 29 | const ref = "${{github.ref}}" 30 | const pull_number = Number(ref.split("/")[2]) 31 | await github.pulls.createReview({ 32 | ...context.repo, 33 | pull_number, 34 | body:"테스트 코드를 다시 확인해주세요", 35 | event: "REQUEST_CHANGES" 36 | }) 37 | if: failure() 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pnp.* 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/plugins 5 | !.yarn/releases 6 | !.yarn/sdks 7 | !.yarn/versions 8 | 9 | /.idea/inspectionProfiles/Project_Default.xml 10 | /.idea/modules.xml 11 | /.idea/vcs.xml 12 | /.idea/.gitignore 13 | /.idea/stackflow.iml 14 | 15 | .DS_Store 16 | dist 17 | node_modules 18 | .ultra.cache.json 19 | 20 | .env.dev 21 | .env -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "semi": true, 5 | "trailingComma": "all", 6 | "printWidth": 100, 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "eslint.nodePath": ".yarn/sdks", 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "typescript.enablePromptUseWorkspaceTsdk": true 9 | } 10 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint your application uses 20 | module.exports = absRequire(`eslint`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.19.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vim 6 | - vscode 7 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/typescript.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/typescript.js your application uses 20 | module.exports = absRequire(`typescript/lib/typescript.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.7.4-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 5 | spec: '@yarnpkg/plugin-typescript' 6 | 7 | yarnPath: .yarn/releases/yarn-3.2.1.cjs 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 기여 문서 2 | 3 | ### Yarn berry 사용하기 4 | 5 | yarn의 버전을 아래 명령어를 사용해서 맞춰주세요 6 | 7 | ``` 8 | yarn set version 3.2.1 9 | ``` 10 | 11 | ### .env 작성하기 12 | 13 | 아래와 같이 .env파일을 만들어주세요!(뒤에 !가 들어가있는 부분은 직접 값을 넣어주셔야해요) 14 | 15 | ``` 16 | VITE_BASE_SERVER_URL=http://0.0.0.0:8080 17 | VITE_AUTH0_DOMAIN=AUTO0 도메인! 18 | VITE_AUTH0_CLIENT_ID=AUTH0 클라이언트 아이디! 19 | VITE_EMAIL_DOMAIN_NAME=* 20 | 21 | SLACK_BOT_TOKEN=슬랙 봇 토큰! 22 | 23 | CODEPOCKET_CHANNEL_ID=코드 알림이 올라올 슬랙 채널 아이디! 24 | 25 | CHAPTER_FRONTED_CHANNEL_ID=코드가 올라올 슬랙 채널 아이디! 26 | 27 | MONGO_DB_URI=mongodb://root:example@localhost:27017 28 | 29 | MONGO_DB_NAME=codepocket 30 | 31 | BASE_SERVER_URL=http://0.0.0.0:8080 32 | ``` 33 | 34 | ### Codepocket 구동하기 35 | 36 | Codepocket은 docker compose파일을 제공하고 있음으로 쉽게 동작시켜 볼 수 있어요. 37 | 38 | Docker Desktop을 설치해주고 아래 명령어를 실행시키면 쉽게 Codepocket을 동작시킬 수 있어요! 39 | 40 | ``` 41 | docker compose up -d 42 | ``` 43 | -------------------------------------------------------------------------------- /Dockerfile.compose: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN yarn 8 | RUN yarn build 9 | 10 | CMD ["yarn", "dev"] 11 | -------------------------------------------------------------------------------- /Dockerfile.kontrol: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | ENV CI=true 4 | ENV NODE_ENV=production 5 | 6 | WORKDIR /app 7 | 8 | COPY . . 9 | 10 | RUN yarn install --immutable --silent 11 | RUN yarn build 12 | 13 | EXPOSE 8080 14 | 15 | CMD ["yarn", "workspace", "@codepocket/server", "start"] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codepocket (Deprecated) 2 | 3 | - [codepocket (외부 공개용)](https://codepocket.pages.dev/) 4 | - [codepocket-daangn (내부용)](https://codepocket-daangn.pages.dev/) 5 | 6 | ## Introduction 7 | 8 | > **함께 다양한 코드를 모으고 기여해서 중복 노력이 없는 개발 환경을 만들어요** 9 | 10 | Codepocket은 중복 노력을 줄이기 위해서 만들어진 오픈소스 소프트웨어예요. 11 | 코드들을 모아놓는 레지스트리로 아래와 같은 필요에 의해 만들어졌어요 12 | 13 | **❗️ 현재는 `js`, `jsx`, `ts`, `tsx` 확장자의 파일만 지원합니다. ❗️** 14 | 15 | - 자주 쓰는 코드를 올리고 필요할 때 가져다 사용해요 16 | - 다른 사람이 올린 자주 쓰는 코드를 구경하고 필요하다면 가져다 사용해요 17 | - Codepocket에 올라온 코드에 스토리를 만들어서 코드의 다양한 사용법을 공유해요 18 | 19 | ## Getting Started 20 | 21 | > 현재는 cli를 통해서만 Codepocket에 코드를 올리고 내려받을 수 있어요. 22 | 23 | ``` 24 | $ npm i -g @codepocket/cli 25 | $ yarn global add @codepocket/cli 26 | ``` 27 | 28 | [자세한 cli 사용법](https://github.com/daangn/codepocket/blob/main/cli/README.md) 29 | 30 | ## Contribution Guide 31 | 32 | [기여하기](https://github.com/daangn/codepocket/blob/main/CONTRIBUTING.md) 33 | 34 | ## Packages 35 | 36 | > Codepocket monorepo packages 37 | 38 | - [@codepocket/cli](https://github.com/daangn/codepocket/tree/main/cli) 39 | - [@codepocket/client](https://github.com/daangn/codepocket/tree/main/client) 40 | - [@codepocket/core-server](https://github.com/daangn/codepocket/tree/main/core/server) 41 | - [@codepocket/schema](https://github.com/daangn/codepocket/tree/main/schema) 42 | - [@codepocket/server](https://github.com/daangn/codepocket/tree/main/server) 43 | 44 | ## License 45 | 46 | [Apache 2.0](https://github.com/daangn/codepocket/blob/main/LICENSE) 47 | -------------------------------------------------------------------------------- /cli/.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | bin -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # @codepocket/cli 2 | 3 | ## Install 4 | 5 | ``` 6 | $ npm i -g @codepocket/cli 7 | $ yarn global add @codepocket/cli 8 | ``` 9 | 10 | ## Required 11 | 12 | > 환경변수로 웹에서 발급받은 토큰정보를 넣어주세요 13 | 14 | ``` 15 | export POCKET_TOKEN= 16 | ``` 17 | 18 | ## Commands 19 | 20 | ### help 21 | 22 | ``` 23 | pocket --help 24 | ``` 25 | 26 | ### push 27 | 28 | ``` 29 | pocket push <파일경로> -n [파일명] 30 | ``` 31 | 32 | | 옵션 | 설명 | 예제 | 33 | | ---------- | --------------------------------------- | ---------------------------------------- | 34 | | -n, --name | 저장될 파일이름을 직접 지정할 수 있어요 | pocket push pocket.txt -n codepocket.txt | 35 | 36 | ### pull 37 | 38 | ``` 39 | pocket pull <작성자> <코드명> -p [저장경로] 40 | ``` 41 | 42 | | 옵션 | 설명 | 예제 | 43 | | ---------- | ------------------------------------------ | --------------------------------------------------- | 44 | | -p, --path | 파일이 저장될 경로를 직접 지정할 수 있어요 | pocket pull author code.txt ./pocket/codepocket.txt | 45 | 46 | ### list 47 | 48 | ``` 49 | pocket list -a [작성자] -f [파일명] 50 | ``` 51 | 52 | | 옵션 | 설명 | 예제 | 53 | | -------------- | ------------------------------------------------ | --------------------- | 54 | | -a, --author | 입력된 작성자 이름으로 리스트를 조회할 수 있어요 | pocket list -a author | 55 | | -f, --fileName | 입력된 파일명 이름으로 리스트를 조회할 수 있어요 | pocket list -f code | 56 | 57 | ### delete 58 | 59 | > 자신이 올린 코드만 삭제할 수 있어요 60 | 61 | ``` 62 | pocket delete <코드명> 63 | ``` 64 | -------------------------------------------------------------------------------- /cli/bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../lib/index.ts'; 4 | -------------------------------------------------------------------------------- /cli/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | setupFilesAfterEnv: ['/lib/setupTests.ts'], 4 | testMatch: ['**/lib/**/*.test.ts'], 5 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 6 | transform: { 7 | '^.+\\.(t|j)sx?$': ['@swc/jest'], 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /cli/lib/__mocks__/mockup.ts: -------------------------------------------------------------------------------- 1 | import { GetCodeNamesResponse } from '@codepocket/schema'; 2 | 3 | const generateListCodeItem = (key: string, isAnonymous: boolean) => ({ 4 | codeName: `${key}codeName`, 5 | codeAuthor: `${key}codeAuthor`, 6 | isAnonymous, 7 | }); 8 | 9 | export const generateListCodeResponseMock = ( 10 | { isAuthor } = { isAuthor: false }, 11 | ): GetCodeNamesResponse => ({ 12 | codeInfos: [ 13 | generateListCodeItem(isAuthor ? '' : '1', false), 14 | generateListCodeItem(isAuthor ? '' : '2', false), 15 | generateListCodeItem(isAuthor ? '' : '3', true), 16 | ], 17 | message: '', 18 | }); 19 | -------------------------------------------------------------------------------- /cli/lib/__mocks__/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | 3 | import * as handlers from './handlers'; 4 | 5 | const executedHandler = Object.values(handlers).map((handler) => handler()); 6 | 7 | const server = setupServer(...executedHandler); 8 | export default server; 9 | -------------------------------------------------------------------------------- /cli/lib/command/__test__/delete.test.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { deleteCodeHandler } from '../../__mocks__/handlers'; 4 | import server from '../../__mocks__/server'; 5 | import { logger } from '../../utils'; 6 | import deleteCommand from '../delete'; 7 | 8 | jest.mock('chalk', () => ({ 9 | yellow: jest.fn(), 10 | green: jest.fn(), 11 | })); 12 | 13 | const consoleErrorSpy = jest.spyOn(logger, 'error'); 14 | const consoleLogSpy = jest.spyOn(logger, 'info'); 15 | 16 | beforeEach(() => { 17 | consoleErrorSpy.mockClear(); 18 | consoleLogSpy.mockClear(); 19 | }); 20 | 21 | const chalkYellowMock = chalk.yellow as jest.MockedFunction; 22 | const chalkGreenMock = chalk.green as jest.MockedFunction; 23 | chalkYellowMock.mockImplementation((value: unknown) => value as string); 24 | chalkGreenMock.mockImplementation((value: unknown) => value as string); 25 | 26 | const FILENAME = 'FILENAME'; 27 | 28 | it('삭제가 정상적으로 진행될 경우, 테스트', async () => { 29 | const expectedLog = '🌟 저장소에서 코드가 삭제되었어요!'; 30 | await deleteCommand(FILENAME); 31 | expect(consoleLogSpy).toBeCalledWith(expectedLog); 32 | expect(consoleLogSpy).toBeCalledTimes(1); 33 | }); 34 | 35 | it('서버에러일 경우, 에러 테스트', async () => { 36 | server.use(deleteCodeHandler('SERVER')); 37 | const expectedError = '서버 에러 발생'; 38 | 39 | await deleteCommand(FILENAME); 40 | expect(consoleErrorSpy).toBeCalledWith(expectedError); 41 | expect(consoleErrorSpy).toBeCalledTimes(1); 42 | }); 43 | 44 | it('네트워크 에러일 경우, 에러 테스트', async () => { 45 | server.use(deleteCodeHandler('NETWORK')); 46 | const expectedError = '네트워크 에러 발생'; 47 | 48 | await deleteCommand(FILENAME); 49 | expect(consoleErrorSpy).toBeCalledWith(expectedError); 50 | expect(consoleErrorSpy).toBeCalledTimes(1); 51 | }); 52 | -------------------------------------------------------------------------------- /cli/lib/command/daangn.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { logger } from '../utils'; 4 | 5 | export default async () => { 6 | logger.info(chalk.cyan('당근을 흔드셨군요!🥕')); 7 | logger.info( 8 | chalk.bold.rgb(237, 145, 33).underline('https://github.com/daangn/codepocket/issues') + 9 | chalk.cyan(' 여기서 이슈를 남겨주세요!'), 10 | ); 11 | logger.info(chalk.gray.strikethrough('테스트는 유저가')); 12 | }; 13 | -------------------------------------------------------------------------------- /cli/lib/command/delete.ts: -------------------------------------------------------------------------------- 1 | import { to } from 'await-to-js'; 2 | import chalk from 'chalk'; 3 | 4 | import { deleteCodeAPI } from '../api'; 5 | import { logger } from '../utils'; 6 | 7 | export default async (codeName: string) => { 8 | const [error] = await to( 9 | (async () => { 10 | const pocketToken = process.env.POCKET_TOKEN || ''; 11 | await deleteCodeAPI({ codeName, pocketToken }); 12 | })(), 13 | ); 14 | 15 | if (error) return logger.error(chalk.yellow(error.message)); 16 | return logger.info(chalk.green('🌟 저장소에서 코드가 삭제되었어요!')); 17 | }; 18 | -------------------------------------------------------------------------------- /cli/lib/command/list.ts: -------------------------------------------------------------------------------- 1 | import { to } from 'await-to-js'; 2 | import chalk from 'chalk'; 3 | 4 | import { listCodeAPI } from '../api'; 5 | import { logger } from '../utils'; 6 | 7 | export default async ({ fileName, author }: { fileName?: string; author?: string }) => { 8 | const [error, result] = await to( 9 | (async () => { 10 | const response = await listCodeAPI({ 11 | codeName: fileName || '', 12 | codeAuthor: author || '', 13 | }); 14 | return response; 15 | })(), 16 | ); 17 | if (error) return logger.error(chalk.yellow(error.message)); 18 | 19 | const authors = [ 20 | ...new Set(result.codeInfos.filter((code) => !code.isAnonymous).map((code) => code.codeAuthor)), 21 | ]; 22 | 23 | const codeInfoList = result.codeInfos 24 | .map((code) => `${code.isAnonymous ? '' : code.codeAuthor}/${code.codeName}`) 25 | .join('\r\n'); 26 | return logger.info( 27 | `${chalk.green(authors.length === 1 ? authors[0] : '모두')}의 코드들입니다🥕\n${codeInfoList}`, 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /cli/lib/command/open.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | import { WEB_DEV_URL, WEB_PROD_URL } from '../env'; 4 | 5 | export default (author: string, fileName: string) => { 6 | const baseUrl = process.env.NODE_ENV === 'development' ? WEB_DEV_URL : WEB_PROD_URL; 7 | const url = `${baseUrl}/detail/${author}/${fileName}`; 8 | execSync(`open ${url}`, { encoding: 'utf-8' }); 9 | }; 10 | -------------------------------------------------------------------------------- /cli/lib/command/pull.ts: -------------------------------------------------------------------------------- 1 | import { to } from 'await-to-js'; 2 | import chalk from 'chalk'; 3 | import fs from 'fs'; 4 | import inquirer from 'inquirer'; 5 | import path from 'path'; 6 | 7 | import { getCodeAuthors, pullCodeAPI } from '../api'; 8 | import { logger } from '../utils'; 9 | 10 | export default async (codeName: string, option: { path?: string }) => { 11 | const ANONYMOUS = '익명이'; 12 | 13 | const [error] = await to( 14 | (async () => { 15 | const optionPath = option.path || '.'; 16 | const currentCommandPath = process.env.INIT_CWD || ''; 17 | 18 | const authors = await getCodeAuthors({ codeName }); 19 | const anonymousAuthor = authors.find((author) => author.isAnonymous)?.codeAuthor; 20 | const { selectedAuthor } = await inquirer.prompt({ 21 | name: 'selectedAuthor', 22 | type: 'list', 23 | message: '누구의 코드를 가져오시겠어요?', 24 | choices: authors.map((author) => (author.isAnonymous ? ANONYMOUS : author.codeAuthor)), 25 | }); 26 | 27 | const codeAuthor = selectedAuthor !== ANONYMOUS ? selectedAuthor : anonymousAuthor; 28 | const code = await pullCodeAPI({ codeAuthor, codeName }); 29 | const myPath = path.resolve(currentCommandPath, optionPath); 30 | if (!fs.existsSync(myPath)) { 31 | const [err] = await to((async () => fs.writeFileSync(myPath, code))()); 32 | if (err) throw new Error('🚨 존재하지 않는 경로예요'); 33 | return; 34 | } 35 | 36 | const newPath = `${myPath}/${codeName}`; 37 | const noDuplicatedFile = !fs.lstatSync(myPath).isFile() && !fs.existsSync(newPath); 38 | if (!noDuplicatedFile) throw new Error('🚨 해당 경로에 동일한 이름의 파일이 존재해요'); 39 | fs.writeFileSync(newPath, code); 40 | })(), 41 | ); 42 | if (error) return logger.error(chalk.yellow(error.message)); 43 | return logger.info(chalk.green('🌟 코드를 성공적으로 가져왔어요!')); 44 | }; 45 | -------------------------------------------------------------------------------- /cli/lib/command/push.ts: -------------------------------------------------------------------------------- 1 | import { to } from 'await-to-js'; 2 | import chalk from 'chalk'; 3 | import fs from 'fs'; 4 | import inquirer from 'inquirer'; 5 | import path from 'path'; 6 | 7 | import { pushCodeAPI } from '../api'; 8 | import { logger } from '../utils'; 9 | 10 | const getFileName = (filePath: string) => 11 | filePath 12 | .split('/') 13 | .reverse() 14 | .find((p) => !p.includes('index')); 15 | 16 | export default async (filePath: string, option: { name?: string }) => { 17 | const [error] = await to( 18 | (async () => { 19 | const isExistPath = fs.existsSync(filePath); 20 | const isPathIsDir = isExistPath && !fs.lstatSync(filePath).isFile(); 21 | if (!isExistPath || isPathIsDir) throw Error('🚨 올바른 파일 경로를 입력해주세요'); 22 | 23 | const currentCommandPath = process.env.INIT_CWD || ''; 24 | const code = fs.readFileSync(path.resolve(currentCommandPath, filePath), { 25 | encoding: 'utf-8', 26 | }); 27 | const pocketToken = process.env.POCKET_TOKEN || ''; 28 | const codeName = option.name || getFileName(filePath); 29 | if (codeName === '' || codeName === '.') 30 | throw Error('🚨 index는 파일명으로 사용불가능해요. -n옵션을 사용해보세요'); 31 | if (!codeName) throw Error('🚨 입력하신 경로에서 파일명을 찾을 수 없어요'); 32 | 33 | const { isAnonymous } = await inquirer.prompt({ 34 | name: 'isAnonymous', 35 | type: 'confirm', 36 | message: '익명으로 올리시겠어요?', 37 | default: true, 38 | }); 39 | 40 | await pushCodeAPI({ code, codeName, pocketToken, isAnonymous }); 41 | })(), 42 | ); 43 | if (error) return logger.error(chalk.yellow(error.message)); 44 | return logger.info(chalk.green('🌟 기여해주셔서 고마워요!')); 45 | }; 46 | -------------------------------------------------------------------------------- /cli/lib/env.ts: -------------------------------------------------------------------------------- 1 | // 유저가 직접 입력 2 | export const TOKEN = process.env.POCKET_TOKEN; 3 | 4 | export const WEB_DEV_URL = process.env.BASE_CLIENT_URL_DEV; 5 | export const WEB_PROD_URL = process.env.BASE_CLIENT_URL_PROD; 6 | -------------------------------------------------------------------------------- /cli/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { Command, program } from 'commander'; 2 | 3 | import packageJSON from '../package.json' assert { type: 'json' }; // eslint-disable-line 4 | import DaangnCommandAction from './command/daangn'; 5 | import deleteCommandAction from './command/delete'; 6 | import listCommandAction from './command/list'; 7 | import openCommandAction from './command/open'; 8 | import pullCommandAction from './command/pull'; 9 | import pushCommandAction from './command/push'; 10 | 11 | const pushCommand = new Command('push') 12 | .description('인자로 입력받은 경로의 코드를 codepocket에 추가해요') 13 | .argument('', '올리고 싶은 코드의 경로를 입력해주세요') 14 | .option('-n, --name ', '파일의 이름을 직접 설정하고 싶으시면 입력해주세요') 15 | .action(pushCommandAction); 16 | 17 | const pullCommand = new Command('pull') 18 | .description('입력받은 개발자의 코드를 codepocket에서 가져와요. 저장될 경로를 지정할 수 있어요') 19 | .argument('', '코드이름을 입력해주세요') 20 | .option('-p, --path ', '코드가 복사될 경로를 입력해주세요') 21 | .action(pullCommandAction); 22 | 23 | const listCommand = new Command('list') 24 | .description('저장된 파일명들을 가져와요. 작성자를 지정할 수 있어요') 25 | .option('-a, --author ', '해당 작성자가 작성한 파일명들을 가져올 수 있어요') 26 | .option('-f, --fileName ', '입력한 글자가 포함된 파일명들을 가져올 수 있어요') 27 | .action(listCommandAction); 28 | 29 | const deleteCommand = new Command('delete') 30 | .description('자신이 올린 파일을 삭제해요') 31 | .argument('', '삭제할 파일 이름을 입력해요') 32 | .action(deleteCommandAction); 33 | 34 | const openCommand = new Command('open') 35 | .argument('', '저자를 입력해주세요') 36 | .argument('', '파일명을 입력해주세요') 37 | .action(openCommandAction); 38 | 39 | const daangnCommand = new Command('🥕🥕') 40 | .description('불만이 있으면 흔들어주세요!') 41 | .action(DaangnCommandAction); 42 | 43 | program.helpOption('-h, --help', '도와주세요😥'); 44 | program.version(packageJSON.version, '-v, --version', '현재 버전 보기'); 45 | program.description('여러분의 코드를 공유해봐요!\n불만이 있다면 🥕을 흔들어 주세요!'); 46 | program.addCommand(pushCommand); 47 | program.addCommand(pullCommand); 48 | program.addCommand(listCommand); 49 | program.addCommand(deleteCommand); 50 | program.addCommand(openCommand); 51 | program.addCommand(daangnCommand); 52 | program.parseAsync(process.argv); 53 | -------------------------------------------------------------------------------- /cli/lib/setupTests.ts: -------------------------------------------------------------------------------- 1 | import server from './__mocks__/server'; 2 | 3 | beforeAll(() => { 4 | server.listen(); 5 | }); 6 | 7 | afterEach(() => { 8 | server.resetHandlers(); 9 | }); 10 | 11 | afterAll(() => { 12 | server.close(); 13 | }); 14 | -------------------------------------------------------------------------------- /cli/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { JwtType } from '@codepocket/schema'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | import { TOKEN } from './env'; 5 | 6 | export const logger = { 7 | info: console.log, 8 | error: console.error, 9 | }; 10 | 11 | export const getBaseUrl = (): string => { 12 | try { 13 | if (!TOKEN) throw new Error('발급받은 토큰을 환경변수에 넣어주세요!'); 14 | 15 | // NOTE: KEY값은 server/src/dbModule/user.ts의 KEY변수와 동일하게 맞춰주세요 16 | const KEY = 'key'; 17 | const decoded = jwt.verify(TOKEN, KEY) as JwtType; 18 | return decoded.serverUrl; 19 | } catch (error) { 20 | logger.error((error as Error).message); 21 | return ''; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/cli", 3 | "version": "0.0.7", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/daangn/codepocket.git" 7 | }, 8 | "license": "Apache-2.0", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.mjs" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "source": "./bin/index.ts", 17 | "bin": { 18 | "pocket": "./dist/index.mjs" 19 | }, 20 | "scripts": { 21 | "build": "nanobundle build", 22 | "pocket": "NODE_ENV=development yarn node -r tsm ./lib/index.ts", 23 | "prepare": "yarn build", 24 | "test": "NODE_ENV=development jest --silent --verbose", 25 | "test:log": "NODE_ENV=development jest" 26 | }, 27 | "dependencies": { 28 | "@codepocket/schema": "^0.0.4", 29 | "await-to-js": "^3.0.0", 30 | "axios": "^0.27.2", 31 | "chalk": "^5.0.1", 32 | "commander": "^9.3.0", 33 | "dotenv-safe": "^8.2.0", 34 | "inquirer": "^9.1.0", 35 | "jsonwebtoken": "^8.5.1" 36 | }, 37 | "devDependencies": { 38 | "@swc/core": "^1.2.211", 39 | "@swc/jest": "^0.2.21", 40 | "@types/dotenv-safe": "^8", 41 | "@types/inquirer": "^9", 42 | "@types/jest": "^28.1.4", 43 | "@types/jsonwebtoken": "^8", 44 | "@types/node": "^18.0.3", 45 | "jest": "^28.1.2", 46 | "msw": "^0.43.1", 47 | "nanobundle": "^0.0.28", 48 | "tsm": "^2.2.2" 49 | }, 50 | "packageManager": "yarn@3.2.1", 51 | "publishConfig": { 52 | "access": "public" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "baseUrl": ".", 6 | "outDir": "dist" 7 | }, 8 | "include": ["./lib"] 9 | } 10 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:react-hooks/recommended" 4 | ], 5 | "plugins": [ 6 | "react-hooks" 7 | ], 8 | "rules": { 9 | "react-hooks/rules-of-hooks": "error", 10 | "react-hooks/exhaustive-deps": "warn", 11 | "import/no-extraneous-dependencies": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/.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 | -------------------------------------------------------------------------------- /client/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-interactions', 7 | ], 8 | framework: '@storybook/react', 9 | core: { 10 | builder: '@storybook/builder-vite', 11 | }, 12 | features: { 13 | storyStoreV7: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /client/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # @codepocket/client 2 | 3 | > **코드를 보고, 검색하고, 스토리를 만들어요** 4 | 5 | - `@codepocket/cli`로 push한 코드를 볼 수 있어요. 6 | - 해당 코드에 대한 여러 스토리를 만들어 볼 수 있어요. 7 | - 검색을 통해서 다른 사람이 올린 코드를 볼 수 있어요. 8 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Codepocket 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jest-environment-jsdom', 3 | testMatch: ['**/src/**/*.test.tsx'], 4 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 5 | transform: { 6 | '^.+\\.(t|j)sx?$': [ 7 | '@swc/jest', 8 | { 9 | jsc: { 10 | transform: { 11 | react: { 12 | runtime: 'automatic', 13 | }, 14 | }, 15 | }, 16 | }, 17 | ], 18 | }, 19 | moduleNameMapper: { 20 | '^@shared/(.*)$': '/src/shared/$1', 21 | '\\.css$': '/styleMock.js', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/client", 3 | "version": "0.0.3", 4 | "private": true, 5 | "scripts": { 6 | "build": "tsc && vite build", 7 | "build-storybook": "build-storybook", 8 | "dev": "NODE_ENV=development vite", 9 | "preview": "vite preview", 10 | "storybook": "start-storybook -p 6006" 11 | }, 12 | "dependencies": { 13 | "@auth0/auth0-react": "^1.10.2", 14 | "@codepocket/schema": "^0.0.4", 15 | "@codesandbox/sandpack-react": "^1.2.4", 16 | "@codesandbox/sandpack-themes": "^1.0.0", 17 | "@seed-design/design-token": "^1.0.0-alpha.0", 18 | "@seed-design/stylesheet": "^1.0.0-beta.1", 19 | "@tanstack/react-query": "^4.1.3", 20 | "@tanstack/react-query-devtools": "^4.0.3", 21 | "@vanilla-extract/css": "^1.7.2", 22 | "@vanilla-extract/recipes": "^0.2.5", 23 | "@vanilla-extract/vite-plugin": "^3.2.1", 24 | "@vitejs/plugin-react": "^1.3.0", 25 | "await-to-js": "^3.0.0", 26 | "axios": "^0.27.2", 27 | "jotai": "^1.7.7", 28 | "lodash": "^4.17.21", 29 | "polished": "^4.2.2", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-error-boundary": "^3.1.4", 33 | "react-router-dom": "^6.3.0", 34 | "react-syntax-highlighter": "^15.5.0", 35 | "vite": "^3.0.3", 36 | "vite-plugin-checker": "^0.4.8", 37 | "vite-plugin-rewrite-all": "^0.1.2" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.18.6", 41 | "@mdx-js/react": "^1.6.22", 42 | "@storybook/addon-actions": "^6.5.9", 43 | "@storybook/addon-docs": "^6.5.9", 44 | "@storybook/addon-essentials": "^6.5.9", 45 | "@storybook/addon-interactions": "^6.5.9", 46 | "@storybook/addon-links": "^6.5.9", 47 | "@storybook/builder-vite": "^0.1.39", 48 | "@storybook/react": "^6.5.9", 49 | "@storybook/testing-library": "^0.0.13", 50 | "@swc/core": "^1.2.218", 51 | "@swc/jest": "^0.2.22", 52 | "@testing-library/react": "^13.3.0", 53 | "@types/babel__core": "^7", 54 | "@types/jest": "^28.1.6", 55 | "@types/lodash": "^4", 56 | "@types/react": "^18.0.14", 57 | "@types/react-dom": "^18.0.6", 58 | "@types/react-syntax-highlighter": "^15", 59 | "babel-loader": "^8.2.5", 60 | "eslint-plugin-react-hooks": "^4.6.0", 61 | "jest": "^28.1.3", 62 | "jest-environment-jsdom": "^28.1.3", 63 | "msw": "^0.44.2", 64 | "typescript": "^4.7.4", 65 | "vitest": "^0.19.1" 66 | }, 67 | "packageManager": "yarn@3.2.1" 68 | } 69 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from 'react-router-dom'; 2 | 3 | import AuthPage from './auth/Auth'; 4 | import DetailPage from './detail/Detail'; 5 | import PocketPage from './pocket/Pocket'; 6 | import * as routes from './routes'; 7 | import PrivateRoute from './shared/components/PrivateRoute'; 8 | import TokenPage from './token/Token'; 9 | 10 | function App() { 11 | return ( 12 | 13 | } /> 14 | } />} 17 | /> 18 | } />} 21 | /> 22 | } /> 23 | } /> 24 | 25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /client/src/__mocks__/handlers.ts: -------------------------------------------------------------------------------- 1 | import { VerifyUserResponse } from '@codepocket/schema'; 2 | import { rest } from 'msw'; 3 | 4 | import { BASE_SERVER_URL } from '../shared/constant'; 5 | 6 | export const verifyUserHandler = () => 7 | rest.post(`${BASE_SERVER_URL}/user/auth`, (_, res, ctx) => { 8 | return res( 9 | ctx.status(200), 10 | ctx.json({ validUser: true, userName: 'shell', message: 'success' }), 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/__mocks__/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | 3 | import * as handlers from './handlers'; 4 | 5 | const executedHandler = Object.values(handlers).map((handler) => handler()); 6 | 7 | const server = setupServer(...executedHandler); 8 | export default server; 9 | -------------------------------------------------------------------------------- /client/src/auth/Auth.test.tsx: -------------------------------------------------------------------------------- 1 | // import { render, screen } from '@shared/utils/test-utils'; 2 | import { expect, it } from 'vitest'; 3 | 4 | // import Auth from './Auth'; 5 | 6 | it('test', () => { 7 | // render(); 8 | // screen.getByText('Codepocket'); 9 | expect(1 + 1).toBe(2); 10 | }); 11 | -------------------------------------------------------------------------------- /client/src/auth/api.ts: -------------------------------------------------------------------------------- 1 | export const verifyUserUrl = '/user/auth'; 2 | export const createUserUrl = '/user'; 3 | -------------------------------------------------------------------------------- /client/src/auth/components/SubTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import useTyping from '../../hooks/useTyping'; 2 | import * as style from './style.css'; 3 | 4 | interface SubTitleInterface { 5 | content: string; 6 | startDelay: number; 7 | } 8 | 9 | const SubTitle: React.FC = ({ content, startDelay }) => { 10 | const { text } = useTyping({ 11 | content, 12 | startDelay, 13 | }); 14 | 15 | return

{text || ' '}

; 16 | }; 17 | 18 | export default SubTitle; 19 | -------------------------------------------------------------------------------- /client/src/auth/components/SubTitle/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as m from '@shared/styles/media.css'; 3 | import * as u from '@shared/styles/utils.css'; 4 | import { style } from '@vanilla-extract/css'; 5 | import { rem } from 'polished'; 6 | 7 | export const subtitle = style([ 8 | u.positionRelative, 9 | { 10 | color: vars.$scale.color.gray900, 11 | fontSize: rem(17), 12 | }, 13 | m.small({ 14 | fontSize: rem(14), 15 | }), 16 | ]); 17 | -------------------------------------------------------------------------------- /client/src/auth/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as SubTitle } from './SubTitle'; 2 | -------------------------------------------------------------------------------- /client/src/auth/hooks/useCreateUser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateUserRequest, 3 | CreateUserResponse, 4 | createUserResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { localStorage } from '@shared/utils/localStorage'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | import { generateTokenPath } from '../../routes'; 11 | import { createUserUrl } from '../api'; 12 | 13 | const useCreateUser = () => { 14 | const navigate = useNavigate(); 15 | const { mutate: createUserMutate } = useCustomMutation< 16 | CreateUserResponse, 17 | CreateUserResponse, 18 | CreateUserRequest['body'] 19 | >({ 20 | url: createUserUrl, 21 | method: 'POST', 22 | validator: createUserResponseValidate, 23 | options: { 24 | onSuccess: async (response) => { 25 | const { pocketToken: token, userId } = response; 26 | localStorage.setUserId(userId); 27 | localStorage.setUserToken(token); 28 | navigate(generateTokenPath({ token })); 29 | }, 30 | }, 31 | }); 32 | 33 | return { createUser: createUserMutate }; 34 | }; 35 | 36 | export default useCreateUser; 37 | -------------------------------------------------------------------------------- /client/src/auth/hooks/useCustomAuth0.ts: -------------------------------------------------------------------------------- 1 | import { useAuth0 } from '@auth0/auth0-react'; 2 | import { useCallback, useMemo } from 'react'; 3 | 4 | interface UseCustomAuth0Props { 5 | domain: string; 6 | } 7 | 8 | const useCustomAuth0 = ({ domain }: UseCustomAuth0Props) => { 9 | const { isAuthenticated, user, logout, loginWithPopup } = useAuth0(); 10 | 11 | const isExternalUser = useMemo(() => domain === '*', [domain]); 12 | const isValidEmailDomain = useCallback(() => { 13 | if (isExternalUser) return true; 14 | if (!user || !user.email) return false; 15 | 16 | const domains = domain.split(','); 17 | const userDomain = user?.email.split('@')[1]; 18 | return domains.includes(userDomain); 19 | }, [domain, isExternalUser, user]); 20 | 21 | return { 22 | user, 23 | isAuthenticated, 24 | isExternalUser, 25 | loginWithPopup, 26 | logout, 27 | isValidEmailDomain, 28 | }; 29 | }; 30 | 31 | export default useCustomAuth0; 32 | -------------------------------------------------------------------------------- /client/src/auth/hooks/useTyping.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import { useCallback, useEffect, useState } from 'react'; 3 | 4 | interface TypingHookProps { 5 | content: string; 6 | startDelay?: number; 7 | } 8 | 9 | function sleep(t: number) { 10 | return new Promise((resolve) => { 11 | setTimeout(resolve, t); 12 | }); 13 | } 14 | 15 | function useTyping({ content, startDelay = 0 }: TypingHookProps) { 16 | const [text, setText] = useState(''); 17 | const [isTypingEnd, setIsTypingEnd] = useState(true); 18 | 19 | const addText = useCallback( 20 | (text: string) => `${text.slice(0, text.length - 1) + content[text.length - 1]}|`, 21 | [content], 22 | ); 23 | 24 | useEffect(() => { 25 | if (isTypingEnd) return; 26 | (async () => { 27 | await sleep(70); 28 | if (text.length === content.length + 1) return setIsTypingEnd(true); 29 | return setText((text) => addText(text)); 30 | })(); 31 | }, [content, text, isTypingEnd, addText]); 32 | 33 | useEffect(() => { 34 | (async () => { 35 | await sleep(startDelay); 36 | setIsTypingEnd(false); 37 | setText('|'); 38 | })(); 39 | }, [content, startDelay]); 40 | 41 | return { text, isTypingEnd }; 42 | } 43 | 44 | export default useTyping; 45 | -------------------------------------------------------------------------------- /client/src/auth/hooks/useVerifyUser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VerifyUserRequest, 3 | VerifyUserResponse, 4 | verifyUserResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { localStorage } from '@shared/utils/localStorage'; 8 | import { useCallback } from 'react'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | import { verifyUserUrl } from '../api'; 12 | 13 | type VerifyUserBodyType = VerifyUserRequest['body']; 14 | interface UseVerifyUserMutationProps { 15 | path: string; 16 | } 17 | 18 | const useVerifyUser = ({ path }: UseVerifyUserMutationProps) => { 19 | const navigate = useNavigate(); 20 | 21 | const onSuccess = (_: VerifyUserResponse, vars: VerifyUserBodyType) => { 22 | localStorage.setUserToken(vars.pocketToken); 23 | navigate(path); 24 | }; 25 | 26 | const { mutate: verifyUserMutate } = useCustomMutation< 27 | VerifyUserResponse, 28 | VerifyUserResponse, 29 | VerifyUserBodyType 30 | >({ 31 | url: verifyUserUrl, 32 | validator: verifyUserResponseValidate, 33 | method: 'POST', 34 | options: { 35 | onSuccess, 36 | }, 37 | }); 38 | 39 | const verifyUser = useCallback(async () => { 40 | const token = localStorage.getUserToken(); 41 | if (!token) return; 42 | 43 | verifyUserMutate({ pocketToken: token }); 44 | }, [verifyUserMutate]); 45 | 46 | return { verifyUser }; 47 | }; 48 | 49 | export default useVerifyUser; 50 | -------------------------------------------------------------------------------- /client/src/auth/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as k from '@shared/styles/keyframes.css'; 3 | import * as m from '@shared/styles/media.css'; 4 | import * as t from '@shared/styles/token.css'; 5 | import * as u from '@shared/styles/utils.css'; 6 | import { style } from '@vanilla-extract/css'; 7 | import { rem } from 'polished'; 8 | 9 | export const wrapper = style([ 10 | u.positionRelative, 11 | u.fullWidth, 12 | u.fullHeight, 13 | u.flexCenter, 14 | u.flexColumn, 15 | { 16 | rowGap: rem(20), 17 | }, 18 | ]); 19 | 20 | export const buttonWrapper = style([ 21 | u.flexColumn, 22 | u.flexAlignCenter, 23 | u.positionRelative, 24 | { 25 | width: rem(350), 26 | height: rem(150), 27 | justifyContent: 'space-between', 28 | }, 29 | ]); 30 | 31 | export const titleWrapper = style([ 32 | u.positionRelative, 33 | u.fullWidth, 34 | u.flexCenter, 35 | u.flexColumn, 36 | { 37 | rowGap: rem(5), 38 | }, 39 | ]); 40 | 41 | export const title = style([ 42 | t.typography.heading2, 43 | { 44 | animation: `1.25s ${k.fadeInWithSinkDown}`, 45 | }, 46 | m.small({ 47 | fontSize: rem(40), 48 | }), 49 | ]); 50 | 51 | export const button = style([ 52 | u.fullWidth, 53 | u.borderNone, 54 | u.borderRadius2, 55 | u.cursorPointer, 56 | { 57 | height: rem(52), 58 | fontSize: rem(16), 59 | backgroundColor: vars.$scale.color.blue500, 60 | color: 'white', 61 | fontWeight: 'bold', 62 | transition: 'background 0.2s ease', 63 | 64 | ':hover': { 65 | backgroundColor: vars.$scale.color.blue300, 66 | }, 67 | }, 68 | m.small({ 69 | width: '90vw', 70 | }), 71 | ]); 72 | 73 | export const modalContent = style([ 74 | u.flexColumn, 75 | { 76 | justifyContent: 'center', 77 | alignItems: 'center', 78 | rowGap: rem(10), 79 | height: rem(100), 80 | fontSize: rem(18), 81 | }, 82 | ]); 83 | -------------------------------------------------------------------------------- /client/src/detail/api.ts: -------------------------------------------------------------------------------- 1 | export type CodeFullName = `${string}/${string}`; 2 | export type StoryFullName = `${string}-${string}`; 3 | export type PocketCode = `${CodeFullName}_${StoryFullName}`; 4 | 5 | export interface CodeData { 6 | code: string; 7 | uploadedChatChannel: string; 8 | uploadedChatTimeStamp: string; 9 | } 10 | 11 | export const getCodeUrl = `/code/id`; 12 | export const getStoryNamesUrl = `/story/names`; 13 | export const getStoryCodeUrl = `/story/code`; 14 | export const createStoryUrl = `/story`; 15 | -------------------------------------------------------------------------------- /client/src/detail/components/ErrorModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Modal } from '@shared/components'; 2 | import { ModalInterface } from '@shared/contexts/ModalContext'; 3 | 4 | import * as style from './style.css'; 5 | 6 | const ErrorModal = ({ closeModal }: ModalInterface) => { 7 | return ( 8 |
9 | 10 |
올바르지 않은 요청이 발생했어요(한번 더 확인해주세요)
11 | 12 |
13 | ); 14 | }; 15 | 16 | export default ErrorModal; 17 | -------------------------------------------------------------------------------- /client/src/detail/components/ErrorModal/style.css.ts: -------------------------------------------------------------------------------- 1 | import * as u from '@shared/styles/utils.css'; 2 | import { style } from '@vanilla-extract/css'; 3 | import { rem } from 'polished'; 4 | 5 | export const modalContent = style([ 6 | u.flexColumn, 7 | { 8 | justifyContent: 'center', 9 | alignItems: 'center', 10 | rowGap: rem(10), 11 | fontSize: rem(18), 12 | }, 13 | ]); 14 | -------------------------------------------------------------------------------- /client/src/detail/components/Sandpack/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as m from '@shared/styles/media.css'; 3 | import * as t from '@shared/styles/token.css'; 4 | import * as u from '@shared/styles/utils.css'; 5 | import { style } from '@vanilla-extract/css'; 6 | import { recipe } from '@vanilla-extract/recipes'; 7 | import { rem } from 'polished'; 8 | 9 | export const storyCreatingForm = style([ 10 | u.flex, 11 | t.mt18, 12 | { gap: rem(12), justifyContent: 'right' }, 13 | m.small({ 14 | flexDirection: 'column', 15 | }), 16 | ]); 17 | 18 | export const sandpackWrapper = style([ 19 | { 20 | height: rem(600), 21 | }, 22 | m.medium({ 23 | height: 'auto', 24 | }), 25 | ]); 26 | 27 | export const submitButton = recipe({ 28 | base: [ 29 | u.borderNone, 30 | u.borderRadius2, 31 | u.cursorPointer, 32 | { 33 | width: rem(150), 34 | height: rem(52), 35 | fontSize: rem(16), 36 | display: 'block', 37 | color: vars.$static.color.staticWhite, 38 | transition: 'background 0.2s ease', 39 | 40 | ':hover': { 41 | backgroundColor: vars.$scale.color.blue400, 42 | }, 43 | }, 44 | m.small({ 45 | width: '100%', 46 | }), 47 | ], 48 | variants: { 49 | enable: { 50 | true: { 51 | backgroundColor: vars.$scale.color.blue500, 52 | }, 53 | false: { 54 | cursor: 'not-allowed', 55 | backgroundColor: vars.$scale.color.blue300, 56 | }, 57 | }, 58 | }, 59 | }); 60 | 61 | export const storyNameInput = recipe({ 62 | base: [ 63 | u.border, 64 | u.borderRadius2, 65 | { 66 | outline: 'none', 67 | width: rem(200), 68 | height: rem(52), 69 | fontSize: rem(16), 70 | paddingLeft: rem(15), 71 | }, 72 | m.small({ 73 | width: '100%', 74 | }), 75 | ], 76 | variants: { 77 | enable: { 78 | true: { 79 | backgroundColor: vars.$static.color.staticWhite, 80 | }, 81 | false: { 82 | cursor: 'not-allowed', 83 | backgroundColor: vars.$scale.color.gray100, 84 | }, 85 | }, 86 | }, 87 | }); 88 | 89 | /* modal */ 90 | export const modalParagraph = style([ 91 | u.textAlignCenter, 92 | t.mb24, 93 | t.mt18, 94 | { 95 | fontSize: rem(20), 96 | }, 97 | ]); 98 | 99 | export const buttonWrapper = style([u.flex, { gap: rem(10) }]); 100 | -------------------------------------------------------------------------------- /client/src/detail/components/SandpackLoader/index.tsx: -------------------------------------------------------------------------------- 1 | // deprecated : 원하면 삭제해도 됌 2 | import { Icon } from '@shared/components'; 3 | 4 | import * as style from './style.css'; 5 | 6 | interface SandpackLoaderProps { 7 | resetErrorBoundary?: (...args: Array) => void; 8 | } 9 | 10 | const SandpackLoader: React.FC = (props) => ( 11 |
12 | {props.resetErrorBoundary ? ( 13 | <> 14 | 15 | 코드를 불러올 수 없어요! 16 | 17 | ) : ( 18 |
19 | )} 20 |
21 | ); 22 | 23 | export default SandpackLoader; 24 | -------------------------------------------------------------------------------- /client/src/detail/components/SandpackLoader/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as k from '@shared/styles/keyframes.css'; 3 | import * as t from '@shared/styles/token.css'; 4 | import * as u from '@shared/styles/utils.css'; 5 | import { style } from '@vanilla-extract/css'; 6 | import { rem } from 'polished'; 7 | 8 | export const skeletonContainer = style([ 9 | u.flexCenter, 10 | u.flexColumn, 11 | { 12 | width: rem(1400), 13 | height: rem(600), 14 | }, 15 | ]); 16 | 17 | export const loader = style({ 18 | border: '16px solid #f3f3f3', 19 | borderTop: `16px solid ${vars.$scale.color.blue700}`, 20 | borderRadius: '50%', 21 | width: '120px', 22 | height: '120px', 23 | animation: `${k.rotate} 1s ease-in-out infinite`, 24 | }); 25 | 26 | export const warning = style([t.mt12]); 27 | -------------------------------------------------------------------------------- /client/src/detail/components/StoryConfirmModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from '@shared/components'; 2 | import { ModalInterface } from '@shared/contexts/ModalContext'; 3 | 4 | import * as style from './style.css'; 5 | 6 | const StoryConfirmModal = ({ closeModal, onConfirm }: ModalInterface) => { 7 | const confirm = () => { 8 | if (onConfirm) onConfirm(); 9 | if (closeModal) closeModal(); 10 | }; 11 | return ( 12 |
13 |

정말로 스토리를 생성하시겠어요?

14 |
15 | 16 | {onConfirm && } 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default StoryConfirmModal; 23 | -------------------------------------------------------------------------------- /client/src/detail/components/StoryConfirmModal/style.css.ts: -------------------------------------------------------------------------------- 1 | import * as t from '@shared/styles/token.css'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { rem } from 'polished'; 5 | 6 | export const modalParagraph = style([ 7 | u.textAlignCenter, 8 | t.mb24, 9 | t.mt18, 10 | { 11 | fontSize: rem(20), 12 | }, 13 | ]); 14 | 15 | export const buttonWrapper = style([u.flex, { gap: rem(10) }]); 16 | -------------------------------------------------------------------------------- /client/src/detail/hooks/useCreateStory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateStoryRequest, 3 | CreateStoryResponse, 4 | createStoryResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { localStorage } from '@shared/utils/localStorage'; 8 | import { useQueryClient } from '@tanstack/react-query'; 9 | 10 | import { createStoryUrl, getStoryNamesUrl } from '../api'; 11 | 12 | type CreateStoryBodyType = CreateStoryRequest['body']; 13 | interface UseCreateStory { 14 | codeName?: string; 15 | codeAuthor?: string; 16 | codeId: string; 17 | selectStory: (name: string) => void; 18 | onError: () => void; 19 | } 20 | 21 | const useCreateStory = ({ codeId, selectStory, onError }: UseCreateStory) => { 22 | const queryClient = useQueryClient(); 23 | const { mutate: createStoryMutate, error } = useCustomMutation< 24 | CreateStoryResponse, 25 | CreateStoryResponse, 26 | CreateStoryBodyType 27 | >({ 28 | url: createStoryUrl, 29 | method: 'POST', 30 | validator: createStoryResponseValidate, 31 | options: { 32 | onSuccess: async (res) => { 33 | await queryClient.invalidateQueries([getStoryNamesUrl]); 34 | selectStory(res.storyId); 35 | }, 36 | onError, 37 | }, 38 | }); 39 | 40 | const createStory = ({ codes, storyName }: Pick) => { 41 | const pocketToken = localStorage.getUserToken(); 42 | if (!pocketToken || !storyName) return; 43 | 44 | createStoryMutate({ codes, storyName, codeId, pocketToken }); 45 | }; 46 | 47 | return { createStory, error }; 48 | }; 49 | 50 | export default useCreateStory; 51 | -------------------------------------------------------------------------------- /client/src/detail/hooks/useDeleteStory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeleteStoryRequest, 3 | DeleteStoryResponse, 4 | deleteStoryResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { useQueryClient } from '@tanstack/react-query'; 8 | 9 | import { getStoryNamesUrl } from '../api'; 10 | 11 | type DeleteStoryRequestBody = DeleteStoryRequest['body']; 12 | 13 | interface UseDeleteStoryParams { 14 | onSuccessDelete: () => void; 15 | } 16 | 17 | const useDeleteStory = ({ onSuccessDelete }: UseDeleteStoryParams) => { 18 | const queryClient = useQueryClient(); 19 | const { mutate: deleteStoryMutate } = useCustomMutation< 20 | DeleteStoryResponse, 21 | { message: string }, 22 | DeleteStoryRequestBody 23 | >({ 24 | url: '/story', 25 | method: 'DELETE', 26 | validator: deleteStoryResponseValidate, 27 | options: { 28 | onSuccess: async () => { 29 | await queryClient.invalidateQueries([getStoryNamesUrl]); 30 | onSuccessDelete(); 31 | }, 32 | }, 33 | }); 34 | return { deleteStory: deleteStoryMutate }; 35 | }; 36 | 37 | export default useDeleteStory; 38 | -------------------------------------------------------------------------------- /client/src/detail/hooks/useStory.ts: -------------------------------------------------------------------------------- 1 | import { GetStoryCodeResponse, getStoryCodeResponseValidate } from '@codepocket/schema'; 2 | import useCustomQuery from '@shared/hooks/useCustomQuery'; 3 | import { useCallback, useEffect, useState } from 'react'; 4 | 5 | import { getStoryCodeUrl } from '../api'; 6 | import { SelectedStory } from '../components/Sandpack'; 7 | 8 | interface UseStoryParams { 9 | onError: () => void; 10 | } 11 | 12 | const useStory = ({ onError }: UseStoryParams) => { 13 | const [selectedStoryId, setSelectedStoryId] = useState(''); 14 | const [selectedStoryCodes, setSelectedStoryCodes] = useState(); 15 | const { refetch: getStory, error } = useCustomQuery({ 16 | url: getStoryCodeUrl, 17 | validator: getStoryCodeResponseValidate, 18 | params: { 19 | storyId: selectedStoryId, 20 | }, 21 | options: { 22 | enabled: false, 23 | onError, 24 | }, 25 | }); 26 | 27 | const selectStory = async (id: string) => { 28 | if (selectedStoryId === id) return setSelectedStoryId(''); 29 | return setSelectedStoryId(id); 30 | }; 31 | 32 | const getStoryCode = useCallback(async () => { 33 | if (!selectedStoryId) return setSelectedStoryCodes(undefined); 34 | 35 | const { data } = await getStory(); 36 | const newSelectedStory = { codes: data?.codes || {} }; 37 | return setSelectedStoryCodes(newSelectedStory); 38 | }, [getStory, selectedStoryId]); 39 | 40 | useEffect(() => { 41 | getStoryCode(); 42 | }, [selectedStoryId]); 43 | 44 | return { selectedStoryCodes, error, selectedStoryId, selectStory }; 45 | }; 46 | export default useStory; 47 | -------------------------------------------------------------------------------- /client/src/detail/hooks/useStoryNames.ts: -------------------------------------------------------------------------------- 1 | import { GetStoryNamesResponse, getStoryNamesResponseValidate } from '@codepocket/schema'; 2 | import useCustomQuery from '@shared/hooks/useCustomQuery'; 3 | 4 | import { getStoryNamesUrl } from '../api'; 5 | 6 | interface UseStoryNames { 7 | codeId: string; 8 | onError: () => void; 9 | } 10 | 11 | const useStoryNames = ({ codeId, onError }: UseStoryNames) => 12 | useCustomQuery({ 13 | url: getStoryNamesUrl, 14 | validator: getStoryNamesResponseValidate, 15 | params: { codeId }, 16 | options: { onError }, 17 | }); 18 | 19 | export default useStoryNames; 20 | -------------------------------------------------------------------------------- /client/src/detail/hooks/useUpdateStory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UpdateStoryRequest, 3 | UpdateStoryResponse, 4 | updateStoryResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { useQueryClient } from '@tanstack/react-query'; 8 | 9 | import { getStoryNamesUrl } from '../api'; 10 | 11 | type UpdateStoryRequestBody = UpdateStoryRequest['body']; 12 | 13 | const useUpdateStory = () => { 14 | const queryClient = useQueryClient(); 15 | const { mutate: updateStoryMutate } = useCustomMutation< 16 | UpdateStoryResponse, 17 | { message: string }, 18 | UpdateStoryRequestBody 19 | >({ 20 | url: '/story', 21 | method: 'PUT', 22 | validator: updateStoryResponseValidate, 23 | options: { 24 | onSuccess: async () => { 25 | await queryClient.invalidateQueries([getStoryNamesUrl]); 26 | }, 27 | }, 28 | }); 29 | return { updateStory: updateStoryMutate }; 30 | }; 31 | 32 | export default useUpdateStory; 33 | -------------------------------------------------------------------------------- /client/src/detail/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as m from '@shared/styles/media.css'; 3 | import * as t from '@shared/styles/token.css'; 4 | import * as u from '@shared/styles/utils.css'; 5 | import { style } from '@vanilla-extract/css'; 6 | import { rem } from 'polished'; 7 | 8 | export const wrapper = style([u.flexColumn, u.flexAlignCenter, u.fullWidth, u.fullHeight]); 9 | 10 | export const codeBlock = style([ 11 | t.mt50, 12 | { 13 | width: rem(1400), 14 | }, 15 | m.large({ 16 | width: '100%', 17 | padding: '0 1rem', 18 | }), 19 | ]); 20 | 21 | export const header = style([ 22 | u.flex, 23 | u.fullWidth, 24 | { justifyContent: 'space-between', alignItems: 'center' }, 25 | ]); 26 | 27 | export const headerIcon = style([ 28 | u.cursorPointer, 29 | { 30 | width: rem(50), 31 | }, 32 | m.medium({ 33 | width: rem(30), 34 | }), 35 | ]); 36 | 37 | export const title = style([ 38 | { 39 | fontSize: rem(48), 40 | fontWeight: 'bold', 41 | }, 42 | m.medium({ 43 | fontSize: rem(30), 44 | }), 45 | ]); 46 | 47 | export const highlight = style({ 48 | color: vars.$scale.color.blue800, 49 | fontWeight: 'normal', 50 | }); 51 | 52 | export const article = style([u.fullWidth, t.mt18, { zIndex: -1 }]); 53 | -------------------------------------------------------------------------------- /client/src/detail/utils/filterObj.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | export const filterObjWithKey = (obj: T, keys: (keyof T)[]): Omit => 3 | Object.keys(obj) 4 | .filter((key) => keys.includes(key as keyof T)) 5 | .reduce((cur, key) => { 6 | cur[key as keyof Omit] = obj[key as keyof Omit]; 7 | return cur; 8 | }, {} as Omit); 9 | 10 | export const filterObjValueWithKey = (obj: T, key: string) => 11 | Object.entries(obj) 12 | .map(([k, v]) => [k, v[key]]) 13 | .reduce((acc, [k, v]) => { 14 | acc[k] = v; 15 | return acc; 16 | }, {}); 17 | 18 | export const createObjWithCertainValue = (keys: string[], value: T) => 19 | keys.reduce((acc, cur) => { 20 | acc[cur] = value; 21 | return acc; 22 | }, {} as { [key in string]: T }); 23 | -------------------------------------------------------------------------------- /client/src/detail/utils/getAllCodesFromSandpack.ts: -------------------------------------------------------------------------------- 1 | import { filterObjValueWithKey, filterObjWithKey } from './filterObj'; 2 | 3 | interface GetAllCodesFromSandpack { 4 | files: any; 5 | codeName: string; 6 | } 7 | 8 | const getAllCodesFromSandpack = ({ files, codeName }: GetAllCodesFromSandpack) => { 9 | const ROOT_FILE = '/App.tsx'; 10 | const SANDPACK_FILE_CODE = 'code'; 11 | const storyNames = [ROOT_FILE, `/${codeName}`]; 12 | const codes = filterObjValueWithKey(filterObjWithKey(files, storyNames), SANDPACK_FILE_CODE); 13 | return codes; 14 | }; 15 | 16 | export default getAllCodesFromSandpack; 17 | -------------------------------------------------------------------------------- /client/src/detail/utils/parse.ts: -------------------------------------------------------------------------------- 1 | const dependencyRegex = 2 | /import([ \n\t]*(?:[^ \n\t]+[ \n\t]*,?)?(?:[ \n\t]*(?:[ \n\t]*[^ \n\t"']+[ \n\t]*,?)+)?[ \n\t]*)from[ \n\t]*(['"])([^'"\n]+)(?:['"])/g; 3 | export const getDependenciesFromText = (text: string) => 4 | [...(text.match(dependencyRegex) || [])].map((dependency) => dependency.split("'")[1]); 5 | 6 | export const removeExtension = (filename: string) => filename.split('.')[0]; 7 | 8 | export const getExtension = (filename: string) => filename.split('.').pop() || ''; 9 | 10 | export const convertFirstToUpperCase = (str: string) => 11 | (str.slice(0, 1).toUpperCase() + str.slice(1)) as T; 12 | 13 | export const checkWordInArray = (word: string, words: string[]) => words.includes(word); 14 | -------------------------------------------------------------------------------- /client/src/detail/utils/textGenerator.ts: -------------------------------------------------------------------------------- 1 | import { convertFirstToUpperCase, getExtension, removeExtension } from './parse'; 2 | 3 | export const genrateBaseCode = (name: string) => { 4 | const reactExtension = ['jsx', 'tsx']; 5 | const isComponent = reactExtension.includes(getExtension(name)); 6 | const importedName = isComponent 7 | ? convertFirstToUpperCase(removeExtension(name)) 8 | : removeExtension(name); 9 | const renderedJSX = isComponent 10 | ? `<${importedName} />` 11 | : `

콘솔창을 보려면 F12를 누르세요

`; 12 | 13 | return `import ${importedName} from './${removeExtension(name)}' 14 | 15 | export default function App(): JSX.Element { 16 | return ${renderedJSX} 17 | } 18 | `; 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | readonly VITE_BASE_SERVER_URL: string; 3 | readonly VITE_AUTH0_DOMAIN: string; 4 | readonly VITE_AUTH0_CLIENT_ID: string; 5 | readonly VITE_EMAIL_DOMAIN_NAME: string; 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './shared/styles/global.css'; 2 | import '@seed-design/stylesheet/global.css'; 3 | 4 | import { Auth0Provider } from '@auth0/auth0-react'; 5 | import { AUTH0_CLIENT_ID, AUTH0_DOMAIN } from '@shared/constant'; 6 | import { ModalProvider } from '@shared/contexts/ModalContext'; 7 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 8 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 9 | import React from 'react'; 10 | import ReactDOM from 'react-dom/client'; 11 | import { BrowserRouter } from 'react-router-dom'; 12 | 13 | import App from './App'; 14 | 15 | const queryClient = new QueryClient(); 16 | const isDev = process.env.NODE_ENV === 'development'; 17 | 18 | ReactDOM.createRoot(document.getElementById('root')!).render( 19 | 20 | 21 | 22 | 23 | 24 | 25 | {isDev && } 26 | 27 | 28 | 29 | 30 | , 31 | ); 32 | -------------------------------------------------------------------------------- /client/src/pocket/Pocket.tsx: -------------------------------------------------------------------------------- 1 | import { CodeList, MoreButton, Searchbar, SearchHelpText } from './components'; 2 | import CodeblockSkeleton from './components/CodeBlockSkeleton'; 3 | import ErrorMessage from './components/ErrorMessage'; 4 | import FloatingActionButton from './components/FloatingActionButton'; 5 | import useCodes from './hooks/useCodes'; 6 | import * as style from './style.css'; 7 | 8 | const Skeleton = () => ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | const PocketPage: React.FC = () => { 18 | const { codes, error, isLast, searchText, isLoading, changeSearchText, getNextCodes } = 19 | useCodes(); 20 | 21 | return ( 22 | <> 23 | 24 |
25 |

Codepocket

26 | 27 | {searchText ? : null} 28 | 29 | {isLoading && } 30 | {!isLoading && 31 | (!error ? ( 32 | 39 | ) : ( 40 | 41 | ))} 42 |
43 | 44 | ); 45 | }; 46 | 47 | export default PocketPage; 48 | -------------------------------------------------------------------------------- /client/src/pocket/api.ts: -------------------------------------------------------------------------------- 1 | export const getCodesUrl = '/codes'; 2 | 3 | export const deleteCodeByIdUrl = '/code/delete/id'; 4 | -------------------------------------------------------------------------------- /client/src/pocket/components/CodeBlockSkeleton/index.tsx: -------------------------------------------------------------------------------- 1 | import * as style from './style.css'; 2 | 3 | const CodeblockSkeleton = () => { 4 | return ( 5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default CodeblockSkeleton; 28 | -------------------------------------------------------------------------------- /client/src/pocket/components/CodeList/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetCodesResponse } from '@codepocket/schema'; 2 | 3 | import Codeblock from '../Codeblock'; 4 | import * as style from './style.css'; 5 | 6 | const CodeList: React.FC<{ codes: GetCodesResponse['codes'] }> = ({ codes }) => { 7 | return ( 8 |
    9 | {codes.map(({ userId, codeId, code, codeAuthor, codeName, isAnonymous }) => ( 10 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default CodeList; 25 | -------------------------------------------------------------------------------- /client/src/pocket/components/CodeList/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { rem } from 'polished'; 5 | 6 | export const codeList = style([u.fullWidth]); 7 | 8 | export const codeItem = style([ 9 | u.fullWidth, 10 | u.borderRadius2, 11 | { maxHeight: rem(250), marginTop: rem(70), overflow: 'hidden' }, 12 | ]); 13 | 14 | export const lastCodeItemInformation = style([ 15 | u.flex, 16 | u.flexColumn, 17 | u.flexCenter, 18 | { 19 | rowGap: rem(10), 20 | color: vars.$scale.color.gray700, 21 | }, 22 | ]); 23 | -------------------------------------------------------------------------------- /client/src/pocket/components/DeleteModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from '@shared/components'; 2 | import { ModalInterface } from '@shared/contexts/ModalContext'; 3 | import { localStorage } from '@shared/utils/localStorage'; 4 | import { useCallback, useMemo } from 'react'; 5 | 6 | import useDeleteCode from '../../hooks/useDeleteCode'; 7 | import * as style from './style.css'; 8 | 9 | const DeleteModal = ({ closeModal, targetId }: ModalInterface) => { 10 | const { deleteCode: deleteCodeMutation } = useDeleteCode(); 11 | 12 | const pocketToken = useMemo(() => localStorage.getUserToken() || '', []); 13 | const codeId = useMemo(() => targetId || '', [targetId]); 14 | 15 | const deleteCode = useCallback(() => { 16 | if (!closeModal) return; 17 | deleteCodeMutation({ codeId, pocketToken }); 18 | closeModal(); 19 | }, [closeModal, codeId, deleteCodeMutation, pocketToken]); 20 | 21 | return ( 22 |
23 |

정말로 삭제하시겠어요?

24 |
25 | 26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default DeleteModal; 33 | -------------------------------------------------------------------------------- /client/src/pocket/components/DeleteModal/style.css.ts: -------------------------------------------------------------------------------- 1 | import * as u from '@shared/styles/utils.css'; 2 | import { style } from '@vanilla-extract/css'; 3 | import { rem } from 'polished'; 4 | 5 | export const deleteModalContainer = style([u.flexColumn, { width: rem(400), rowGap: rem(10) }]); 6 | 7 | export const deleteModalButtonContainer = style([u.flex, { columnGap: rem(10) }]); 8 | 9 | export const deleteModalHeaderText = style({ fontSize: rem(20) }); 10 | -------------------------------------------------------------------------------- /client/src/pocket/components/ErrorMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@shared/components'; 2 | 3 | import * as style from './style.css'; 4 | 5 | interface ErrorMessageProps { 6 | message: string; 7 | } 8 | 9 | const ErrorMessage = ({ message }: ErrorMessageProps) => { 10 | return ( 11 |
12 | 13 | {message} 14 |
15 | ); 16 | }; 17 | 18 | export default ErrorMessage; 19 | -------------------------------------------------------------------------------- /client/src/pocket/components/ErrorMessage/style.css.ts: -------------------------------------------------------------------------------- 1 | import * as u from '@shared/styles/utils.css'; 2 | import { style } from '@vanilla-extract/css'; 3 | import { rem } from 'polished'; 4 | 5 | export const alertBase = style([ 6 | u.fullWidth, 7 | u.flexColumn, 8 | u.flexCenter, 9 | { 10 | fontWeight: 'bold', 11 | fontSize: rem(18), 12 | margin: rem(30), 13 | padding: rem(10), 14 | gap: rem(15), 15 | }, 16 | ]); 17 | -------------------------------------------------------------------------------- /client/src/pocket/components/FloatingActionButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { modals } from '@shared/contexts/GlobalModal'; 2 | import { useModalDispatch } from '@shared/contexts/ModalContext'; 3 | import Transition from '@shared/utils/Transition'; 4 | import { useState } from 'react'; 5 | 6 | import useScrollDirection from '../../hooks/useScrollDirection'; 7 | import * as style from './style.css'; 8 | 9 | interface FloatingButtonProps {} 10 | 11 | const FloatingActionButton: React.FC = () => { 12 | const [selected, setSelected] = useState(false); 13 | const modalDispatch = useModalDispatch(); 14 | const { scrollDir } = useScrollDirection(); 15 | 16 | const toggleButton = () => setSelected((prev) => !prev); 17 | 18 | const onClickButton = () => { 19 | toggleButton(); 20 | if (!selected) 21 | modalDispatch({ 22 | type: 'OPEN_MODAL', 23 | Component: modals.createModal, 24 | closeModal: toggleButton, 25 | }); 26 | }; 27 | 28 | return ( 29 | 30 | {() => ( 31 |
32 | 35 |
36 | )} 37 |
38 | ); 39 | }; 40 | 41 | export default FloatingActionButton; 42 | -------------------------------------------------------------------------------- /client/src/pocket/components/FloatingActionButton/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as k from '@shared/styles/keyframes.css'; 3 | import * as m from '@shared/styles/media.css'; 4 | import * as u from '@shared/styles/utils.css'; 5 | import { recipe } from '@vanilla-extract/recipes'; 6 | import { rem } from 'polished'; 7 | 8 | const ANIMATION_DURATION_SECOND = 0.3; 9 | 10 | export const floatingButton = recipe({ 11 | base: [ 12 | u.cursorPointer, 13 | u.fullHeight, 14 | u.fullWidth, 15 | { 16 | border: 'none', 17 | borderRadius: '50%', 18 | fontSize: 'inherit', 19 | color: vars.$static.color.staticWhite, 20 | backgroundColor: vars.$scale.color.blue500, 21 | transition: `all ${ANIMATION_DURATION_SECOND}s`, 22 | 23 | ':hover': { 24 | backgroundColor: vars.$scale.color.blue400, 25 | }, 26 | }, 27 | ], 28 | variants: { 29 | selected: { 30 | true: { 31 | transform: 'rotate(315deg)', 32 | ':active': { 33 | transform: 'rotate(345deg)', 34 | }, 35 | }, 36 | false: { 37 | transform: 'rotate(0deg)', 38 | ':active': { 39 | transform: 'rotate(-30deg)', 40 | }, 41 | }, 42 | }, 43 | }, 44 | }); 45 | 46 | export const wrapper = recipe({ 47 | base: [ 48 | u.positionFixed, 49 | u.flexCenter, 50 | { 51 | zIndex: 1, 52 | width: rem(70), 53 | height: rem(70), 54 | right: rem(50), 55 | bottom: rem(50), 56 | fontSize: rem(50), 57 | borderRadius: '50%', 58 | transition: `all ${ANIMATION_DURATION_SECOND}s linear`, 59 | boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', 60 | }, 61 | m.medium({ 62 | width: rem(55), 63 | height: rem(55), 64 | right: rem(20), 65 | bottom: rem(20), 66 | fontSize: rem(30), 67 | }), 68 | ], 69 | variants: { 70 | useOnMode: { 71 | true: { animation: `${ANIMATION_DURATION_SECOND}s ${k.scaleUp}` }, 72 | false: { animation: `${ANIMATION_DURATION_SECOND}s ${k.scaleDown}` }, 73 | }, 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /client/src/pocket/components/Modal/CreateModal.tsx: -------------------------------------------------------------------------------- 1 | import type { ModalInterface } from '@shared/contexts/ModalContext'; 2 | import { localStorage } from '@shared/utils/localStorage'; 3 | 4 | import useCreateCode from '../../hooks/useCreateCode'; 5 | import ModalContentTemplate, { OnConfirmModal } from './Template'; 6 | 7 | const CreateModal = ({ closeModal }: ModalInterface) => { 8 | const { createCode } = useCreateCode(); 9 | 10 | const onConfirm = ({ code, codeName, isAnonymous }: OnConfirmModal) => { 11 | if (!closeModal) return; 12 | createCode({ 13 | code, 14 | codeName, 15 | isAnonymous, 16 | pocketToken: localStorage.getUserToken() || '', 17 | }); 18 | closeModal(); 19 | }; 20 | 21 | return ; 22 | }; 23 | 24 | export default CreateModal; 25 | -------------------------------------------------------------------------------- /client/src/pocket/components/Modal/EditModal.tsx: -------------------------------------------------------------------------------- 1 | import type { ModalInterface } from '@shared/contexts/ModalContext'; 2 | import useCode from '@shared/hooks/useCode'; 3 | import { localStorage } from '@shared/utils/localStorage'; 4 | 5 | import useUpdateCode from '../../hooks/useUpdateCode'; 6 | import ModalContentTemplate, { OnConfirmModal } from './Template'; 7 | 8 | const CreateModal = ({ closeModal, targetId }: ModalInterface) => { 9 | const { data: codeInfo } = useCode({ codeId: targetId }); 10 | const { updateCode } = useUpdateCode({ codeId: targetId }); 11 | 12 | const onConfirm = ({ code, codeName, isAnonymous }: OnConfirmModal) => { 13 | if (!closeModal) return; 14 | updateCode({ 15 | code, 16 | codeName, 17 | isAnonymous, 18 | codeId: targetId || '', 19 | pocketToken: localStorage.getUserToken() || '', 20 | }); 21 | closeModal(); 22 | }; 23 | 24 | return ( 25 | 33 | ); 34 | }; 35 | 36 | export default CreateModal; 37 | -------------------------------------------------------------------------------- /client/src/pocket/components/MoreButton/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import * as style from './style.css'; 3 | 4 | interface MoreButtonInterface { 5 | onClick: () => void; 6 | isSearch: boolean; 7 | isLast: boolean | undefined; 8 | hasCode: boolean; 9 | isLoading: boolean; 10 | } 11 | 12 | const MoreButton: React.FC = ({ 13 | onClick, 14 | isSearch, 15 | isLast, 16 | hasCode, 17 | isLoading, 18 | }) => { 19 | const getMessage = () => { 20 | if (!isLast) return 'MORE'; 21 | if (!hasCode && isSearch) return '검색 결과에 해당하는 코드가 없어요'; 22 | if (!hasCode && !isSearch) return '빈 레지스트리예요'; 23 | return '마지막 코드예요'; 24 | }; 25 | 26 | return ( 27 | 34 | ); 35 | }; 36 | 37 | export default MoreButton; 38 | -------------------------------------------------------------------------------- /client/src/pocket/components/MoreButton/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { rem } from 'polished'; 5 | 6 | export const buttonBase = style([ 7 | u.fullWidth, 8 | u.borderNone, 9 | u.borderRadius2, 10 | { 11 | transition: 'all 0.3s ease', 12 | fontWeight: 'bold', 13 | fontSize: rem(18), 14 | height: rem(70), 15 | margin: rem(30), 16 | padding: rem(10), 17 | border: `1px solid ${vars.$static.color.staticWhite}`, 18 | }, 19 | ]); 20 | 21 | export const moreButton = style([ 22 | buttonBase, 23 | u.cursorPointer, 24 | { 25 | backgroundColor: vars.$scale.color.blue100, 26 | color: vars.$scale.color.blue700, 27 | 28 | ':hover': { 29 | border: `1px solid ${vars.$scale.color.blue600}`, 30 | backgroundColor: vars.$scale.color.blue200, 31 | }, 32 | }, 33 | ]); 34 | 35 | export const disableMoreButton = style([buttonBase, u.cursorNotAllowed]); 36 | -------------------------------------------------------------------------------- /client/src/pocket/components/PendingFallback/index.tsx: -------------------------------------------------------------------------------- 1 | import * as style from './style.css'; 2 | 3 | const PendingFallback = () => { 4 | return
; 5 | }; 6 | 7 | export default PendingFallback; 8 | -------------------------------------------------------------------------------- /client/src/pocket/components/PendingFallback/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as k from '@shared/styles/keyframes.css'; 3 | import * as u from '@shared/styles/utils.css'; 4 | import { style } from '@vanilla-extract/css'; 5 | import { rem } from 'polished'; 6 | 7 | export const pendingFallback = style([ 8 | u.fullWidth, 9 | u.flexCenter, 10 | u.borderRadius2, 11 | { 12 | height: rem(100), 13 | marginTop: rem(20), 14 | backgroundColor: vars.$scale.color.gray100, 15 | animation: `2s infinite ${k.gradation}`, 16 | }, 17 | ]); 18 | -------------------------------------------------------------------------------- /client/src/pocket/components/SearchHelpText/index.tsx: -------------------------------------------------------------------------------- 1 | import * as style from './style.css'; 2 | 3 | interface SearchHelpTextInterface { 4 | searchText: string; 5 | } 6 | 7 | const SearchHelpText: React.FC = ({ searchText }) => { 8 | return ( 9 |
10 |

11 | {searchText} 12 |

13 |
14 | ); 15 | }; 16 | 17 | export default SearchHelpText; 18 | -------------------------------------------------------------------------------- /client/src/pocket/components/SearchHelpText/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { rem } from 'polished'; 5 | 6 | export const searchHelpTextBox = style([ 7 | u.fullWidth, 8 | { 9 | margin: rem(10), 10 | fontSize: rem(14), 11 | }, 12 | ]); 13 | 14 | export const searchText = style([ 15 | u.borderRadius2, 16 | { 17 | padding: rem(4), 18 | backgroundColor: vars.$scale.color.blue100, 19 | fontWeight: 'bold', 20 | }, 21 | ]); 22 | -------------------------------------------------------------------------------- /client/src/pocket/components/Searchbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@shared/components'; 2 | import debounce from 'lodash/debounce'; 3 | import { useCallback, useEffect, useState } from 'react'; 4 | 5 | import useScrollPosition from '../../hooks/useScrollPosition'; 6 | import * as style from './style.css'; 7 | 8 | interface SearchbarInterface { 9 | searchText?: string; 10 | changeSearchText: (text: string) => void; 11 | } 12 | 13 | const Searchbar: React.FC = ({ changeSearchText }) => { 14 | const [text, setText] = useState(''); 15 | const { isScrollTop } = useScrollPosition(); 16 | 17 | const onChangeSearchbar = (event: React.ChangeEvent) => { 18 | setText(event.target.value); 19 | }; 20 | 21 | const debouncedChangeSearchText = useCallback( 22 | debounce((text: string) => changeSearchText(text), 500), 23 | [], 24 | ); 25 | 26 | useEffect(() => { 27 | debouncedChangeSearchText(text); 28 | }, [debouncedChangeSearchText, text]); 29 | 30 | return ( 31 |
36 | 46 | 47 | 48 | 49 |
50 | ); 51 | }; 52 | 53 | export default Searchbar; 54 | -------------------------------------------------------------------------------- /client/src/pocket/components/Searchbar/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { recipe } from '@vanilla-extract/recipes'; 5 | import { rem } from 'polished'; 6 | 7 | export const container = recipe({ 8 | base: [ 9 | u.fullWidth, 10 | u.top0, 11 | u.borderRadius2, 12 | { 13 | position: 'sticky', 14 | marginTop: rem(30), 15 | zIndex: 10, 16 | transition: 'transform 0.5s ease, height 0.2s ease', 17 | }, 18 | ], 19 | variants: { 20 | isScrollTop: { 21 | true: { 22 | transform: 'translateY(0)', 23 | height: rem(40), 24 | }, 25 | false: { 26 | transform: `translateY(${rem(8)})`, 27 | height: rem(45), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | export const searchbox = recipe({ 34 | base: [ 35 | u.fullWidth, 36 | u.fullHeight, 37 | u.borderRadius2, 38 | u.borderNone, 39 | { 40 | padding: rem(10), 41 | paddingRight: rem(40), 42 | backgroundColor: vars.$scale.color.gray100, 43 | 44 | ':focus': { 45 | outline: 'none', 46 | border: `2px solid ${vars.$scale.color.blue500}`, 47 | }, 48 | }, 49 | ], 50 | variants: { 51 | isScrollTop: { 52 | false: { 53 | border: `2px solid ${vars.$scale.color.blue500}`, 54 | }, 55 | }, 56 | }, 57 | }); 58 | 59 | export const searchicon = style([ 60 | u.right0, 61 | { 62 | width: rem(20), 63 | margin: rem(10), 64 | position: 'absolute', 65 | }, 66 | ]); 67 | -------------------------------------------------------------------------------- /client/src/pocket/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Codeblock } from './Codeblock'; 2 | export { default as CodeList } from './CodeList'; 3 | export { default as DeleteModal } from './DeleteModal'; 4 | export { default as CreateModal } from './Modal/CreateModal'; 5 | export { default as EditModal } from './Modal/EditModal'; 6 | export { default as MoreButton } from './MoreButton'; 7 | export { default as PendingFallback } from './PendingFallback'; 8 | export { default as Searchbar } from './Searchbar'; 9 | export { default as SearchHelpText } from './SearchHelpText'; 10 | -------------------------------------------------------------------------------- /client/src/pocket/hooks/useCodes.ts: -------------------------------------------------------------------------------- 1 | import { GetCodesResponse } from '@codepocket/schema'; 2 | import useCustomInfiniteQuery from '@shared/hooks/useCustomInfiniteQuery'; 3 | import { useCallback, useMemo, useState } from 'react'; 4 | 5 | import { getCodesUrl } from '../api'; 6 | 7 | const LIMIT = 5; 8 | const useCodes = () => { 9 | const [searchText, setSearchText] = useState(''); 10 | 11 | const { data, error, isLoading, fetchNextPage } = useCustomInfiniteQuery({ 12 | url: getCodesUrl, 13 | params: { 14 | search: searchText, 15 | limit: LIMIT, 16 | }, 17 | }); 18 | 19 | const isLast = useMemo(() => !!data?.pages.at(-1)?.data.isLast, [data]); 20 | const codes = useMemo( 21 | () => 22 | data?.pages.reduce( 23 | (acc, page) => [...acc, ...page.data.codes], 24 | [], 25 | ) || [], 26 | [data], 27 | ); 28 | 29 | const getNextCodes = useCallback(() => { 30 | fetchNextPage(); 31 | }, [fetchNextPage]); 32 | 33 | const changeSearchText = useCallback((text: string) => { 34 | setSearchText(text); 35 | }, []); 36 | 37 | return { 38 | codes, 39 | error, 40 | isLast, 41 | searchText, 42 | isLoading, 43 | changeSearchText, 44 | getNextCodes, 45 | }; 46 | }; 47 | export default useCodes; 48 | -------------------------------------------------------------------------------- /client/src/pocket/hooks/useCreateCode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateCodeRequest, 3 | CreateCodeResponse, 4 | createCodeResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { useQueryClient } from '@tanstack/react-query'; 8 | 9 | import { getCodesUrl } from '../api'; 10 | 11 | type CreateCodeBodyType = CreateCodeRequest['body']; 12 | 13 | const useCreateCode = () => { 14 | const queryClient = useQueryClient(); 15 | const { mutate: createCodeMutate } = useCustomMutation< 16 | CreateCodeResponse, 17 | { message: string }, 18 | CreateCodeBodyType 19 | >({ 20 | url: '/code/create', 21 | method: 'POST', 22 | validator: createCodeResponseValidate, 23 | options: { 24 | onSuccess: async () => { 25 | await queryClient.invalidateQueries([getCodesUrl]); 26 | }, 27 | }, 28 | }); 29 | 30 | return { createCode: createCodeMutate }; 31 | }; 32 | 33 | export default useCreateCode; 34 | -------------------------------------------------------------------------------- /client/src/pocket/hooks/useDeleteCode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeleteCodeByIdRequest, 3 | DeleteCodeByIdResponse, 4 | deleteCodeByIdResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { useQueryClient } from '@tanstack/react-query'; 8 | 9 | import { deleteCodeByIdUrl, getCodesUrl } from '../api'; 10 | 11 | type DeleteCodeByIdBodyType = DeleteCodeByIdRequest['body']; 12 | 13 | const useDeleteCode = () => { 14 | const queryClient = useQueryClient(); 15 | const { mutate: deleteCodeMutate } = useCustomMutation< 16 | DeleteCodeByIdResponse, 17 | { message: string }, 18 | DeleteCodeByIdBodyType 19 | >({ 20 | url: deleteCodeByIdUrl, 21 | method: 'POST', 22 | validator: deleteCodeByIdResponseValidate, 23 | options: { 24 | onSuccess: async () => { 25 | await queryClient.invalidateQueries([getCodesUrl]); 26 | }, 27 | }, 28 | }); 29 | 30 | return { deleteCode: deleteCodeMutate }; 31 | }; 32 | 33 | export default useDeleteCode; 34 | -------------------------------------------------------------------------------- /client/src/pocket/hooks/useScrollDirection.ts: -------------------------------------------------------------------------------- 1 | import throttle from 'lodash/throttle'; 2 | import { useCallback, useEffect, useRef, useState } from 'react'; 3 | 4 | type ScrollDirection = 'up' | 'down'; 5 | interface UseScrollDirection { 6 | threshold: number; 7 | } 8 | 9 | const useScrollDirection = (props: UseScrollDirection = { threshold: 10 }) => { 10 | const [scrollDir, setScrollDir] = useState('up'); 11 | const prevScrollY = useRef(0); 12 | 13 | const updateScrollDir = useCallback(() => { 14 | const currentScrollY = window.pageYOffset; 15 | if (Math.abs(currentScrollY - prevScrollY.current) < props.threshold) return; 16 | 17 | setScrollDir(currentScrollY > prevScrollY.current ? 'down' : 'up'); 18 | prevScrollY.current = currentScrollY; 19 | }, [props.threshold]); 20 | 21 | const onScroll = throttle(() => window.requestAnimationFrame(updateScrollDir), 100); 22 | 23 | useEffect(() => { 24 | prevScrollY.current = window.pageYOffset; 25 | 26 | window.addEventListener('scroll', onScroll); 27 | return () => window.removeEventListener('scroll', onScroll); 28 | }, [onScroll]); 29 | 30 | return { scrollDir }; 31 | }; 32 | 33 | export default useScrollDirection; 34 | -------------------------------------------------------------------------------- /client/src/pocket/hooks/useScrollPosition.ts: -------------------------------------------------------------------------------- 1 | import throttle from 'lodash/throttle'; 2 | import { useEffect, useMemo, useState } from 'react'; 3 | 4 | const THROTTLE_WAIT_MS = 100; 5 | 6 | const useScrollPosition = () => { 7 | const [scrollPosition, setScrollPosition] = useState(0); 8 | 9 | const isScrollTop = useMemo(() => scrollPosition < 100, [scrollPosition]); 10 | 11 | useEffect(() => { 12 | const updatePosition = throttle(() => { 13 | setScrollPosition(window.pageYOffset); 14 | }, THROTTLE_WAIT_MS); 15 | 16 | window.addEventListener('scroll', updatePosition); 17 | return () => window.removeEventListener('scroll', updatePosition); 18 | }, []); 19 | 20 | return { 21 | isScrollTop, 22 | scrollPosition, 23 | }; 24 | }; 25 | 26 | export default useScrollPosition; 27 | -------------------------------------------------------------------------------- /client/src/pocket/hooks/useUpdateCode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UpdateCodeRequest, 3 | UpdateCodeResponse, 4 | updateCodeResponseValidate, 5 | } from '@codepocket/schema'; 6 | import { getCodeByIdAPI } from '@shared/api'; 7 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 8 | import { useQueryClient } from '@tanstack/react-query'; 9 | 10 | import { getCodesUrl } from '../api'; 11 | 12 | type UpdateCodeBodyType = UpdateCodeRequest['body']; 13 | 14 | interface UseUpdateCode { 15 | codeId?: string; 16 | onError?: () => void; 17 | } 18 | 19 | const useUpdateCode = ({ codeId }: UseUpdateCode) => { 20 | const queryClient = useQueryClient(); 21 | const { mutate: updateCodeMutate } = useCustomMutation< 22 | UpdateCodeResponse, 23 | { message: string }, 24 | UpdateCodeBodyType 25 | >({ 26 | url: '/code/update', 27 | method: 'PUT', 28 | validator: updateCodeResponseValidate, 29 | options: { 30 | onSuccess: async () => { 31 | await queryClient.invalidateQueries([getCodesUrl]); 32 | await queryClient.invalidateQueries([getCodeByIdAPI, { codeId }]); 33 | }, 34 | }, 35 | }); 36 | 37 | return { updateCode: updateCodeMutate }; 38 | }; 39 | 40 | export default useUpdateCode; 41 | -------------------------------------------------------------------------------- /client/src/pocket/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as m from '@shared/styles/media.css'; 3 | import * as t from '@shared/styles/token.css'; 4 | import * as u from '@shared/styles/utils.css'; 5 | import { style } from '@vanilla-extract/css'; 6 | import { rem } from 'polished'; 7 | 8 | export const wrapper = style([ 9 | u.flex, 10 | u.flexColumn, 11 | u.flexAlignCenter, 12 | { margin: '0 auto', width: rem(700), paddingBottom: rem(40) }, 13 | m.medium({ 14 | width: '90%', 15 | }), 16 | ]); 17 | 18 | export const title = style([ 19 | t.mt50, 20 | t.typography.heading2, 21 | { textAlign: 'center' }, 22 | m.medium({ fontSize: rem(40) }), 23 | m.small({ fontSize: rem(34) }), 24 | ]); 25 | 26 | export const codeList = style([u.fullWidth]); 27 | 28 | export const codeItem = style([ 29 | u.fullWidth, 30 | u.borderRadius2, 31 | { maxHeight: rem(250), marginTop: rem(70), overflow: 'hidden' }, 32 | ]); 33 | 34 | export const codeItemHeader = style([ 35 | u.fullWidth, 36 | u.flex, 37 | u.flexAlignCenter, 38 | u.cursorPointer, 39 | { height: rem(50), justifyContent: 'space-between', padding: `0 ${rem(20)}` }, 40 | ]); 41 | 42 | export const codeItemInfo = style([{ fontWeight: 'bold' }]); 43 | 44 | export const codeItemCode = style([{ backgroundColor: vars.$scale.color.gray300 }]); 45 | -------------------------------------------------------------------------------- /client/src/routes.ts: -------------------------------------------------------------------------------- 1 | export interface DetailPathParam { 2 | codeId: string; 3 | } 4 | 5 | export interface TokenPathQuery { 6 | token: string; 7 | } 8 | 9 | export const pocketPath = '/'; 10 | export const generatePocketPath = () => '/'; 11 | 12 | export const detailPath = `/detail/:codeId`; 13 | export const generateDetailPath = ({ codeId }: DetailPathParam) => `/detail/${codeId}`; 14 | 15 | export const authPath = '/auth'; 16 | 17 | export const tokenPath = '/token'; 18 | export const generateTokenPath = ({ token }: TokenPathQuery) => `/token?token=${token}`; 19 | -------------------------------------------------------------------------------- /client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, afterEach, beforeAll } from 'vitest'; 2 | 3 | import server from './__mocks__/server'; 4 | 5 | beforeAll(() => { 6 | server.listen(); 7 | }); 8 | 9 | afterEach(() => { 10 | server.resetHandlers(); 11 | }); 12 | 13 | afterAll(() => { 14 | server.close(); 15 | }); 16 | -------------------------------------------------------------------------------- /client/src/shared/api.ts: -------------------------------------------------------------------------------- 1 | export const getCodeByIdAPI = '/code/id'; 2 | -------------------------------------------------------------------------------- /client/src/shared/components/Alert/index.tsx: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import { Icon } from '@shared/components'; 3 | 4 | import * as style from './style.css'; 5 | 6 | interface AlertInterface { 7 | status: 'error' | 'warning' | 'info' | 'success'; 8 | children: React.ReactNode; 9 | } 10 | 11 | const AlertIcon = ({ status }: Pick) => { 12 | const icon = { 13 | error: , 14 | info: , 15 | success: , 16 | warning: , 17 | }; 18 | return icon[status]; 19 | }; 20 | 21 | const AlertTitle = ({ children }: Pick) => { 22 | return

{children}

; 23 | }; 24 | 25 | const AlertDescription = ({ children }: Pick) => { 26 | return

{children}

; 27 | }; 28 | 29 | const AlertContainer = ({ status, children }: Pick) => { 30 | return ( 31 |
32 | 33 |
{children}
34 |
35 | ); 36 | }; 37 | 38 | /** 39 | * @description 어떤 알림 뷰를 보여주고 싶을 때 사용해요 40 | * @example 41 | * 42 | 타이틀이에요 43 | 설명이에요 44 | 45 | */ 46 | const Alert = Object.assign(AlertContainer, { 47 | Description: AlertDescription, 48 | Title: AlertTitle, 49 | }); 50 | 51 | export default Alert; 52 | -------------------------------------------------------------------------------- /client/src/shared/components/Alert/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { recipe } from '@vanilla-extract/recipes'; 5 | import { rem } from 'polished'; 6 | 7 | export const alertContainer = recipe({ 8 | base: [ 9 | u.fullWidth, 10 | u.flexAlignCenter, 11 | u.borderRadius, 12 | { 13 | height: 'auto', 14 | padding: rem(12), 15 | columnGap: rem(10), 16 | }, 17 | ], 18 | variants: { 19 | status: { 20 | error: { background: vars.$scale.color.red100 }, 21 | info: { background: vars.$scale.color.blue100 }, 22 | success: { background: vars.$scale.color.green50 }, 23 | warning: { background: vars.$scale.color.carrot100 }, 24 | }, 25 | }, 26 | }); 27 | 28 | export const alertTextContainer = style([ 29 | u.flexColumn, 30 | { 31 | rowGap: rem(6), 32 | }, 33 | ]); 34 | 35 | export const alertTitle = style({ 36 | fontSize: rem(18), 37 | fontWeight: 'bold', 38 | }); 39 | 40 | export const alertDescription = style([ 41 | { 42 | fontSize: rem(14), 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /client/src/shared/components/AsyncBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { useQueryErrorResetBoundary } from '@tanstack/react-query'; 2 | import { ComponentProps, Suspense, useCallback } from 'react'; 3 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; 4 | 5 | type ErrorBoundaryProps = ComponentProps; 6 | 7 | interface AsyncBoundaryProps extends Omit { 8 | pendingFallback: ComponentProps['fallback']; 9 | rejectedFallback?: ErrorBoundaryProps['fallbackRender']; 10 | } 11 | 12 | function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { 13 | return ( 14 |
15 |

Something went wrong:

16 |
{error.message}
17 | 18 |
19 | ); 20 | } 21 | 22 | function AsyncBoundary({ pendingFallback, rejectedFallback, children }: AsyncBoundaryProps) { 23 | const { reset } = useQueryErrorResetBoundary(); 24 | const resetHandler = useCallback(() => { 25 | reset(); 26 | }, [reset]); 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | export default AsyncBoundary; 36 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/@types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IconInterface { 2 | color?: string; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Check.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Check = ({ color }: IconInterface) => ( 4 | 5 | 9 | 13 | 14 | ); 15 | 16 | export default Check; 17 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Clip.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Clip = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default Clip; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Close.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Close = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default Close; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Code.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Code = ({ color }: IconInterface) => ( 4 | 5 | 11 | 17 | 23 | 29 | 30 | ); 31 | 32 | export default Code; 33 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Edit.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Edit = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default Edit; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Information.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Information = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default Information; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/LeftChevron.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const LeftChevron = ({ color }: IconInterface) => ( 4 | 5 | 10 | 11 | ); 12 | 13 | export default LeftChevron; 14 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Profile.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Profile = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default Profile; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/RightChevron.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const RightChevron = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default RightChevron; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/Search.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const Search = ({ color }: IconInterface) => ( 4 | 11 | 17 | 23 | 24 | ); 25 | 26 | export default Search; 27 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/WarningFill.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | 3 | const WarningFill = ({ color }: IconInterface) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default WarningFill; 15 | -------------------------------------------------------------------------------- /client/src/shared/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import type { IconInterface } from './@types'; 2 | import Check from './Check'; 3 | import Clip from './Clip'; 4 | import Close from './Close'; 5 | import Code from './Code'; 6 | import Delete from './Delete'; 7 | import Edit from './Edit'; 8 | import Information from './Information'; 9 | import LeftChevron from './LeftChevron'; 10 | import Profile from './Profile'; 11 | import RightChevron from './RightChevron'; 12 | import Search from './Search'; 13 | import WarningFill from './WarningFill'; 14 | 15 | const mapping = { 16 | leftChevron: LeftChevron, 17 | rightChevron: RightChevron, 18 | warningFill: WarningFill, 19 | check: Check, 20 | information: Information, 21 | close: Close, 22 | profile: Profile, 23 | search: Search, 24 | code: Code, 25 | clip: Clip, 26 | delete: Delete, 27 | edit: Edit, 28 | } as const; 29 | 30 | const Icon = ({ icon, ...props }: IconInterface & { icon: keyof typeof mapping }) => 31 | mapping[icon](props as any); 32 | 33 | export default Icon; 34 | -------------------------------------------------------------------------------- /client/src/shared/components/IconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@shared/components'; 2 | import React from 'react'; 3 | 4 | import * as style from './style.css'; 5 | 6 | interface IconButtonInterface { 7 | icon: ReturnType; 8 | onClick?: (event: React.MouseEvent) => void; 9 | } 10 | 11 | /** 12 | * @description 아이콘이 들어간 버튼을 만들고 싶을 때 사용해요 13 | * @example 14 | * } /> 15 | */ 16 | const IconButton = ({ icon, onClick }: IconButtonInterface) => { 17 | return ( 18 | 21 | ); 22 | }; 23 | 24 | export default IconButton; 25 | -------------------------------------------------------------------------------- /client/src/shared/components/IconButton/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as u from '@shared/styles/utils.css'; 3 | import { style } from '@vanilla-extract/css'; 4 | import { rem } from 'polished'; 5 | 6 | export const iconButton = style([ 7 | u.flexCenter, 8 | u.cursorPointer, 9 | { 10 | background: 'none', 11 | border: `1px solid ${vars.$scale.color.gray300}`, 12 | borderRadius: rem(10), 13 | transition: 'background 0.2s ease', 14 | 15 | ':hover': { 16 | backgroundColor: vars.$scale.color.gray200, 17 | }, 18 | }, 19 | ]); 20 | -------------------------------------------------------------------------------- /client/src/shared/components/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | VerifyUserRequest, 3 | VerifyUserResponse, 4 | verifyUserResponseValidate, 5 | } from '@codepocket/schema'; 6 | import useCustomMutation from '@shared/hooks/useCustomMutation'; 7 | import { ReactElement, useCallback, useEffect } from 'react'; 8 | import { useNavigate, useParams } from 'react-router-dom'; 9 | 10 | import { verifyUserUrl } from '../../auth/api'; 11 | import { authPath } from '../../routes'; 12 | import { localStorage } from '../utils/localStorage'; 13 | 14 | type VerifyUserBodyType = VerifyUserRequest['body']; 15 | interface PrivateRoutProps { 16 | element: ReactElement; 17 | path: (params?: any) => string; 18 | } 19 | 20 | const PrivateRoute: React.FC = (props) => { 21 | const params = useParams(); 22 | const navigate = useNavigate(); 23 | const navigateAuthPage = useCallback( 24 | () => navigate(authPath, { replace: true, state: { path: props.path(params) } }), 25 | [navigate, params, props], 26 | ); 27 | const { mutate: verifyUserMutate } = useCustomMutation< 28 | VerifyUserResponse, 29 | VerifyUserResponse, 30 | VerifyUserBodyType 31 | >({ 32 | url: verifyUserUrl, 33 | validator: verifyUserResponseValidate, 34 | method: 'POST', 35 | options: { 36 | onError: navigateAuthPage, 37 | }, 38 | }); 39 | 40 | const authenticateUser = useCallback(async () => { 41 | const token = localStorage.getUserToken(); 42 | if (!token) return navigateAuthPage(); 43 | 44 | return verifyUserMutate({ pocketToken: token }); 45 | }, [navigateAuthPage, verifyUserMutate]); 46 | 47 | useEffect(() => { 48 | authenticateUser(); 49 | }, [authenticateUser]); 50 | 51 | return props.element; 52 | }; 53 | 54 | export default PrivateRoute; 55 | -------------------------------------------------------------------------------- /client/src/shared/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Alert } from './Alert'; 2 | export { default as AsyncBoundary } from './AsyncBoundary'; 3 | export { default as Icon } from './Icon'; 4 | export { default as IconButton } from './IconButton'; 5 | export { default as Modal } from './Modal'; 6 | export { default as PrivateRoute } from './PrivateRoute'; 7 | -------------------------------------------------------------------------------- /client/src/shared/constant.ts: -------------------------------------------------------------------------------- 1 | export const BASE_SERVER_URL = import.meta.env.VITE_BASE_SERVER_URL; 2 | 3 | export const AUTH0_DOMAIN = import.meta.env.VITE_AUTH0_DOMAIN; 4 | export const AUTH0_CLIENT_ID = import.meta.env.VITE_AUTH0_CLIENT_ID; 5 | 6 | export const EMAIL_DOMAIN_NAME = import.meta.env.VITE_EMAIL_DOMAIN_NAME; 7 | -------------------------------------------------------------------------------- /client/src/shared/contexts/GlobalModal.tsx: -------------------------------------------------------------------------------- 1 | import Modal from '@shared/components/Modal'; 2 | import { useModalDispatch, useModalState } from '@shared/contexts/ModalContext'; 3 | import { useCallback } from 'react'; 4 | 5 | import { CreateModal, DeleteModal, EditModal } from '../../pocket/components'; 6 | 7 | export const modals = { 8 | createModal: CreateModal, 9 | deleteModal: DeleteModal, 10 | editModal: EditModal, 11 | }; 12 | 13 | const GlobalModal = () => { 14 | const dispatch = useModalDispatch(); 15 | const state = useModalState(); 16 | 17 | const { ModalComponent, targetId } = state; 18 | const closeModal = useCallback(() => { 19 | if (state.closeModal) state.closeModal(); 20 | dispatch({ type: 'CLOSE_MODAL' }); 21 | }, [dispatch, state]); 22 | 23 | return ( 24 | 25 | {ModalComponent && ( 26 | 27 | )} 28 | 29 | ); 30 | }; 31 | 32 | export default GlobalModal; 33 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import to from 'await-to-js'; 2 | import { useCallback, useState } from 'react'; 3 | 4 | interface UseClipboardParameters { 5 | text: string; 6 | } 7 | 8 | const useClipboard = ({ text }: UseClipboardParameters) => { 9 | const [isCopied, setIsCopied] = useState(false); 10 | 11 | const copyToClipboard = useCallback(async () => { 12 | const [error] = await to(navigator.clipboard.writeText(text)); 13 | if (error) return setIsCopied(false); 14 | 15 | setIsCopied(true); 16 | return setTimeout(() => { 17 | setIsCopied(false); 18 | }, 2000); 19 | }, [text]); 20 | 21 | return { isCopied, copyToClipboard }; 22 | }; 23 | 24 | export default useClipboard; 25 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useCode.ts: -------------------------------------------------------------------------------- 1 | import { GetCodeResponse, getCodeResponseValidate } from '@codepocket/schema'; 2 | import { getCodeByIdAPI } from '@shared/api'; 3 | import useCustomQuery from '@shared/hooks/useCustomQuery'; 4 | 5 | interface UseCode { 6 | codeId?: string; 7 | onError?: () => void; 8 | } 9 | 10 | const useCode = ({ codeId, onError }: UseCode) => 11 | useCustomQuery({ 12 | url: getCodeByIdAPI, 13 | validator: getCodeResponseValidate, 14 | params: { codeId: `${codeId}` }, 15 | options: { onError }, 16 | }); 17 | 18 | export default useCode; 19 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useCustomInfiniteQuery.ts: -------------------------------------------------------------------------------- 1 | import { QueryFunctionContext, useInfiniteQuery } from '@tanstack/react-query'; 2 | 3 | import { APIErrorType, axiosInstance } from '../lib/axios'; 4 | 5 | type ParamsType = { [param: string]: string | number | undefined }; 6 | interface useCustomInfiniteQueryInterface { 7 | url: string; 8 | params?: ParamsType; 9 | } 10 | 11 | interface Fetcher { 12 | data: Response; 13 | nextPage: number; 14 | prevPage: number; 15 | } 16 | 17 | const fetcher = async ({ 18 | queryKey, 19 | pageParam = 0, 20 | }: QueryFunctionContext<[string, ParamsType?]>): Promise> => { 21 | const [url, params] = queryKey; 22 | const { data } = await axiosInstance.get(url, { params: { ...params, offset: pageParam } }); 23 | return { data, nextPage: pageParam + 1, prevPage: pageParam - 1 }; 24 | }; 25 | 26 | const useCustomInfiniteQuery = ({ url, params }: useCustomInfiniteQueryInterface) => { 27 | const res = useInfiniteQuery< 28 | Fetcher, 29 | APIErrorType, 30 | Fetcher, 31 | [string, ParamsType?] 32 | >([url!, params], ({ queryKey, meta, pageParam }) => fetcher({ queryKey, meta, pageParam }), { 33 | getNextPageParam: (lastPage) => lastPage.nextPage, 34 | getPreviousPageParam: (firstPage) => firstPage.prevPage, 35 | }); 36 | 37 | return res; 38 | }; 39 | 40 | export default useCustomInfiniteQuery; 41 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useCustomMutation.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, UseMutationOptions } from '@tanstack/react-query'; 2 | 3 | import { APIErrorType, axiosInstance } from '../lib/axios'; 4 | 5 | export type MethodType = 'POST' | 'DELETE' | 'PUT' | 'UPDATE'; 6 | 7 | type CustomUseMutationOptions = Omit< 8 | UseMutationOptions, 9 | 'mutationKey' 10 | >; 11 | 12 | interface CustomMutationInterface { 13 | url: string; 14 | method: MethodType; 15 | validator: (res: Response | undefined) => res is Response; 16 | options?: CustomUseMutationOptions, Variable, Context>; 17 | } 18 | 19 | const fetcher = 20 | (url: string, method: MethodType) => 21 | async (data: Variable) => { 22 | const response = await axiosInstance({ url, method, data }); 23 | return response.data; 24 | }; 25 | 26 | const useCustomMutation = ({ 27 | url, 28 | method, 29 | options, 30 | }: CustomMutationInterface) => { 31 | const { data, isSuccess, ...others } = useMutation< 32 | Response, 33 | APIErrorType, 34 | Variable, 35 | Context 36 | >(fetcher(url, method), { ...options }); 37 | 38 | return { data, isSuccess, ...others }; 39 | }; 40 | 41 | export default useCustomMutation; 42 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useCustomQuery.ts: -------------------------------------------------------------------------------- 1 | import { QueryFunctionContext, QueryKey, useQuery, UseQueryOptions } from '@tanstack/react-query'; 2 | 3 | import { APIErrorType, axiosInstance } from '../lib/axios'; 4 | 5 | type QueryOptions = Omit< 6 | UseQueryOptions, 7 | '' 8 | > & { 9 | initialData?: () => undefined; 10 | }; 11 | 12 | interface CustomQueryInterface { 13 | url: string; 14 | params?: { [param: string]: string }; 15 | validator: (res: Response | undefined) => res is Response; 16 | options?: QueryOptions; 17 | } 18 | 19 | const fetcher = async ({ queryKey }: QueryFunctionContext): Promise => { 20 | const [url, params] = queryKey; 21 | const { data } = await axiosInstance.get(url as string, { params }); 22 | return data; 23 | }; 24 | 25 | const useCustomQuery = ({ url, params, options }: CustomQueryInterface) => { 26 | const commonOptions: QueryOptions = { staleTime: 1000000, cacheTime: 1000000 }; 27 | const { data, ...others } = useQuery( 28 | [url!, params], 29 | ({ queryKey, meta }) => fetcher({ queryKey, meta }), 30 | { 31 | ...commonOptions, 32 | ...options, 33 | }, 34 | ); 35 | 36 | return { data, ...others }; 37 | }; 38 | 39 | export default useCustomQuery; 40 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useKeyboard.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | 3 | type KeyType = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Escape' | 'Space'; 4 | interface KeyEvent { 5 | key: KeyType; 6 | keyEvent: (event: KeyboardEvent) => void; 7 | } 8 | 9 | interface KeyboardHandlerProps { 10 | keyEvents: KeyEvent[]; 11 | } 12 | 13 | const keyMapper: { [key in KeyType]: string } = { 14 | ArrowUp: 'ArrowUp', 15 | ArrowDown: 'ArrowDown', 16 | ArrowLeft: 'ArrowLeft', 17 | ArrowRight: 'ArrowRight', 18 | Escape: 'Escape', 19 | Space: ' ', 20 | }; 21 | 22 | const useKeyboard = ({ keyEvents }: KeyboardHandlerProps) => { 23 | const executeEvents = useCallback( 24 | (event: KeyboardEvent) => { 25 | keyEvents.forEach(({ key, keyEvent }) => { 26 | if (event.key === keyMapper[key]) keyEvent(event); 27 | }); 28 | }, 29 | [keyEvents], 30 | ); 31 | 32 | useEffect(() => { 33 | window.addEventListener('keydown', executeEvents); 34 | return () => window.removeEventListener('keydown', executeEvents); 35 | }, [keyEvents, executeEvents]); 36 | }; 37 | 38 | export default useKeyboard; 39 | -------------------------------------------------------------------------------- /client/src/shared/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | interface UseModal { 4 | isOpened: boolean; 5 | } 6 | 7 | const useModal = ({ isOpened }: UseModal) => { 8 | const [isModalOpened, setIsModalOpened] = useState(false); 9 | 10 | const closeModal = () => setIsModalOpened(false); 11 | 12 | useEffect(() => { 13 | setIsModalOpened(isOpened); 14 | }, [isOpened]); 15 | 16 | return { closeModal, isModalOpened }; 17 | }; 18 | 19 | export default useModal; 20 | -------------------------------------------------------------------------------- /client/src/shared/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | 3 | import { BASE_SERVER_URL } from '../constant'; 4 | 5 | export interface ResponseType extends AxiosResponse { 6 | data: T; 7 | } 8 | 9 | interface ErrorType extends AxiosResponse { 10 | response: { 11 | data: T; 12 | }; 13 | } 14 | 15 | type NetworkError = ErrorType<{ message: string }>; // 존재하지 않는 경로 등 16 | type ServerError = ErrorType; // 서버에서 에러 반환 등 17 | export type APIErrorType = NetworkError | ServerError; 18 | 19 | const transformResponse = (data: string) => { 20 | try { 21 | return JSON.parse(data); 22 | } catch (error) { 23 | return { message: (error as Error).message }; 24 | } 25 | }; 26 | 27 | export const axiosInstance = axios.create({ 28 | transformResponse, 29 | baseURL: BASE_SERVER_URL, 30 | }); 31 | -------------------------------------------------------------------------------- /client/src/shared/styles/global.css.ts: -------------------------------------------------------------------------------- 1 | import { globalStyle } from '@vanilla-extract/css'; 2 | 3 | globalStyle('html, body, #root', { 4 | margin: 0, 5 | padding: 0, 6 | height: '100%', 7 | }); 8 | 9 | globalStyle('body', { 10 | lineHeight: '1.3', 11 | }); 12 | 13 | globalStyle( 14 | 'html, body, div, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, em, img, ins, kbd, q, s, samp, small, strike, strong, article, footer, header,main,nav, section', 15 | { 16 | margin: '0', 17 | padding: '0', 18 | border: '0', 19 | fontSize: '100%', 20 | font: 'inherit', 21 | verticalAlign: 'baseline', 22 | fontFamily: 23 | '-apple-system, BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"', 24 | }, 25 | ); 26 | 27 | globalStyle( 28 | 'article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section', 29 | { 30 | display: 'block', 31 | }, 32 | ); 33 | 34 | globalStyle('ol, ul', { 35 | listStyle: 'none', 36 | padding: 0, 37 | margin: 0, 38 | }); 39 | 40 | globalStyle('h1, h2, h3, h4, h5, h6, p', { 41 | wordBreak: 'keep-all', 42 | whiteSpace: 'pre-wrap', 43 | letterSpacing: '-0.02em', 44 | lineHeight: '1.3', 45 | }); 46 | 47 | globalStyle('span', { 48 | wordBreak: 'keep-all', 49 | whiteSpace: 'pre', 50 | letterSpacing: '-0.02em', 51 | lineHeight: '1.3', 52 | }); 53 | 54 | globalStyle('*, *:after, *:before', { 55 | boxSizing: 'border-box', 56 | }); 57 | 58 | globalStyle('a', { 59 | textDecoration: 'none', 60 | color: 'inherit', 61 | }); 62 | -------------------------------------------------------------------------------- /client/src/shared/styles/keyframes.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import { keyframes } from '@vanilla-extract/css'; 3 | import { rem } from 'polished'; 4 | 5 | export const fadeIn = (to: number) => 6 | keyframes({ 7 | '0%': { opacity: 0 }, 8 | '100%': { opacity: to }, 9 | }); 10 | 11 | export const fadeOut = (from: number) => 12 | keyframes({ 13 | '0%': { opacity: from }, 14 | '100%': { opacity: 0 }, 15 | }); 16 | 17 | export const fadeInWithSinkDown = keyframes({ 18 | '0%': { opacity: 0, transform: 'translateY(-40px)' }, 19 | '100%': { opacity: 1, transform: 'translateY(0)' }, 20 | }); 21 | 22 | export const rotate = keyframes({ 23 | '0%': { transform: 'rotate(0deg)' }, 24 | '100%': { transform: 'rotate(360deg)' }, 25 | }); 26 | 27 | export const jumpUp = keyframes({ 28 | '0%': { top: rem(0), zIndex: -1 }, 29 | '100%': { top: rem(-30), zIndex: -1 }, 30 | }); 31 | 32 | export const sinkDown = keyframes({ 33 | '0%': { top: rem(-30), zIndex: -1 }, 34 | '100%': { top: rem(0), zIndex: -1 }, 35 | }); 36 | 37 | export const scaleUp = keyframes({ 38 | '0%': { transform: 'scale(0)' }, 39 | '100%': { transform: 'scale(1)' }, 40 | }); 41 | 42 | export const scaleDown = keyframes({ 43 | '0%': { transform: 'scale(1)' }, 44 | '100%': { transform: 'scale(0)' }, 45 | }); 46 | 47 | export const gradation = keyframes({ 48 | '0%': { backgroundColor: vars.$scale.color.gray100 }, 49 | '50%': { backgroundColor: vars.$scale.color.gray300 }, 50 | '100%': { backgroundColor: vars.$scale.color.gray100 }, 51 | }); 52 | -------------------------------------------------------------------------------- /client/src/shared/styles/media.css.ts: -------------------------------------------------------------------------------- 1 | import { StyleRule } from '@vanilla-extract/css'; 2 | 3 | export function small(token: StyleRule) { 4 | return { 5 | '@media': { 6 | 'screen and (max-width: 360px)': token, 7 | }, 8 | }; 9 | } 10 | 11 | export function medium(token: StyleRule) { 12 | return { 13 | '@media': { 14 | 'screen and (max-width: 768px)': token, 15 | }, 16 | }; 17 | } 18 | 19 | export function large(token: StyleRule) { 20 | return { 21 | '@media': { 22 | 'screen and (max-width: 1420px)': token, 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /client/src/shared/utils/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 3 | import React from 'react'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | interface ProviderProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | const Providers: React.FC = (props) => { 11 | const queryClient = new QueryClient(); 12 | const isDev = process.env.NODE_ENV === 'development'; 13 | 14 | return ( 15 | 16 | 17 | 18 | {props.children} 19 | {isDev && } 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default Providers; 27 | -------------------------------------------------------------------------------- /client/src/shared/utils/Transition.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react'; 2 | 3 | type StatusType = 'on' | 'off' | 'oning' | 'offing'; 4 | interface TransitionProps { 5 | children: (state: StatusType) => React.ReactNode; 6 | isOn?: boolean; 7 | timeout?: number; 8 | } 9 | 10 | function Transition({ children, isOn, timeout = 500 }: TransitionProps) { 11 | const timerRef = useRef(); 12 | const [status, setStatus] = useState(isOn ? 'on' : 'off'); 13 | 14 | const onTransitionEnd = useCallback(() => { 15 | if (status === 'off') return; 16 | setStatus('offing'); 17 | timerRef.current = setTimeout(() => { 18 | setStatus('off'); 19 | }, timeout); 20 | }, [setStatus, timeout, status]); 21 | 22 | const onTransitionStart = useCallback(() => { 23 | if (status === 'on') return; 24 | setStatus('oning'); 25 | timerRef.current = setTimeout(() => { 26 | setStatus('on'); 27 | }, timeout); 28 | }, [setStatus, timeout, status]); 29 | 30 | useEffect(() => { 31 | if (!isOn) return onTransitionEnd(); 32 | onTransitionStart(); 33 | 34 | return () => clearTimeout(timerRef.current); 35 | }, [isOn, onTransitionEnd, onTransitionStart]); 36 | 37 | if (status === 'off') return <>; 38 | return <>{children(status)}; 39 | } 40 | 41 | export default Transition; 42 | -------------------------------------------------------------------------------- /client/src/shared/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | const keys = { 2 | USER_TOKEN_KEY: 'userToken', 3 | USER_ID: 'userId', 4 | } as const; 5 | 6 | const store = { 7 | set(key: string, value: T) { 8 | window.localStorage.setItem(key, JSON.stringify(value)); 9 | }, 10 | get(key: string): T | null { 11 | const item = window.localStorage.getItem(key); 12 | if (!item) return null; 13 | 14 | try { 15 | return JSON.parse(item); 16 | } catch { 17 | return item as unknown as T; 18 | } 19 | }, 20 | remove(key: string) { 21 | window.localStorage.removeItem(key); 22 | }, 23 | } as const; 24 | 25 | export const localStorage = { 26 | setUserToken: (userToken: string) => store.set(keys.USER_TOKEN_KEY, userToken), 27 | getUserToken: () => store.get(keys.USER_TOKEN_KEY), 28 | setUserId: (userId: string) => store.set(keys.USER_ID, userId), 29 | getUserId: () => store.get(keys.USER_ID), 30 | } as const; 31 | -------------------------------------------------------------------------------- /client/src/shared/utils/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import { render, RenderOptions } from '@testing-library/react'; 2 | 3 | import Providers from './Providers'; 4 | 5 | const customRender = (ui: React.ReactElement, options?: Omit) => 6 | render(ui, { 7 | wrapper: ({ children }) => {children}, 8 | ...options, 9 | }); 10 | 11 | export * from '@testing-library/react'; 12 | export { customRender as render }; 13 | -------------------------------------------------------------------------------- /client/src/token/hooks/useSearch.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | function useSearch() { 5 | const { search } = useLocation(); 6 | 7 | return useMemo(() => new URLSearchParams(search), [search]); 8 | } 9 | 10 | export default useSearch; 11 | -------------------------------------------------------------------------------- /client/src/token/style.css.ts: -------------------------------------------------------------------------------- 1 | import { vars } from '@seed-design/design-token'; 2 | import * as m from '@shared/styles/media.css'; 3 | import * as t from '@shared/styles/token.css'; 4 | import * as u from '@shared/styles/utils.css'; 5 | import { style } from '@vanilla-extract/css'; 6 | import { recipe } from '@vanilla-extract/recipes'; 7 | import { rem } from 'polished'; 8 | 9 | export const wrapper = style([ 10 | u.positionAbsolute, 11 | u.flexColumn, 12 | { 13 | width: rem(500), 14 | gap: rem(10), 15 | top: '50%', 16 | left: '50%', 17 | transform: 'translate(-50%, -50%)', 18 | }, 19 | m.medium({ 20 | width: rem(360), 21 | }), 22 | m.small({ 23 | width: '88vw', 24 | }), 25 | ]); 26 | 27 | export const title = style([t.typography.heading4]); 28 | 29 | export const description = style([ 30 | t.typography.caption1, 31 | { 32 | color: vars.$scale.color.gray700, 33 | }, 34 | ]); 35 | 36 | export const clipBoardText = style([ 37 | t.typography.body2, 38 | { fontWeight: 'bold', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, 39 | ]); 40 | export const clipBoardIconBox = style({ paddingTop: rem(4), paddingLeft: rem(5) }); 41 | export const clipBoardContainer = recipe({ 42 | base: [ 43 | u.borderRadius2, 44 | u.flex, 45 | u.flexAlignCenter, 46 | u.cursorPointer, 47 | { 48 | border: 'none', 49 | padding: rem(10), 50 | height: rem(52), 51 | backgroundColor: vars.$scale.color.gray100, 52 | justifyContent: 'space-between', 53 | transition: 'background 0.2s ease, border 0.2s ease', 54 | 55 | ':hover': { 56 | backgroundColor: vars.$scale.color.gray300, 57 | }, 58 | }, 59 | ], 60 | variants: { 61 | isCopied: { 62 | true: { border: `${rem(2)} solid ${vars.$scale.color.blue600}` }, 63 | false: { border: `${rem(2)} solid white` }, 64 | }, 65 | }, 66 | }); 67 | 68 | export const linkButton = style([ 69 | u.fullWidth, 70 | u.borderNone, 71 | u.borderRadius2, 72 | u.cursorPointer, 73 | { 74 | height: rem(52), 75 | fontSize: rem(16), 76 | backgroundColor: vars.$scale.color.blue500, 77 | color: 'white', 78 | fontWeight: 'bold', 79 | transition: 'background 0.2s ease', 80 | 81 | ':hover': { 82 | backgroundColor: vars.$scale.color.blue600, 83 | }, 84 | }, 85 | ]); 86 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/styleMock.js: -------------------------------------------------------------------------------- 1 | // FIXME: 고도화 2 | export const input = () => {}; 3 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "types": ["vitest/importMeta"], 20 | "paths": { 21 | "@shared/*": ["./src/shared/*"] 22 | } 23 | }, 24 | "include": ["src", "vite.config.ts"], 25 | "exclude": ["**.test.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | import jotaiDebugLabel from 'jotai/babel/plugin-debug-label'; 5 | import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh'; 6 | import path from 'path'; 7 | import { defineConfig } from 'vite'; 8 | import checker from 'vite-plugin-checker'; 9 | import pluginRewriteAll from 'vite-plugin-rewrite-all'; 10 | 11 | export default defineConfig({ 12 | test: { 13 | globals: true, 14 | environment: 'jsdom', 15 | setupFiles: ['./src/setupTests.ts'], 16 | includeSource: ['src/**/*.ts', 'src/**/*.tsx'], 17 | deps: { 18 | fallbackCJS: true, 19 | }, 20 | }, 21 | plugins: [ 22 | react({ babel: { plugins: [jotaiDebugLabel, jotaiReactRefresh] } }), 23 | pluginRewriteAll(), 24 | vanillaExtractPlugin(), 25 | checker({ 26 | typescript: true, 27 | }), 28 | ], 29 | define: { 30 | 'import.meta.vitest': 'undefined', 31 | }, 32 | resolve: { 33 | alias: [{ find: '@shared', replacement: path.join(__dirname, './src/shared') }], 34 | }, 35 | server: { 36 | port: 3000, 37 | host: true, 38 | }, 39 | envDir: '../', 40 | }); 41 | -------------------------------------------------------------------------------- /core/server/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | lib/ 4 | .DS_Store 5 | .bsb.lock 6 | .merlin 7 | 8 | *.bs.js 9 | *.gen.ts -------------------------------------------------------------------------------- /core/server/README.md: -------------------------------------------------------------------------------- 1 | # @codepocket/core-server 2 | 3 | > **@codepocket/server**의 핵심 로직을 담당하고 있어요. 4 | 5 | - 디비와 api통신에 관련된 부분들은 완전히 제외하고, 의존성 주입(DI)을 통해 필요한 함수들을 주입받고 있어요. 6 | - slack api를 사용자의 선택에 따라 제공할 수 있어요. 7 | - 현재 fastify/mongodb로 구성된 server를 다른 기술스택으로 바꿀 때 사용할 수 있어요. 8 | - connector함수는 api들에서 중복이 많은 함수나 설정값들을 받고, core로직들을 반환해주어요. 9 | -------------------------------------------------------------------------------- /core/server/bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/core-server", 3 | "version": "0.0.0", 4 | "sources": [ 5 | { 6 | "dir": "src", 7 | "subdirs": true 8 | } 9 | ], 10 | "package-specs": [ 11 | { 12 | "module": "es6", 13 | "in-source": true 14 | } 15 | ], 16 | "suffix": ".bs.js", 17 | "bs-dependencies": [], 18 | "gentypeconfig": { 19 | "language": "typescript", 20 | "shims": {}, 21 | "generatedFileExtension": ".gen.ts", 22 | "module": "es6", 23 | "debug": { 24 | "all": false, 25 | "basic": false 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/core-server", 3 | "version": "0.0.3", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/daangn/codepocket.git" 8 | }, 9 | "main": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "build": "tsc", 13 | "re:build": "rescript" 14 | }, 15 | "dependencies": { 16 | "@codepocket/schema": "^0.0.4" 17 | }, 18 | "devDependencies": { 19 | "gentype": "^4.5.0", 20 | "rescript": "^9.1.4", 21 | "typescript": "^4.7.4" 22 | }, 23 | "packageManager": "yarn@3.2.1" 24 | } 25 | -------------------------------------------------------------------------------- /core/server/src/createStory.ts: -------------------------------------------------------------------------------- 1 | import { createStoryRequestValidate, CreateStoryResponse } from '@codepocket/schema'; 2 | import { PocketToken, StoryInfoWithCode, StoryInfoWithCodeId, UserNameWithId } from 'types'; 3 | 4 | export interface CreateStoryType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 존재하는 스토리가 있다면 에러 */ 8 | existStoryErrorFunc: Response; 9 | /* 성공했을 경우 */ 10 | successResponseFunc: (body: CreateStoryResponse) => Response; 11 | 12 | /* 스토리가 존재하는지 체크하는 함수 */ 13 | isStoryExist: (param: StoryInfoWithCodeId) => Promise; 14 | /* 토큰으로 유저이름을 가져오는 함수 */ 15 | getUserInfo: (params: PocketToken) => Promise; 16 | /* 스토리 생성 함수 */ 17 | createStory: (params: StoryInfoWithCode) => Promise; 18 | } 19 | 20 | export default async (request: T, modules: CreateStoryType) => { 21 | if (!createStoryRequestValidate(request)) throw modules.validateError; 22 | const { pocketToken, codeId, storyName, codes } = request.body; 23 | 24 | const { userName: storyAuthor, userId } = await modules.getUserInfo({ pocketToken }); 25 | 26 | const isStoryExist = await modules.isStoryExist({ codeId, storyAuthor, storyName }); 27 | if (isStoryExist) throw modules.existStoryErrorFunc; 28 | const storyId = await modules.createStory({ codeId, storyAuthor, storyName, userId, codes }); 29 | 30 | return modules.successResponseFunc({ message: '', storyId }); 31 | }; 32 | -------------------------------------------------------------------------------- /core/server/src/createUser.ts: -------------------------------------------------------------------------------- 1 | import { createUserRequestValidate, CreateUserResponse } from '@codepocket/schema'; 2 | import { UserInfo, UserPrivateInfo } from 'types'; 3 | 4 | export interface CreateUserType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: CreateUserResponse) => Response; 9 | 10 | /* 유저가 존재하면 토큰과 ID를 가져오는 함수 */ 11 | getUserPrivateInfo: (params: UserInfo) => Promise; 12 | /* 유저 생성 함수 */ 13 | createUser: (params: UserInfo) => Promise; 14 | } 15 | 16 | export default async (request: T, modules: CreateUserType) => { 17 | if (!createUserRequestValidate(request)) throw modules.validateError; 18 | const { userName, email } = request.body; 19 | 20 | let privateInfo = await modules.getUserPrivateInfo({ userName, email }); 21 | if (!privateInfo) privateInfo = await modules.createUser({ userName, email }); 22 | 23 | return modules.successResponseFunc({ 24 | message: '', 25 | pocketToken: privateInfo.pocketToken, 26 | userId: privateInfo.userId, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /core/server/src/deleteCode.ts: -------------------------------------------------------------------------------- 1 | import { deleteCodeRequestValidate, DeleteCodeResponse } from '@codepocket/schema'; 2 | import { SlackConfig } from 'slack'; 3 | // import { deleteMessageToSlack } from 'slack'; 4 | import { CodeInfo, PocketToken, UserNameWithId } from 'types'; 5 | 6 | export interface DeleteCodeType { 7 | /* validator에러 */ 8 | validateError?: Response; 9 | /* 코드가 존재하지 않을 경우 에러 */ 10 | existCodeErrorFunc: () => Response; 11 | /* 슬랙 통신이 잘못되었을 경우 에러 */ 12 | slackAPIError?: () => Response; 13 | /* 성공했을 경우 */ 14 | successResponseFunc: (body: DeleteCodeResponse) => Response; 15 | 16 | slackConfig?: SlackConfig; 17 | /* 유저 이름을 가져오는 함수 */ 18 | getUserInfo: (params: PocketToken) => Promise; 19 | /* 존재하는 코드인지 확인하는 함수 */ 20 | isExistCode: (params: CodeInfo) => Promise; 21 | /* 코드를 삭제하는 함수 */ 22 | deleteCode: (params: CodeInfo) => Promise; 23 | } 24 | 25 | export default async (request: T, modules: DeleteCodeType) => { 26 | if (!deleteCodeRequestValidate(request)) throw modules.validateError; 27 | const { pocketToken, codeName } = request.body; 28 | 29 | const { userName: codeAuthor } = await modules.getUserInfo({ pocketToken }); 30 | const existCode = await modules.isExistCode({ codeAuthor, codeName }); 31 | if (!existCode) throw modules.existCodeErrorFunc(); 32 | 33 | await modules.deleteCode({ codeAuthor, codeName }); 34 | 35 | if (modules.slackConfig) { 36 | // deleteMessageToSlack(); 37 | } 38 | 39 | return modules.successResponseFunc({ message: '' }); 40 | }; 41 | -------------------------------------------------------------------------------- /core/server/src/deleteCodeById.ts: -------------------------------------------------------------------------------- 1 | import { deleteCodeByIdRequestValidate, DeleteCodeResponse } from '@codepocket/schema'; 2 | import { SlackConfig } from 'slack'; 3 | // import { deleteMessageToSlack } from 'slack'; 4 | import { CodeAuthorWithId, CodeId, PocketToken, UserNameWithId } from 'types'; 5 | 6 | export interface DeleteCodeByIdType { 7 | /* validator에러 */ 8 | validateError?: Response; 9 | /* 코드가 존재하지 않을 경우 에러 */ 10 | existCodeErrorFunc: () => Response; 11 | /* 슬랙 통신이 잘못되었을 경우 에러 */ 12 | slackAPIError?: () => Response; 13 | /* 성공했을 경우 */ 14 | successResponseFunc: (body: DeleteCodeResponse) => Response; 15 | 16 | slackConfig?: SlackConfig; 17 | /* 유저 이름을 가져오는 함수 */ 18 | getUserInfo: (params: PocketToken) => Promise; 19 | /* 존재하는 코드인지 확인하는 함수 */ 20 | isExistCode: (params: CodeAuthorWithId) => Promise; 21 | /* 코드를 삭제하는 함수 */ 22 | deleteCodeById: (params: CodeId) => Promise; 23 | } 24 | 25 | export default async (request: T, modules: DeleteCodeByIdType) => { 26 | if (!deleteCodeByIdRequestValidate(request)) throw modules.validateError; 27 | const { pocketToken, codeId } = request.body; 28 | 29 | const { userName: codeAuthor } = await modules.getUserInfo({ pocketToken }); 30 | const existCode = await modules.isExistCode({ codeId, codeAuthor }); 31 | if (!existCode) throw modules.existCodeErrorFunc(); 32 | 33 | await modules.deleteCodeById({ codeId }); 34 | 35 | if (modules.slackConfig) { 36 | // deleteMessageToSlack(); 37 | } 38 | 39 | return modules.successResponseFunc({ message: '' }); 40 | }; 41 | -------------------------------------------------------------------------------- /core/server/src/deleteStory.ts: -------------------------------------------------------------------------------- 1 | import { deleteStoryRequestValidate, DeleteStoryResponse } from '@codepocket/schema'; 2 | import { StoryInfoWithCodeId } from 'types'; 3 | 4 | export interface DeleteStoryType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 스토리가 존재하지 않을 경우 에러 */ 8 | existStoryErrorFunc: () => Response; 9 | /* 성공했을 경우 */ 10 | successResponseFunc: (body: DeleteStoryResponse) => Response; 11 | 12 | /* 존재하는 스토리인지 확인하는 함수 */ 13 | isExistStory: (params: StoryInfoWithCodeId) => Promise; 14 | /* 스토리를 삭제하는 함수 */ 15 | deleteStory: (params: StoryInfoWithCodeId) => Promise; 16 | } 17 | 18 | export default async (request: T, modules: DeleteStoryType) => { 19 | if (!deleteStoryRequestValidate(request)) throw modules.validateError; 20 | const { codeId, storyAuthor, storyName } = request.body; 21 | 22 | const existStory = await modules.isExistStory({ storyAuthor, storyName, codeId }); 23 | if (!existStory) throw modules.existStoryErrorFunc(); 24 | 25 | await modules.deleteStory({ storyAuthor, storyName, codeId }); 26 | 27 | return modules.successResponseFunc({ message: '' }); 28 | }; 29 | -------------------------------------------------------------------------------- /core/server/src/getCode.ts: -------------------------------------------------------------------------------- 1 | import { getCodeRequestValidate, GetCodeResponse } from '@codepocket/schema'; 2 | import { CodeId, CodeInfoWithCode } from 'types'; 3 | 4 | export interface GetCodeType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: GetCodeResponse) => Response; 9 | 10 | /* code의 id로 code의 정보들을 가져오는 함수 */ 11 | getCodeInfoById: (params: CodeId) => Promise; 12 | } 13 | 14 | export default async (request: T, modules: GetCodeType) => { 15 | if (!getCodeRequestValidate(request)) throw modules.validateError; 16 | const { codeId } = request.query; 17 | 18 | const codeInfo = await modules.getCodeInfoById({ codeId }); 19 | 20 | const { code, codeAuthor, codeName, isAnonymous } = codeInfo; 21 | return modules.successResponseFunc({ code, codeAuthor, codeName, isAnonymous, message: '' }); 22 | }; 23 | -------------------------------------------------------------------------------- /core/server/src/getCodeAuthors.ts: -------------------------------------------------------------------------------- 1 | import { getCodeAuthorsRequestValidate, GetCodeAuthorsResponse } from '@codepocket/schema'; 2 | import { CodeAuthor, CodeName } from 'types'; 3 | 4 | export interface GetCodeAuthorsType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: GetCodeAuthorsResponse) => Response; 9 | 10 | /* 특정 codeName을 가지고 있는 codeAuthor들을 찾는 함수 */ 11 | findCodeAuthors: (params: CodeName) => Promise; 12 | } 13 | 14 | export default async (request: T, modules: GetCodeAuthorsType) => { 15 | if (!getCodeAuthorsRequestValidate(request)) throw modules.validateError; 16 | const { codeName } = request.query; 17 | 18 | const codeAuthors = await modules.findCodeAuthors({ codeName }); 19 | 20 | return modules.successResponseFunc({ message: '', codeAuthors }); 21 | }; 22 | -------------------------------------------------------------------------------- /core/server/src/getCodeNames.ts: -------------------------------------------------------------------------------- 1 | import { getCodeNamesRequestValidate, GetCodeNamesResponse } from '@codepocket/schema'; 2 | import { CodeInfoWithAnonymous, FindCodeInfoUsingRegexParams } from 'types'; 3 | 4 | export interface GetCodeNamesType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: GetCodeNamesResponse) => Response; 9 | 10 | /* Regex로 코드 정보를 찾아오기 */ 11 | findCodeInfoUsingRegex: ( 12 | params: FindCodeInfoUsingRegexParams, 13 | ) => Promise; 14 | } 15 | 16 | export default async (request: T, modules: GetCodeNamesType) => { 17 | if (!getCodeNamesRequestValidate(request)) throw modules.validateError; 18 | const { codeAuthor, codeName } = request.query; 19 | 20 | const codeNameRegex = new RegExp(codeName || '', 'gi'); 21 | const codeAuthorRegex = new RegExp(codeAuthor || '', 'gi'); 22 | 23 | const codeInfos = await modules.findCodeInfoUsingRegex({ 24 | codeAuthorRegex, 25 | codeNameRegex, 26 | isCodeAuthorExist: !!codeAuthor, 27 | }); 28 | 29 | return modules.successResponseFunc({ message: '', codeInfos }); 30 | }; 31 | -------------------------------------------------------------------------------- /core/server/src/getCodes.ts: -------------------------------------------------------------------------------- 1 | import { getCodesRequestValidate, GetCodesResponse } from '@codepocket/schema'; 2 | import { SearchCodesParam } from 'types'; 3 | 4 | export interface GetCodesType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: GetCodesResponse) => Response; 9 | 10 | /* 검색 조건들로 코드들을 검색하는 함수 */ 11 | searchCodes: (params: SearchCodesParam) => Promise; 12 | } 13 | 14 | export default async (request: T, modules: GetCodesType) => { 15 | if (!getCodesRequestValidate(request)) throw modules.validateError; 16 | const { limit = '5', offset = '0', search = '' } = request.query; 17 | 18 | const searchRegex = new RegExp(search, 'gi'); 19 | const codes = await modules.searchCodes({ 20 | searchRegex, 21 | offset: Number(offset), 22 | limit: Number(limit), 23 | }); 24 | 25 | const isLast = codes.length < +limit; 26 | 27 | return modules.successResponseFunc({ message: '', codes, isLast }); 28 | }; 29 | -------------------------------------------------------------------------------- /core/server/src/getStoryCode.ts: -------------------------------------------------------------------------------- 1 | import { getStoryCodeRequestValidate, GetStoryCodeResponse } from '@codepocket/schema'; 2 | import { StoryId } from 'types'; 3 | 4 | export interface GetStoryCodeType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: GetStoryCodeResponse) => Response; 9 | /* 스토리 코드들을 가져오는 함수 */ 10 | getStoryCode: (param: StoryId) => Promise<{ [x: string]: string }>; 11 | } 12 | 13 | export default async (request: T, modules: GetStoryCodeType) => { 14 | if (!getStoryCodeRequestValidate(request)) throw modules.validateError; 15 | const { storyId } = request.query; 16 | 17 | const codes = await modules.getStoryCode({ storyId }); 18 | return modules.successResponseFunc({ codes, message: '' }); 19 | }; 20 | -------------------------------------------------------------------------------- /core/server/src/getStoryNames.ts: -------------------------------------------------------------------------------- 1 | import { getStoryNamesRequestValidate, GetStoryNamesResponse } from '@codepocket/schema'; 2 | import { CodeId, StoryNamesWithCodeId } from 'types'; 3 | 4 | export interface GetStoryNamesType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: GetStoryNamesResponse) => Response; 9 | 10 | /* 스토리 이름들을 가져오는 함수 */ 11 | getStoryNames: (params: CodeId) => Promise; 12 | } 13 | 14 | export default async (request: T, modules: GetStoryNamesType) => { 15 | if (!getStoryNamesRequestValidate(request)) throw modules.validateError; 16 | const { codeId } = request.query; 17 | const storyNames = await modules.getStoryNames({ codeId }); 18 | 19 | return modules.successResponseFunc({ storyNames, message: '' }); 20 | }; 21 | -------------------------------------------------------------------------------- /core/server/src/hello.res: -------------------------------------------------------------------------------- 1 | @genType 2 | type color = 3 | | Red 4 | | Blue; 5 | 6 | Js.log("Hello, Shell, June!") -------------------------------------------------------------------------------- /core/server/src/pullCode.ts: -------------------------------------------------------------------------------- 1 | import { pullCodeRequestValidate, PullCodeResponse } from '@codepocket/schema'; 2 | import { CodeInfo } from 'types'; 3 | 4 | export interface PullCodeType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: PullCodeResponse) => Response; 9 | 10 | /* 코드를 가져오는 함수 */ 11 | getCode: (params: CodeInfo) => Promise; 12 | } 13 | 14 | export default async (request: T, modules: PullCodeType) => { 15 | if (!pullCodeRequestValidate(request)) throw modules.validateError; 16 | const { codeName, codeAuthor } = request.query; 17 | 18 | const code = await modules.getCode({ codeAuthor, codeName }); 19 | 20 | return modules.successResponseFunc({ code, message: '' }); 21 | }; 22 | -------------------------------------------------------------------------------- /core/server/src/slack/api.ts: -------------------------------------------------------------------------------- 1 | import to from 'await-to-js'; 2 | import fetch from 'node-fetch'; 3 | 4 | import { Languages, SLACK_POST_FILE_UPLOAD_URL, SLACK_POST_MESSAGE_URL } from './constants'; 5 | import { getExtensionFromFileName } from './utils'; 6 | 7 | interface PostMessageToSlack { 8 | slackBotToken: string; 9 | channelId: string; 10 | text: string; 11 | threadTs?: string; 12 | } 13 | 14 | interface UploadCodeToSlack { 15 | slackBotToken: string; 16 | channelId: string; 17 | code: string; 18 | codeName: string; 19 | initialComment: string; 20 | language?: Languages; 21 | } 22 | 23 | export const postMessageToSlackAPI = async ({ 24 | text, 25 | slackBotToken, 26 | channelId, 27 | threadTs, 28 | }: PostMessageToSlack) => { 29 | const [error, response] = await to( 30 | fetch(SLACK_POST_MESSAGE_URL, { 31 | method: 'POST', 32 | headers: { 33 | Authorization: `Bearer ${slackBotToken}`, 34 | 'Content-type': 'application/json', 35 | }, 36 | body: JSON.stringify({ 37 | channel: channelId, 38 | thread_ts: threadTs || '', 39 | text, 40 | }), 41 | }), 42 | ); 43 | 44 | return { error, response }; 45 | }; 46 | 47 | const isLanguageExtType = (ext: string): ext is keyof typeof Languages => 48 | Object.keys(Languages).includes(ext); 49 | 50 | export const uploadCodeToSlackAPI = async ({ 51 | codeName, 52 | code, 53 | language, 54 | initialComment, 55 | slackBotToken, 56 | channelId, 57 | }: UploadCodeToSlack) => { 58 | const params = new URLSearchParams(); 59 | const fileExt = getExtensionFromFileName(codeName) || 'text'; 60 | const fileExtInLanguage = isLanguageExtType(fileExt) ? fileExt : 'text'; 61 | 62 | params.append('channels', channelId); 63 | params.append('filetype', language || Languages[fileExtInLanguage]); 64 | params.append('filename', codeName); 65 | params.append('title', codeName); 66 | params.append('initial_comment', initialComment); 67 | params.append('content', code); 68 | 69 | const [error, response] = await to( 70 | fetch(SLACK_POST_FILE_UPLOAD_URL, { 71 | method: 'POST', 72 | headers: { 73 | Authorization: `Bearer ${slackBotToken}`, 74 | 'Content-type': 'application/x-www-form-urlencoded', 75 | }, 76 | body: params, 77 | }), 78 | ); 79 | return { error, response }; 80 | }; 81 | -------------------------------------------------------------------------------- /core/server/src/slack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SLACK_API_BASE_URL = 'https://slack.com/api'; 2 | export const SLACK_POST_MESSAGE_URL = `${SLACK_API_BASE_URL}/chat.postMessage`; 3 | export const SLACK_POST_FILE_UPLOAD_URL = `${SLACK_API_BASE_URL}/files.upload`; 4 | 5 | // https://api.slack.com/types/file#file_types 6 | export const Languages = { 7 | js: 'javascript', 8 | jsx: 'javascript', 9 | ts: 'typescript', 10 | tsx: 'typescript', 11 | go: 'go', 12 | java: 'java', 13 | css: 'css', 14 | html: 'html', 15 | markdown: 'markdown', 16 | ml: 'ocaml', 17 | py: 'python', 18 | rs: 'rust', 19 | sh: 'shell', 20 | text: 'text', 21 | } as const; 22 | // eslint-disable-next-line no-redeclare 23 | export type Languages = typeof Languages[keyof typeof Languages]; 24 | -------------------------------------------------------------------------------- /core/server/src/slack/utils.ts: -------------------------------------------------------------------------------- 1 | export const changeFirstToUpperCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 2 | 3 | export const getExtensionFromFileName = (fileName: string) => 4 | fileName.includes('.') ? fileName.split('.').pop() : null; 5 | -------------------------------------------------------------------------------- /core/server/src/updateStory.ts: -------------------------------------------------------------------------------- 1 | import { updateStoryRequestValidate, UpdateStoryResponse } from '@codepocket/schema'; 2 | import { StoryId, StoryIdWithCode } from 'types'; 3 | 4 | export interface UpdateStoryType { 5 | /* validate에러 */ 6 | validateError?: Response; 7 | /* 존재하지 스토리 없다면 에러 */ 8 | existStoryErrorFunc: () => Response; 9 | /* 성공했을 경우 */ 10 | successResponseFunc: (body: UpdateStoryResponse) => Response; 11 | 12 | /* 스토리가 존재하는지 체크하는 함수 */ 13 | isStoryExist: (param: StoryId) => Promise; 14 | /* 스토리 수정 함수 */ 15 | updateStory: (param: StoryIdWithCode) => Promise; 16 | } 17 | 18 | export default async (request: T, modules: UpdateStoryType) => { 19 | if (!updateStoryRequestValidate(request)) throw modules.validateError; 20 | const { storyId, codes } = request.body; 21 | 22 | const existStory = await modules.isStoryExist({ storyId }); 23 | if (!existStory) throw modules.existStoryErrorFunc(); 24 | 25 | await modules.updateStory({ codes, storyId }); 26 | 27 | return modules.successResponseFunc({ message: '' }); 28 | }; 29 | -------------------------------------------------------------------------------- /core/server/src/verifyUser.ts: -------------------------------------------------------------------------------- 1 | import { verifyUserRequestValidate, VerifyUserResponse } from '@codepocket/schema'; 2 | import { PocketToken, UserNameWithId } from 'types'; 3 | 4 | export interface VerifyUserType { 5 | /* validator에러 */ 6 | validateError?: Response; 7 | /* 성공했을 경우 */ 8 | successResponseFunc: (body: VerifyUserResponse) => Response; 9 | 10 | /* 유저 이름을 가져오는 함수 */ 11 | getUserInfo: ({ pocketToken }: PocketToken) => Promise; 12 | } 13 | 14 | export default async (request: T, modules: VerifyUserType) => { 15 | if (!verifyUserRequestValidate(request)) throw modules.validateError; 16 | const { pocketToken } = request.body; 17 | 18 | const { userName } = await modules.getUserInfo({ pocketToken }); 19 | 20 | return modules.successResponseFunc({ validUser: true, userName, message: '' }); 21 | }; 22 | -------------------------------------------------------------------------------- /core/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "exclude": ["./dist"] 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | restart: always 7 | ports: 8 | - '27017:27017' 9 | command: mongod --port 27017 10 | environment: 11 | MONGO_INITDB_ROOT_USERNAME: root 12 | MONGO_INITDB_ROOT_PASSWORD: example 13 | expose: 14 | - '27017' 15 | 16 | app: 17 | build: 18 | dockerfile: Dockerfile.compose 19 | context: ./ 20 | depends_on: 21 | - mongo 22 | ports: 23 | - '3000:3000' 24 | - '8080:8080' 25 | environment: 26 | NODE_ENV: development 27 | VITE_BASE_SERVER_URL: ${VITE_BASE_SERVER_URL} 28 | VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN} 29 | VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID} 30 | VITE_EMAIL_DOMAIN_NAME: ${VITE_EMAIL_DOMAIN_NAME} 31 | SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} 32 | CODEPOCKET_CHANNEL_ID: ${CODEPOCKET_CHANNEL_ID} 33 | CHAPTER_FRONTED_CHANNEL_ID: ${CHAPTER_FRONTED_CHANNEL_ID} 34 | MONGO_DB_URI: ${MONGO_DB_URI} 35 | MONGO_DB_NAME: ${MONGO_DB_NAME} 36 | BASE_SERVER_URL: ${BASE_SERVER_URL} 37 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "cli/*", 4 | "server/*", 5 | "client/*", 6 | "schema/*", 7 | "core/*" 8 | ], 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "version": "independent", 13 | "npmClient": "yarn", 14 | "useWorkspaces": true 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/monorepo", 3 | "private": true, 4 | "license": "Apache-2.0", 5 | "workspaces": [ 6 | "server", 7 | "cli", 8 | "client", 9 | "schema", 10 | "core/*" 11 | ], 12 | "scripts": { 13 | "build": "ultra -r build", 14 | "dev": "ultra -r dev", 15 | "format": "eslint --fix . --ext .ts,.tsx,.json", 16 | "lerna:publish": "yarn build && lerna publish", 17 | "lint": "eslint . --ext .ts,.tsx,.json", 18 | "setting": "yarn && yarn build", 19 | "test": "ultra -r test" 20 | }, 21 | "devDependencies": { 22 | "@types/eslint": "^7", 23 | "@types/prettier": "^2", 24 | "@typescript-eslint/eslint-plugin": "latest", 25 | "@typescript-eslint/parser": "latest", 26 | "eslint": "^7.32.0 || ^8.2.0", 27 | "eslint-config-airbnb-base": "latest", 28 | "eslint-config-prettier": "^8.5.0", 29 | "eslint-plugin-import": "^2.25.2", 30 | "eslint-plugin-json-format": "^2.0.1", 31 | "eslint-plugin-prettier": "^4.2.1", 32 | "eslint-plugin-simple-import-sort": "^7.0.0", 33 | "lerna": "^5.1.6", 34 | "prettier": "^2.7.1", 35 | "typescript": "^4.7.4", 36 | "ultra-runner": "^3.10.5" 37 | }, 38 | "packageManager": "yarn@3.2.1" 39 | } 40 | -------------------------------------------------------------------------------- /schema/.gitignore: -------------------------------------------------------------------------------- 1 | types 2 | index.ts -------------------------------------------------------------------------------- /schema/README.md: -------------------------------------------------------------------------------- 1 | # @codepocket/schema 2 | 3 | > **JSON Schema를 기반으로 CLI, Client, Server의 reqeust, response 타입을 보장해요** 4 | 5 | - JSON Schema를 기반으로 request, response를 파싱해서 타입을 제공해요 6 | - 만들어진 타입을 기반으로 Ajv를 사용해서 validator를 제공해요 7 | - schema 패키지에서 만들어진 모듈들은 프로젝트 전역에서 사용돼요 8 | -------------------------------------------------------------------------------- /schema/compile.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addAsConst, 3 | alignFormat, 4 | capitalizeFront, 5 | changeTypeName, 6 | changeValidatorName, 7 | go, 8 | injectObject, 9 | makeDir, 10 | readDir, 11 | readFile, 12 | reduce, 13 | removeBlank, 14 | removeDir, 15 | removeFileExtension, 16 | replaceExtension, 17 | writeFile, 18 | } from './utils'; 19 | 20 | const SCHEMA_DIR = './json'; 21 | const TYPE_DIR = './types'; 22 | 23 | const generateType = (fileName: string) => { 24 | const template = readFile('./template.ts'); 25 | const json = readFile(`${SCHEMA_DIR}/${fileName}`); 26 | const typeFileName = `${TYPE_DIR}/${capitalizeFront(replaceExtension(fileName, 'ts'))}`; 27 | 28 | go( 29 | template, 30 | injectObject(go(json, removeBlank, addAsConst)), 31 | changeTypeName(go(fileName, removeFileExtension, capitalizeFront)), 32 | changeValidatorName(go(fileName, removeFileExtension)), 33 | alignFormat, 34 | writeFile(typeFileName), 35 | ); 36 | }; 37 | 38 | const generateIndexText = (acc: string, fileName: string) => { 39 | generateType(fileName); 40 | return `${acc}export * from '${TYPE_DIR}/${capitalizeFront(removeFileExtension(fileName))}';\n`; 41 | }; 42 | 43 | (() => { 44 | removeDir(TYPE_DIR); 45 | makeDir(TYPE_DIR); 46 | 47 | go(readDir(SCHEMA_DIR), reduce(generateIndexText, ''), alignFormat, writeFile('./index.ts')); 48 | })(); 49 | -------------------------------------------------------------------------------- /schema/json/createCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "createCodeRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | }, 11 | "codeName": { 12 | "type": "string" 13 | }, 14 | "code": { 15 | "type": "string" 16 | }, 17 | "isAnonymous": { 18 | "type": "boolean" 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "required": [ 23 | "pocketToken", 24 | "codeName", 25 | "code", 26 | "isAnonymous" 27 | ] 28 | } 29 | }, 30 | "additionalProperties": true, 31 | "required": [ 32 | "body" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /schema/json/createCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "createCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/createStoryRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "createStoryRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | }, 11 | "codeId": { 12 | "type": "string" 13 | }, 14 | "storyName": { 15 | "type": "string" 16 | }, 17 | "codes": { 18 | "type": "object", 19 | "additionalProperties": { 20 | "type": "string" 21 | } 22 | } 23 | }, 24 | "additionalProperties": false, 25 | "required": [ 26 | "pocketToken", 27 | "codeId", 28 | "storyName", 29 | "codes" 30 | ] 31 | } 32 | }, 33 | "additionalProperties": true, 34 | "required": [ 35 | "body" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /schema/json/createStoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "createStoryResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | }, 8 | "storyId": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "required": [ 14 | "message", 15 | "storyId" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /schema/json/createUserRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "createUserRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "userName": { 9 | "type": "string" 10 | }, 11 | "email": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "userName", 18 | "email" 19 | ] 20 | } 21 | }, 22 | "additionalProperties": true, 23 | "required": [ 24 | "body" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /schema/json/createUserResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "createUserResponse", 3 | "type": "object", 4 | "properties": { 5 | "pocketToken": { 6 | "type": "string" 7 | }, 8 | "message": { 9 | "type": "string" 10 | }, 11 | "userId": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "pocketToken", 18 | "message", 19 | "userId" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /schema/json/deleteCodeByIdRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "deleteCodeByIdRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | }, 11 | "codeId": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "pocketToken", 18 | "codeId" 19 | ] 20 | } 21 | }, 22 | "additionalProperties": true, 23 | "required": [ 24 | "body" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /schema/json/deleteCodeByIdResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "deleteCodeByIdResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/deleteCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "deleteCodeRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | }, 11 | "codeName": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "pocketToken", 18 | "codeName" 19 | ] 20 | } 21 | }, 22 | "additionalProperties": true, 23 | "required": [ 24 | "body" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /schema/json/deleteCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "deleteCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/deleteStoryRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "deleteStoryRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "codeId": { 9 | "type": "string" 10 | }, 11 | "storyName": { 12 | "type": "string" 13 | }, 14 | "storyAuthor": { 15 | "type": "string" 16 | } 17 | }, 18 | "additionalProperties": false, 19 | "required": [ 20 | "codeId", 21 | "storyName", 22 | "storyAuthor" 23 | ] 24 | } 25 | }, 26 | "additionalProperties": true, 27 | "required": [ 28 | "body" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /schema/json/deleteStoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "deleteStoryResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/getCodeAuthorsRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodeAuthorsRequest", 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "object", 7 | "properties": { 8 | "codeName": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "required": [ 14 | "codeName" 15 | ] 16 | } 17 | }, 18 | "additionalProperties": true, 19 | "required": [ 20 | "query" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /schema/json/getCodeAuthorsResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodeAuthorsResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | }, 8 | "codeAuthors": { 9 | "type": "array", 10 | "items": { 11 | "type": "object", 12 | "properties": { 13 | "codeAuthor": { 14 | "type": "string" 15 | }, 16 | "isAnonymous": { 17 | "type": "boolean" 18 | } 19 | }, 20 | "additionalProperties": false, 21 | "required": [ 22 | "codeAuthor", 23 | "isAnonymous" 24 | ] 25 | } 26 | } 27 | }, 28 | "additionalProperties": false, 29 | "required": [ 30 | "message", 31 | "codeAuthors" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /schema/json/getCodeNamesRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodeNamesRequest", 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "object", 7 | "properties": { 8 | "codeAuthor": { 9 | "type": "string" 10 | }, 11 | "codeName": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": false 16 | } 17 | }, 18 | "additionalProperties": true, 19 | "required": [ 20 | "query" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /schema/json/getCodeNamesResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodeNamesResponse", 3 | "type": "object", 4 | "properties": { 5 | "codeInfos": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "properties": { 10 | "codeName": { 11 | "type": "string" 12 | }, 13 | "codeAuthor": { 14 | "type": "string" 15 | }, 16 | "isAnonymous": { 17 | "type": "boolean" 18 | } 19 | }, 20 | "additionalProperties": false, 21 | "required": [ 22 | "codeName", 23 | "codeAuthor", 24 | "isAnonymous" 25 | ] 26 | } 27 | }, 28 | "message": { 29 | "type": "string" 30 | } 31 | }, 32 | "additionalProperties": false, 33 | "required": [ 34 | "codeInfos", 35 | "message" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /schema/json/getCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodeRequest", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "type": "object", 5 | "properties": { 6 | "query": { 7 | "type": "object", 8 | "properties": { 9 | "codeId": { 10 | "type": "string" 11 | } 12 | }, 13 | "additionalProperties": false, 14 | "required": [ 15 | "codeId" 16 | ] 17 | } 18 | }, 19 | "additionalProperties": true, 20 | "required": [ 21 | "query" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /schema/json/getCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "codeAuthor": { 6 | "type": "string" 7 | }, 8 | "codeName": { 9 | "type": "string" 10 | }, 11 | "code": { 12 | "type": "string" 13 | }, 14 | "isAnonymous": { 15 | "type": "boolean" 16 | }, 17 | "message": { 18 | "type": "string" 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "required": [ 23 | "codeAuthor", 24 | "codeName", 25 | "code", 26 | "message" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /schema/json/getCodesRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodesRequest", 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "object", 7 | "properties": { 8 | "search": { 9 | "type": "string" 10 | }, 11 | "limit": { 12 | "type": "string" 13 | }, 14 | "offset": { 15 | "type": "string" 16 | } 17 | }, 18 | "additionalProperties": false, 19 | "required": [ 20 | "limit", 21 | "offset" 22 | ] 23 | } 24 | }, 25 | "additionalProperties": true, 26 | "required": [ 27 | "query" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /schema/json/getCodesResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getCodesResponse", 3 | "type": "object", 4 | "properties": { 5 | "isLast": { 6 | "type": "boolean" 7 | }, 8 | "message": { 9 | "type": "string" 10 | }, 11 | "codes": { 12 | "type": "array", 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "codeId": { 17 | "type": "string" 18 | }, 19 | "code": { 20 | "type": "string" 21 | }, 22 | "codeAuthor": { 23 | "type": "string" 24 | }, 25 | "codeName": { 26 | "type": "string" 27 | }, 28 | "userId": { 29 | "type": "string" 30 | }, 31 | "createdAt": { 32 | "type": "string", 33 | "format": "date" 34 | }, 35 | "updatedAt": { 36 | "type": "string", 37 | "format": "date" 38 | }, 39 | "isAnonymous": { 40 | "type": "boolean" 41 | } 42 | }, 43 | "additionalProperties": false, 44 | "required": [ 45 | "codeId", 46 | "code", 47 | "codeAuthor", 48 | "codeName", 49 | "userId", 50 | "createdAt", 51 | "updatedAt", 52 | "isAnonymous" 53 | ] 54 | } 55 | } 56 | }, 57 | "additionalProperties": false, 58 | "required": [ 59 | "codes", 60 | "isLast", 61 | "message" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /schema/json/getStoryCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "GetStoryCodeRequest", 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "object", 7 | "properties": { 8 | "storyId": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "required": [ 14 | "storyId" 15 | ] 16 | } 17 | }, 18 | "additionalProperties": true, 19 | "required": [ 20 | "query" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /schema/json/getStoryCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getStoryCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | }, 8 | "codes": { 9 | "type": "object", 10 | "additionalProperties": { 11 | "type": "string" 12 | } 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "message", 18 | "codes" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /schema/json/getStoryNamesRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "GetStoryNamesRequest", 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "object", 7 | "properties": { 8 | "codeId": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "required": [ 14 | "codeId" 15 | ] 16 | } 17 | }, 18 | "additionalProperties": true, 19 | "required": [ 20 | "query" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /schema/json/getStoryNamesResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "getStoryNamesResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | }, 8 | "storyNames": { 9 | "type": "array", 10 | "items": { 11 | "type": "object", 12 | "properties": { 13 | "storyName": { 14 | "type": "string" 15 | }, 16 | "storyId": { 17 | "type": "string" 18 | }, 19 | "userId": { 20 | "type": "string" 21 | } 22 | }, 23 | "additionalProperties": false, 24 | "required": [ 25 | "storyName", 26 | "storyId", 27 | "userId" 28 | ] 29 | } 30 | } 31 | }, 32 | "additionalProperties": false, 33 | "required": [ 34 | "message", 35 | "storyNames" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /schema/json/jwtType.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "pullCodeRequest", 3 | "type": "object", 4 | "properties": { 5 | "serverUrl": { 6 | "type": "string" 7 | }, 8 | "userName": { 9 | "type": "string" 10 | }, 11 | "iat": { 12 | "type": "number" 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "serverUrl", 18 | "userName" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /schema/json/pullCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "pullCodeRequest", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "type": "object", 5 | "properties": { 6 | "query": { 7 | "type": "object", 8 | "properties": { 9 | "codeAuthor": { 10 | "type": "string" 11 | }, 12 | "codeName": { 13 | "type": "string" 14 | } 15 | }, 16 | "additionalProperties": false, 17 | "required": [ 18 | "codeAuthor", 19 | "codeName" 20 | ] 21 | } 22 | }, 23 | "additionalProperties": true, 24 | "required": [ 25 | "query" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /schema/json/pullCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "pullCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "code": { 6 | "type": "string" 7 | }, 8 | "message": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "required": [ 14 | "code", 15 | "message" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /schema/json/pushCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "pushCodeRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | }, 11 | "codeName": { 12 | "type": "string" 13 | }, 14 | "code": { 15 | "type": "string" 16 | }, 17 | "isAnonymous": { 18 | "type": "boolean" 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "required": [ 23 | "pocketToken", 24 | "codeName", 25 | "code", 26 | "isAnonymous" 27 | ] 28 | } 29 | }, 30 | "additionalProperties": true, 31 | "required": [ 32 | "body" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /schema/json/pushCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "pushCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/updateCodeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "updateCodeRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | }, 11 | "codeId": { 12 | "type": "string" 13 | }, 14 | "codeName": { 15 | "type": "string" 16 | }, 17 | "code": { 18 | "type": "string" 19 | }, 20 | "isAnonymous": { 21 | "type": "boolean" 22 | } 23 | }, 24 | "additionalProperties": false, 25 | "required": [ 26 | "pocketToken", 27 | "codeId", 28 | "codeName", 29 | "code", 30 | "isAnonymous" 31 | ] 32 | } 33 | }, 34 | "additionalProperties": true, 35 | "required": [ 36 | "body" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /schema/json/updateCodeResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "updateCodeResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/updateStoryRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "updateStoryRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "storyId": { 9 | "type": "string" 10 | }, 11 | "codes": { 12 | "type": "object", 13 | "additionalProperties": { 14 | "type": "string" 15 | } 16 | } 17 | }, 18 | "additionalProperties": false, 19 | "required": [ 20 | "storyId", 21 | "codes" 22 | ] 23 | } 24 | }, 25 | "additionalProperties": true, 26 | "required": [ 27 | "body" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /schema/json/updateStoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "updateStoryResponse", 3 | "type": "object", 4 | "properties": { 5 | "message": { 6 | "type": "string" 7 | } 8 | }, 9 | "additionalProperties": false, 10 | "required": [ 11 | "message" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /schema/json/uploadSlackFileResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "uploadSlackFileResponse", 3 | "type": "object", 4 | "properties": { 5 | "file": { 6 | "type": "object", 7 | "properties": { 8 | "channels": { 9 | "type": "array", 10 | "items": { 11 | "type": "string" 12 | } 13 | }, 14 | "shares": { 15 | "type": "object", 16 | "properties": { 17 | "public": { 18 | "type": "object", 19 | "additionalProperties": { 20 | "type": "array", 21 | "items": { 22 | "properties": { 23 | "ts": { 24 | "type": "string" 25 | } 26 | }, 27 | "additionalProperties": false, 28 | "required": [ 29 | "ts" 30 | ] 31 | } 32 | } 33 | } 34 | }, 35 | "additionalProperties": false, 36 | "required": [ 37 | "public" 38 | ] 39 | } 40 | }, 41 | "additionalProperties": false, 42 | "required": [ 43 | "channels", 44 | "shares" 45 | ] 46 | } 47 | }, 48 | "additionalProperties": false, 49 | "required": [ 50 | "file" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /schema/json/verifyUserRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VerifyUserRequest", 3 | "type": "object", 4 | "properties": { 5 | "body": { 6 | "type": "object", 7 | "properties": { 8 | "pocketToken": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "required": [ 14 | "pocketToken" 15 | ] 16 | } 17 | }, 18 | "additionalProperties": true, 19 | "required": [ 20 | "body" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /schema/json/verifyUserResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VerifyUserResponse", 3 | "type": "object", 4 | "properties": { 5 | "validUser": { 6 | "type": "boolean" 7 | }, 8 | "userName": { 9 | "type": "string" 10 | }, 11 | "message": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": false, 16 | "required": [ 17 | "validUser", 18 | "userName", 19 | "message" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/schema", 3 | "version": "0.0.4", 4 | "description": "", 5 | "homepage": "https://github.com/daangn/codepocket#readme", 6 | "bugs": { 7 | "url": "https://github.com/daangn/codepocket/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/daangn/codepocket.git" 12 | }, 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "scripts": { 16 | "build": "yarn compile && yarn tsc", 17 | "compile": "yarn node -r tsm ./compile.ts" 18 | }, 19 | "dependencies": { 20 | "ajv": "^8.11.0", 21 | "prettier": "^2.7.1" 22 | }, 23 | "devDependencies": { 24 | "@types/prettier": "^2", 25 | "json-schema-to-ts": "^2.5.5", 26 | "tsm": "^2.2.2", 27 | "typescript": "^4.7.4" 28 | }, 29 | "packageManager": "yarn@3.2.1", 30 | "publishConfig": { 31 | "access": "public" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /schema/template.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { JSONSchemaType } from 'ajv'; 2 | import { FromSchema } from 'json-schema-to-ts'; 3 | 4 | const ajv = new Ajv({ formats: { date: true, time: true } }); 5 | 6 | // schema로 바뀝니다 7 | const t = {}; 8 | 9 | // api이름으로 바뀝니다 10 | export type Self = FromSchema< 11 | typeof t, 12 | { 13 | deserialize: [ 14 | { 15 | pattern: { 16 | type: 'string'; 17 | format: 'date'; 18 | }; 19 | output: Date; 20 | }, 21 | ]; 22 | } 23 | >; 24 | 25 | // validate이름으로 바뀝니다 26 | export const selfValidate = ajv.compile(t as unknown as JSONSchemaType); 27 | -------------------------------------------------------------------------------- /schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "baseUrl": ".", 6 | "outDir": "dist", 7 | "skipLibCheck": true, 8 | }, 9 | "include": ["./index.ts"], 10 | "exclude": ["**.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /schema/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import prettier from 'prettier'; 3 | 4 | const TEMPLATE_TYPE_NAME = 'Self'; 5 | const TEMPLATE_VALIDATOR_NAME = 'self'; 6 | 7 | const typeRegex = new RegExp(TEMPLATE_TYPE_NAME, 'g'); 8 | const typeValidator = new RegExp(TEMPLATE_VALIDATOR_NAME, 'g'); 9 | 10 | export const removeFileExtension = (fileName: string) => fileName.split('.')[0]; 11 | export const capitalizeFront = (text: string) => text[0].toUpperCase() + text.slice(1); 12 | export const replaceExtension = (fileName: string, extension: string) => 13 | `${removeFileExtension(fileName)}.${extension}`; 14 | 15 | export const removeBlank = (text: string) => text.replace(/\s/g, ''); 16 | export const addAsConst = (text: string) => `${text}as const`; 17 | export const injectObject = (target: string) => (text: string) => text.replace(/\{\}/, target); 18 | export const changeTypeName = (name: string) => (text: string) => text.replace(typeRegex, name); 19 | export const changeValidatorName = (name: string) => (text: string) => 20 | text.replace(typeValidator, name); 21 | export const writeFile = (fileName: string) => (text: string) => fs.writeFileSync(fileName, text); 22 | 23 | export const makeDir = (path: string) => fs.mkdirSync(path); 24 | export const readDir = (path: string) => fs.readdirSync(path); 25 | export const removeDir = (path: string) => fs.rmSync(path, { recursive: true, force: true }); 26 | export const readFile = (path: string) => fs.readFileSync(path).toString(); 27 | 28 | export const alignFormat = (text: string) => prettier.format(text, { parser: 'babel-ts' }); 29 | 30 | export const go = (...args: any[]) => args.reduce((acc, cur) => cur(acc)); 31 | export const reduce = 32 | (f: (a: T, b: S) => T, ac: T) => 33 | (i: any[]) => 34 | i.reduce(f, ac); 35 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # @codepocket/server 2 | 3 | > **client, cli에서 보낸 request에 대한 response를 보내주는 역할을 해요** 4 | 5 | - fastify와 mongodb를 사용하고 있어요 6 | - mongodb를 사용해서 저장한 데이터를 사용하고 있어요 7 | - @codepocket/core-server 패키지에서 핵심 로직들을 가져와서 사용해요 8 | - request에 대한 response를 보내주는 서버 역할을 해요 9 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codepocket/server", 3 | "version": "0.0.3", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/daangn/codepocket.git" 8 | }, 9 | "scripts": { 10 | "dev": "NODE_ENV=development nodemon --exec ts-node --files ./src/index.ts", 11 | "start": "ts-node --files ./src/index.ts" 12 | }, 13 | "dependencies": { 14 | "@codepocket/core-server": "^0.0.3", 15 | "@codepocket/schema": "^0.0.4", 16 | "@fastify/cors": "^8.0.0", 17 | "@fastify/mongodb": "^6.0.1", 18 | "@fastify/swagger": "^7.5.1", 19 | "await-to-js": "^3.0.0", 20 | "dotenv-safe": "^8.2.0", 21 | "fastify": "^4.3.0", 22 | "fastify-plugin": "^4.0.0", 23 | "jsonwebtoken": "^8.5.1", 24 | "mongoose": "^6.4.6", 25 | "node-fetch": "2", 26 | "string-hash": "^1.1.3" 27 | }, 28 | "devDependencies": { 29 | "@types/dotenv-safe": "^8", 30 | "@types/jsonwebtoken": "^8", 31 | "@types/node": "^18.0.6", 32 | "@types/string-hash": "^1", 33 | "nodemon": "^2.0.19", 34 | "ts-node": "^10.9.1" 35 | }, 36 | "packageManager": "yarn@3.2.1" 37 | } 38 | -------------------------------------------------------------------------------- /server/src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | import { Code, Story, User } from '../schema'; 4 | 5 | declare module 'fastify' { 6 | export interface FastifyInstance { 7 | store: { 8 | User: mongoose.Model; 9 | Code: mongoose.Model; 10 | Story: mongoose.Model; 11 | db: typeof mongoose; 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv-safe'; 2 | import path from 'path'; 3 | 4 | (() => { 5 | const result = dotenv.config({ 6 | path: path.join(__dirname, '../..', '.env'), 7 | example: path.join(__dirname, '../..', process.env.CI ? '.env.ci.example' : '.env.example'), 8 | // allowEmptyValues: true, 9 | }); 10 | if (!result.parsed) throw new Error('.env 환경변수 파일 읽기를 실패했어요'); 11 | })(); 12 | -------------------------------------------------------------------------------- /server/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SLACK_API_BASE_URL = 'https://slack.com/api'; 2 | export const SLACK_POST_MESSAGE_URL = `${SLACK_API_BASE_URL}/chat.postMessage`; 3 | export const SLACK_POST_FILE_UPLOAD_URL = `${SLACK_API_BASE_URL}/files.upload`; 4 | 5 | // https://api.slack.com/types/file#file_types 6 | export const Languages = { 7 | js: 'javascript', 8 | jsx: 'javascript', 9 | ts: 'typescript', 10 | tsx: 'typescript', 11 | go: 'go', 12 | java: 'java', 13 | css: 'css', 14 | html: 'html', 15 | markdown: 'markdown', 16 | ml: 'ocaml', 17 | py: 'python', 18 | rs: 'rust', 19 | sh: 'shell', 20 | text: 'text', 21 | } as const; 22 | // eslint-disable-next-line no-redeclare 23 | export type Languages = typeof Languages[keyof typeof Languages]; 24 | -------------------------------------------------------------------------------- /server/src/dbModule/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify'; 2 | 3 | import * as codeModules from './code'; 4 | import * as storyModules from './story'; 5 | import * as userModules from './user'; 6 | 7 | type CurryingFunc = (server: FastifyInstance) => (param: any) => any; 8 | type Modules = { [key in keyof T]: CurryingFunc }; 9 | type UpdatedModules = { [key in keyof T]: ReturnType }; 10 | 11 | const parse = (server: FastifyInstance, modules: Modules) => { 12 | return Object.keys(modules).reduce((acc, key: string) => { 13 | acc[key] = modules[key](server); 14 | return acc; 15 | }, {} as UpdatedModules); 16 | }; 17 | 18 | export default (server: FastifyInstance) => ({ 19 | CodeModule: parse(server, codeModules), 20 | StoryModule: parse(server, storyModules), 21 | UserModule: parse(server, userModules), 22 | }); 23 | -------------------------------------------------------------------------------- /server/src/dbModule/user.ts: -------------------------------------------------------------------------------- 1 | import { Types } from '@codepocket/core-server'; 2 | import { JwtType } from '@codepocket/schema'; 3 | import { to } from 'await-to-js'; 4 | import { FastifyInstance } from 'fastify'; 5 | import jwt from 'jsonwebtoken'; 6 | 7 | import { env } from '../utils/env'; 8 | import { CustomResponse } from '../utils/responseHandler'; 9 | 10 | export const findAuthor = (server: FastifyInstance) => async (token: string) => { 11 | const [findAuthorError, author] = await to( 12 | (async () => await server.store.User.findOne({ token }))(), 13 | ); 14 | 15 | if (findAuthorError) throw new CustomResponse({ customStatus: 5000 }); 16 | if (!author) throw new CustomResponse({ customStatus: 4000 }); 17 | return author; 18 | }; 19 | 20 | export const getUserInfo = 21 | (server: FastifyInstance) => 22 | async ({ pocketToken }: Types.PocketToken) => { 23 | const author = await findAuthor(server)(pocketToken); 24 | return { userName: author.userName, userId: author.id }; 25 | }; 26 | 27 | export const getUserPrivateInfo = 28 | (server: FastifyInstance) => 29 | async ({ userName, email }: Types.UserInfo) => { 30 | const [findAuthorError, author] = await to( 31 | (async () => await server.store.User.findOne({ userName, email }))(), 32 | ); 33 | 34 | if (findAuthorError) throw new CustomResponse({ customStatus: 5000 }); 35 | return author ? { pocketToken: author.token, userId: author.id } : null; 36 | }; 37 | 38 | export const createUser = 39 | (server: FastifyInstance) => 40 | async ({ userName, email }: Types.UserInfo) => { 41 | // NOTE: KEY값은 cli/lib/utils.ts의 KEY변수와 동일하게 맞춰주세요 42 | const KEY = 'key'; 43 | const encoded = { userName, serverUrl: env.SELF_URL } as JwtType; 44 | const token = jwt.sign(encoded, KEY); 45 | 46 | const [createUserError, createUserResponse] = await to( 47 | (async () => await server.store.User.create({ userName, email, token }))(), 48 | ); 49 | if (createUserError) throw new CustomResponse({ customStatus: 5000 }); 50 | 51 | return { pocketToken: token, userId: createUserResponse.id }; 52 | }; 53 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import './config'; 2 | 3 | import cors from '@fastify/cors'; 4 | import swagger from '@fastify/swagger'; 5 | import { to } from 'await-to-js'; 6 | import fastify, { FastifyInstance as H } from 'fastify'; 7 | import { IncomingMessage, Server, ServerResponse } from 'http'; 8 | import mongoose from 'mongoose'; 9 | 10 | import route from './router'; 11 | import { CodeSchema, StorySchema, UserSchema } from './schema'; 12 | import { env } from './utils/env'; 13 | 14 | const PORT = Number(process.env.PORT) || 8080; 15 | const server: H = fastify({ logger: true }); 16 | 17 | const init = async () => { 18 | await server.register(swagger, { 19 | exposeRoute: true, 20 | routePrefix: '/docs', 21 | swagger: { 22 | info: { title: 'codepocket.server.api', version: '0.0.0' }, 23 | }, 24 | }); 25 | server.register(cors, () => { 26 | return (__, callback) => { 27 | const corsOptions = { origin: true }; 28 | callback(null, corsOptions); 29 | }; 30 | }); 31 | server.register(route); 32 | server.register(async () => { 33 | const [err, connection] = await to( 34 | mongoose.connect(env.MONGO_DB_URI || '', { dbName: env.MONGO_DB_NAME }), 35 | ); 36 | if (err) throw err; 37 | server.decorate('store', { 38 | User: connection.model('User', UserSchema), 39 | Code: connection.model('Code', CodeSchema), 40 | Story: connection.model('Story', StorySchema), 41 | db: connection, 42 | }); 43 | }); 44 | 45 | const start = async () => { 46 | const [err] = await to(server.listen({ port: PORT, host: '0.0.0.0' })); 47 | if (err) { 48 | server.log.error(err); 49 | process.exit(1); 50 | } 51 | }; 52 | 53 | start(); 54 | }; 55 | 56 | (async () => { 57 | await init(); 58 | })(); 59 | -------------------------------------------------------------------------------- /server/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | 3 | export interface User { 4 | userName: string; 5 | email: string; 6 | token: string; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | } 10 | 11 | export interface Code { 12 | code: string; 13 | codeName: string; 14 | codeAuthor: string; 15 | userId: string; 16 | isAnonymous: boolean; 17 | uploadedChatChannel?: string; 18 | uploadedChatTimeStamp?: string; 19 | createdAt: Date; 20 | updatedAt: Date; 21 | } 22 | 23 | export interface Story { 24 | codes: string; 25 | codeId: string; 26 | userId: string; 27 | codeName: string; 28 | codeAuthor: string; 29 | storyName: string; 30 | storyAuthor: string; 31 | createdAt: Date; 32 | updatedAt: Date; 33 | } 34 | 35 | export const UserSchema = new Schema({ 36 | userName: { 37 | type: String, 38 | index: true, 39 | unique: true, 40 | }, 41 | email: { 42 | type: String, 43 | index: true, 44 | unique: true, 45 | }, 46 | token: { 47 | type: String, 48 | index: true, 49 | unique: true, 50 | }, 51 | createdAt: Date, 52 | updatedAt: Date, 53 | }); 54 | 55 | export const CodeSchema = new Schema({ 56 | code: String, 57 | codeName: String, 58 | codeAuthor: String, 59 | userId: String, 60 | isAnonymous: Boolean, 61 | uploadedChatChannel: String, 62 | uploadedChatTimeStamp: String, 63 | createdAt: Date, 64 | updatedAt: Date, 65 | }); 66 | 67 | export const StorySchema = new Schema({ 68 | codes: String, 69 | codeId: String, 70 | codeName: String, 71 | codeAuthor: String, 72 | userId: String, 73 | storyName: String, 74 | storyAuthor: String, 75 | createdAt: Date, 76 | updatedAt: Date, 77 | }); 78 | -------------------------------------------------------------------------------- /server/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const env = { 2 | SELF_URL: process.env.BASE_SERVER_URL, 3 | WEB_URL: process.env.BASE_WEB_URL, 4 | MONGO_DB_URI: process.env.MONGO_DB_URI, 5 | MONGO_DB_NAME: process.env.MONGO_DB_NAME, 6 | SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN, 7 | CODEPOCKET_CHANNEL_ID: process.env.CODEPOCKET_CHANNEL_ID, 8 | CHAPTER_FRONTED_CHANNEL_ID: process.env.CHAPTER_FRONTED_CHANNEL_ID, 9 | } as const; 10 | 11 | export const checkSlackPossible = 12 | env.SLACK_BOT_TOKEN && env.CHAPTER_FRONTED_CHANNEL_ID && env.CODEPOCKET_CHANNEL_ID; 13 | -------------------------------------------------------------------------------- /server/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const filterDuplicate = (strings: string[]): string[] => [...new Set(strings)]; 2 | 3 | export const getExtensionFromFileName = (fileName: string) => 4 | fileName.includes('.') ? fileName.split('.').pop() : null; 5 | 6 | export const changeFirstToUpperCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 7 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es2021", 5 | "lib": ["es2021"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "typeRoots": ["types"], 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "checkJs": true, 13 | "noEmit": true 14 | }, 15 | "include": ["src", "@types"], 16 | "exclude": ["node_modules", "dist", "test"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "declaration": true, 8 | "downlevelIteration": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "skipLibCheck": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------