From fb51510fbf5e7cfc77f289cbcf36034d91d9ae90 Mon Sep 17 00:00:00 2001 From: Ubinquitous Date: Mon, 22 Apr 2024 19:57:45 +0900 Subject: [PATCH] initial setting --- .circleci/config.yml | 45 + .editorconfig | 4 + .eslintignore | 1 + .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 38 + .github/CONTRIBUTING.md | 52 + .github/ISSUE_TEMPLATE/bug.yml | 17 + .github/ISSUE_TEMPLATE/feature_request.yml | 24 + .github/PULL_REQUEST_TEMPLATE.md | 9 + .github/labeler.yml | 43 + .github/workflows/labeler.yml | 14 + .gitignore | 40 + .husky/pre-commit | 4 + .husky/pre-push | 4 + .npmrc | 1 + .nvmrc | 1 + .prettierrc | 11 + .vscode/settings.json | 7 + LICENSE | 21 + README.md | 81 + apps/wiki/.eslintrc.cjs | 5 + apps/wiki/apis/header.ts | 14 + apps/wiki/apis/index.ts | 24 + apps/wiki/app/(docs)/[classify]/DocsList.tsx | 53 + apps/wiki/app/(docs)/[classify]/page.tsx | 33 + apps/wiki/app/(docs)/[classify]/style.css.ts | 69 + apps/wiki/app/(docs)/docs/[title]/Docs.tsx | 103 + apps/wiki/app/(docs)/docs/[title]/page.tsx | 53 + .../wiki/app/(docs)/docs/[title]/style.css.ts | 74 + apps/wiki/app/(docs)/teacher/page.tsx | 29 + apps/wiki/app/(user)/ContritbuteDocsList.tsx | 26 + apps/wiki/app/(user)/LikeDocsList.tsx | 23 + apps/wiki/app/(user)/mypage/MyPage.tsx | 45 + apps/wiki/app/(user)/mypage/page.tsx | 13 + apps/wiki/app/(user)/style.css.ts | 37 + apps/wiki/app/(user)/user/[id]/User.tsx | 26 + apps/wiki/app/(user)/user/[id]/page.tsx | 36 + apps/wiki/app/coin/Coin.tsx | 265 + apps/wiki/app/coin/Graph.tsx | 126 + apps/wiki/app/coin/TradeHistory.css.ts | 74 + apps/wiki/app/coin/TradeHistory.tsx | 71 + apps/wiki/app/coin/page.tsx | 28 + apps/wiki/app/coin/rank/CoinRanking.tsx | 68 + apps/wiki/app/coin/rank/page.tsx | 18 + apps/wiki/app/coin/rank/style.css.ts | 92 + apps/wiki/app/coin/style.css.ts | 185 + .../coin/trade/[accountId]/TradeHistory.tsx | 51 + apps/wiki/app/coin/trade/[accountId]/page.tsx | 29 + apps/wiki/app/create/page.tsx | 14 + apps/wiki/app/edit/[title]/page.tsx | 36 + apps/wiki/app/getQueryClient.tsx | 6 + apps/wiki/app/globals.css | 168 + apps/wiki/app/history/[title]/History.tsx | 39 + .../[title]/detail/[id]/HistoryDetail.tsx | 56 + .../app/history/[title]/detail/[id]/page.tsx | 33 + .../history/[title]/detail/[id]/style.css.ts | 54 + apps/wiki/app/history/[title]/page.tsx | 33 + apps/wiki/app/history/[title]/style.css.ts | 36 + apps/wiki/app/layout.css.ts | 35 + apps/wiki/app/layout.tsx | 49 + apps/wiki/app/meta-img.png | Bin 0 -> 11323 bytes apps/wiki/app/not-found.tsx | 17 + apps/wiki/app/oauth/OAuth.tsx | 26 + apps/wiki/app/oauth/page.tsx | 18 + apps/wiki/app/oauth/style.css.ts | 14 + apps/wiki/app/page.css.ts | 137 + apps/wiki/app/page.tsx | 105 + apps/wiki/app/providers.tsx | 40 + .../app/search/[keyword]/SearchNotFound.tsx | 17 + .../app/search/[keyword]/SearchResult.tsx | 65 + apps/wiki/app/search/[keyword]/page.tsx | 32 + apps/wiki/app/search/[keyword]/style.css.ts | 88 + .../wiki/components/(modal)/Confirm/index.tsx | 35 + apps/wiki/components/(modal)/Modal/index.tsx | 13 + apps/wiki/components/(modal)/style.css.ts | 54 + apps/wiki/components/Accordion/index.tsx | 19 + apps/wiki/components/Accordion/style.css.ts | 7 + apps/wiki/components/Aside/Aside.tsx | 65 + apps/wiki/components/Aside/index.tsx | 18 + apps/wiki/components/Aside/style.css.ts | 85 + apps/wiki/components/Board/index.tsx | 28 + apps/wiki/components/Board/style.css.ts | 54 + apps/wiki/components/Container/index.tsx | 90 + apps/wiki/components/Container/style.css.ts | 88 + apps/wiki/components/DragDropUpload/index.tsx | 83 + .../components/DragDropUpload/style.css.ts | 21 + .../wiki/components/Editor/DocsExample.css.ts | 80 + apps/wiki/components/Editor/DocsExample.tsx | 88 + apps/wiki/components/Editor/Editor.tsx | 221 + apps/wiki/components/Editor/index.tsx | 22 + apps/wiki/components/Editor/style.css.ts | 188 + apps/wiki/components/Footer/index.tsx | 19 + apps/wiki/components/Footer/style.css.ts | 26 + apps/wiki/components/FrameEditor/index.tsx | 330 + apps/wiki/components/FrameEditor/style.css.ts | 181 + apps/wiki/components/FrameEncoder/index.tsx | 55 + apps/wiki/components/Header/index.tsx | 82 + apps/wiki/components/Header/style.css.ts | 130 + apps/wiki/components/PasteUpload/index.tsx | 36 + apps/wiki/components/Popular/Popular.tsx | 46 + apps/wiki/components/Popular/index.tsx | 17 + apps/wiki/components/Popular/keyframes.css.ts | 91 + apps/wiki/components/Popular/style.css.ts | 84 + apps/wiki/components/ScrollButton/index.tsx | 43 + .../wiki/components/ScrollButton/style.css.ts | 24 + apps/wiki/components/Toastify/index.tsx | 13 + apps/wiki/components/Toastify/style.css.ts | 29 + apps/wiki/config/index.ts | 225 + apps/wiki/constants/error.constant.ts | 15 + apps/wiki/constants/index.ts | 2 + apps/wiki/constants/token.constant.ts | 6 + apps/wiki/context/index.ts | 17 + apps/wiki/enum/editorType.enum.ts | 8 + apps/wiki/enum/index.ts | 2 + apps/wiki/enum/versionDifferent.enum.ts | 7 + apps/wiki/hooks/index.ts | 3 + apps/wiki/hooks/useDate.ts | 26 + apps/wiki/hooks/useModal.tsx | 37 + apps/wiki/hooks/useUser.ts | 32 + apps/wiki/next-env.d.ts | 5 + apps/wiki/next.config.mjs | 27 + apps/wiki/package.json | 49 + apps/wiki/public/assets/arrow_down.svg | 3 + apps/wiki/public/assets/arrow_right.svg | 3 + apps/wiki/public/assets/bumacoin.png | Bin 0 -> 39565 bytes apps/wiki/public/assets/bumamoney.png | Bin 0 -> 19287 bytes apps/wiki/public/assets/favicon.ico | Bin 0 -> 1150 bytes apps/wiki/public/assets/info.png | Bin 0 -> 96103 bytes apps/wiki/public/assets/logo.png | Bin 0 -> 14207 bytes apps/wiki/public/assets/logoBlack.png | Bin 0 -> 3665 bytes apps/wiki/public/assets/meta-img.png | Bin 0 -> 11323 bytes apps/wiki/public/assets/references.png | Bin 0 -> 938892 bytes apps/wiki/public/assets/school.jpeg | Bin 0 -> 2742014 bytes apps/wiki/public/assets/tier/1.png | Bin 0 -> 9477 bytes apps/wiki/public/assets/tier/2.png | Bin 0 -> 7565 bytes apps/wiki/public/assets/tier/3.png | Bin 0 -> 6865 bytes apps/wiki/public/assets/tier/4.png | Bin 0 -> 5573 bytes apps/wiki/public/assets/tier/5.png | Bin 0 -> 5433 bytes apps/wiki/public/assets/tier/6.png | Bin 0 -> 5111 bytes apps/wiki/public/assets/tier/7.png | Bin 0 -> 3980 bytes apps/wiki/public/assets/tier/8.png | Bin 0 -> 3908 bytes apps/wiki/public/assets/tier/9.png | Bin 0 -> 2972 bytes apps/wiki/public/favicon.ico | Bin 0 -> 1150 bytes apps/wiki/record/classify.record.ts | 29 + apps/wiki/record/index.ts | 3 + apps/wiki/record/role.record.ts | 7 + apps/wiki/record/trade.record.ts | 10 + apps/wiki/services/auth/auth.api.ts | 20 + apps/wiki/services/auth/auth.mutation.ts | 28 + apps/wiki/services/coin/coin.api.ts | 71 + apps/wiki/services/coin/coin.mutation.ts | 45 + apps/wiki/services/coin/coin.query.ts | 37 + apps/wiki/services/docs/docs.api.ts | 56 + apps/wiki/services/docs/docs.mutation.ts | 31 + apps/wiki/services/docs/docs.query.ts | 35 + apps/wiki/services/history/history.api.ts | 11 + apps/wiki/services/history/history.query.ts | 16 + apps/wiki/services/like/like.api.ts | 24 + apps/wiki/services/like/like.mutation.ts | 14 + apps/wiki/services/like/like.query.ts | 15 + apps/wiki/services/user/user.api.ts | 17 + apps/wiki/services/user/user.query.ts | 20 + apps/wiki/storage/index.ts | 25 + apps/wiki/styles/document.css.ts | 114 + apps/wiki/styles/flex.ts | 56 + apps/wiki/styles/font.ts | 43 + apps/wiki/styles/index.ts | 4 + apps/wiki/styles/screen.ts | 6 + apps/wiki/styles/theme.ts | 24 + apps/wiki/tsconfig.json | 7 + apps/wiki/types/coinRank.interface.ts | 8 + apps/wiki/types/contribute.interface.ts | 8 + apps/wiki/types/contributeDocs.interface.ts | 9 + apps/wiki/types/createDocsType.interface.ts | 6 + apps/wiki/types/docsItem.interface.ts | 15 + apps/wiki/types/docsList.interface.ts | 6 + apps/wiki/types/docsListItem.interface.ts | 10 + apps/wiki/types/history.interface.ts | 6 + apps/wiki/types/historyList.interface.ts | 7 + apps/wiki/types/index.ts | 13 + apps/wiki/types/modal.interface.ts | 6 + apps/wiki/types/styleVariants.type.ts | 5 + apps/wiki/types/trade.interface.ts | 9 + apps/wiki/types/user.interface.ts | 11 + apps/wiki/utils/autoClosingTag.ts | 48 + apps/wiki/utils/calculateCoinTier.ts | 15 + apps/wiki/utils/documentCompiler.ts | 67 + apps/wiki/utils/generateOpenGraph.ts | 23 + apps/wiki/utils/index.ts | 7 + apps/wiki/utils/isJsonString.ts | 13 + apps/wiki/utils/priceComma.ts | 11 + apps/wiki/utils/tagRemover.ts | 5 + configs/eslint-config-js/.eslintrc.cjs | 5 + configs/eslint-config-js/index.js | 17 + configs/eslint-config-js/no-import.js | 18 + configs/eslint-config-js/package.json | 18 + configs/eslint-config-ts/.eslintrc.cjs | 5 + configs/eslint-config-ts/index.js | 26 + configs/eslint-config-ts/no-import.js | 14 + configs/eslint-config-ts/package.json | 22 + configs/eslint-config/.eslintrc.cjs | 5 + configs/eslint-config/package.json | 24 + configs/eslint-config/react-js.js | 5 + configs/eslint-config/react-ts-no-import.js | 5 + configs/eslint-config/react-ts.js | 5 + configs/eslint-config/react.js | 75 + configs/typescript-config/base.json | 16 + configs/typescript-config/nextjs.json | 28 + configs/typescript-config/package.json | 10 + configs/typescript-config/react-library.json | 11 + package.json | 26 + packages/icon/.eslintrc.cjs | 5 + packages/icon/index.ts | 21 + packages/icon/package.json | 21 + packages/icon/src/AccidentIcon.tsx | 12 + packages/icon/src/ArrowIcon.tsx | 30 + packages/icon/src/CheckIcon.tsx | 12 + packages/icon/src/ClubIcon.tsx | 12 + packages/icon/src/CoinIcon.tsx | 12 + packages/icon/src/FrameIcon.tsx | 14 + packages/icon/src/GithubIcon.tsx | 12 + packages/icon/src/InstagramIcon.tsx | 14 + packages/icon/src/LikeIcon.tsx | 43 + packages/icon/src/Logo.tsx | 13 + packages/icon/src/MyPageIcon.tsx | 12 + packages/icon/src/PageNextIcon.tsx | 12 + packages/icon/src/PagePrevIcon.tsx | 12 + packages/icon/src/PartyIcon.tsx | 14 + packages/icon/src/PopularIcon.tsx | 12 + packages/icon/src/RoundLogo.tsx | 28 + packages/icon/src/ScrollIcon.tsx | 18 + packages/icon/src/SearchIcon.tsx | 28 + packages/icon/src/TeacherIcon.tsx | 12 + packages/icon/src/WalletIcon.tsx | 12 + packages/icon/src/WriteIcon.tsx | 12 + packages/icon/tsconfig.json | 5 + particle.d.ts | 1 + pnpm-lock.yaml | 9453 +++++++++++++++++ pnpm-workspace.yaml | 4 + renovate.json | 23 + turbo.json | 25 + 241 files changed, 17453 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 apps/wiki/.eslintrc.cjs create mode 100644 apps/wiki/apis/header.ts create mode 100644 apps/wiki/apis/index.ts create mode 100644 apps/wiki/app/(docs)/[classify]/DocsList.tsx create mode 100644 apps/wiki/app/(docs)/[classify]/page.tsx create mode 100644 apps/wiki/app/(docs)/[classify]/style.css.ts create mode 100644 apps/wiki/app/(docs)/docs/[title]/Docs.tsx create mode 100644 apps/wiki/app/(docs)/docs/[title]/page.tsx create mode 100644 apps/wiki/app/(docs)/docs/[title]/style.css.ts create mode 100644 apps/wiki/app/(docs)/teacher/page.tsx create mode 100644 apps/wiki/app/(user)/ContritbuteDocsList.tsx create mode 100644 apps/wiki/app/(user)/LikeDocsList.tsx create mode 100644 apps/wiki/app/(user)/mypage/MyPage.tsx create mode 100644 apps/wiki/app/(user)/mypage/page.tsx create mode 100644 apps/wiki/app/(user)/style.css.ts create mode 100644 apps/wiki/app/(user)/user/[id]/User.tsx create mode 100644 apps/wiki/app/(user)/user/[id]/page.tsx create mode 100644 apps/wiki/app/coin/Coin.tsx create mode 100644 apps/wiki/app/coin/Graph.tsx create mode 100644 apps/wiki/app/coin/TradeHistory.css.ts create mode 100644 apps/wiki/app/coin/TradeHistory.tsx create mode 100644 apps/wiki/app/coin/page.tsx create mode 100644 apps/wiki/app/coin/rank/CoinRanking.tsx create mode 100644 apps/wiki/app/coin/rank/page.tsx create mode 100644 apps/wiki/app/coin/rank/style.css.ts create mode 100644 apps/wiki/app/coin/style.css.ts create mode 100644 apps/wiki/app/coin/trade/[accountId]/TradeHistory.tsx create mode 100644 apps/wiki/app/coin/trade/[accountId]/page.tsx create mode 100644 apps/wiki/app/create/page.tsx create mode 100644 apps/wiki/app/edit/[title]/page.tsx create mode 100644 apps/wiki/app/getQueryClient.tsx create mode 100644 apps/wiki/app/globals.css create mode 100644 apps/wiki/app/history/[title]/History.tsx create mode 100644 apps/wiki/app/history/[title]/detail/[id]/HistoryDetail.tsx create mode 100644 apps/wiki/app/history/[title]/detail/[id]/page.tsx create mode 100644 apps/wiki/app/history/[title]/detail/[id]/style.css.ts create mode 100644 apps/wiki/app/history/[title]/page.tsx create mode 100644 apps/wiki/app/history/[title]/style.css.ts create mode 100644 apps/wiki/app/layout.css.ts create mode 100644 apps/wiki/app/layout.tsx create mode 100644 apps/wiki/app/meta-img.png create mode 100644 apps/wiki/app/not-found.tsx create mode 100644 apps/wiki/app/oauth/OAuth.tsx create mode 100644 apps/wiki/app/oauth/page.tsx create mode 100644 apps/wiki/app/oauth/style.css.ts create mode 100644 apps/wiki/app/page.css.ts create mode 100644 apps/wiki/app/page.tsx create mode 100644 apps/wiki/app/providers.tsx create mode 100644 apps/wiki/app/search/[keyword]/SearchNotFound.tsx create mode 100644 apps/wiki/app/search/[keyword]/SearchResult.tsx create mode 100644 apps/wiki/app/search/[keyword]/page.tsx create mode 100644 apps/wiki/app/search/[keyword]/style.css.ts create mode 100644 apps/wiki/components/(modal)/Confirm/index.tsx create mode 100644 apps/wiki/components/(modal)/Modal/index.tsx create mode 100644 apps/wiki/components/(modal)/style.css.ts create mode 100644 apps/wiki/components/Accordion/index.tsx create mode 100644 apps/wiki/components/Accordion/style.css.ts create mode 100644 apps/wiki/components/Aside/Aside.tsx create mode 100644 apps/wiki/components/Aside/index.tsx create mode 100644 apps/wiki/components/Aside/style.css.ts create mode 100644 apps/wiki/components/Board/index.tsx create mode 100644 apps/wiki/components/Board/style.css.ts create mode 100644 apps/wiki/components/Container/index.tsx create mode 100644 apps/wiki/components/Container/style.css.ts create mode 100644 apps/wiki/components/DragDropUpload/index.tsx create mode 100644 apps/wiki/components/DragDropUpload/style.css.ts create mode 100644 apps/wiki/components/Editor/DocsExample.css.ts create mode 100644 apps/wiki/components/Editor/DocsExample.tsx create mode 100644 apps/wiki/components/Editor/Editor.tsx create mode 100644 apps/wiki/components/Editor/index.tsx create mode 100644 apps/wiki/components/Editor/style.css.ts create mode 100644 apps/wiki/components/Footer/index.tsx create mode 100644 apps/wiki/components/Footer/style.css.ts create mode 100644 apps/wiki/components/FrameEditor/index.tsx create mode 100644 apps/wiki/components/FrameEditor/style.css.ts create mode 100644 apps/wiki/components/FrameEncoder/index.tsx create mode 100644 apps/wiki/components/Header/index.tsx create mode 100644 apps/wiki/components/Header/style.css.ts create mode 100644 apps/wiki/components/PasteUpload/index.tsx create mode 100644 apps/wiki/components/Popular/Popular.tsx create mode 100644 apps/wiki/components/Popular/index.tsx create mode 100644 apps/wiki/components/Popular/keyframes.css.ts create mode 100644 apps/wiki/components/Popular/style.css.ts create mode 100644 apps/wiki/components/ScrollButton/index.tsx create mode 100644 apps/wiki/components/ScrollButton/style.css.ts create mode 100644 apps/wiki/components/Toastify/index.tsx create mode 100644 apps/wiki/components/Toastify/style.css.ts create mode 100644 apps/wiki/config/index.ts create mode 100644 apps/wiki/constants/error.constant.ts create mode 100644 apps/wiki/constants/index.ts create mode 100644 apps/wiki/constants/token.constant.ts create mode 100644 apps/wiki/context/index.ts create mode 100644 apps/wiki/enum/editorType.enum.ts create mode 100644 apps/wiki/enum/index.ts create mode 100644 apps/wiki/enum/versionDifferent.enum.ts create mode 100644 apps/wiki/hooks/index.ts create mode 100644 apps/wiki/hooks/useDate.ts create mode 100644 apps/wiki/hooks/useModal.tsx create mode 100644 apps/wiki/hooks/useUser.ts create mode 100644 apps/wiki/next-env.d.ts create mode 100644 apps/wiki/next.config.mjs create mode 100644 apps/wiki/package.json create mode 100644 apps/wiki/public/assets/arrow_down.svg create mode 100644 apps/wiki/public/assets/arrow_right.svg create mode 100644 apps/wiki/public/assets/bumacoin.png create mode 100644 apps/wiki/public/assets/bumamoney.png create mode 100644 apps/wiki/public/assets/favicon.ico create mode 100644 apps/wiki/public/assets/info.png create mode 100644 apps/wiki/public/assets/logo.png create mode 100644 apps/wiki/public/assets/logoBlack.png create mode 100644 apps/wiki/public/assets/meta-img.png create mode 100644 apps/wiki/public/assets/references.png create mode 100644 apps/wiki/public/assets/school.jpeg create mode 100644 apps/wiki/public/assets/tier/1.png create mode 100644 apps/wiki/public/assets/tier/2.png create mode 100644 apps/wiki/public/assets/tier/3.png create mode 100644 apps/wiki/public/assets/tier/4.png create mode 100644 apps/wiki/public/assets/tier/5.png create mode 100644 apps/wiki/public/assets/tier/6.png create mode 100644 apps/wiki/public/assets/tier/7.png create mode 100644 apps/wiki/public/assets/tier/8.png create mode 100644 apps/wiki/public/assets/tier/9.png create mode 100644 apps/wiki/public/favicon.ico create mode 100644 apps/wiki/record/classify.record.ts create mode 100644 apps/wiki/record/index.ts create mode 100644 apps/wiki/record/role.record.ts create mode 100644 apps/wiki/record/trade.record.ts create mode 100644 apps/wiki/services/auth/auth.api.ts create mode 100644 apps/wiki/services/auth/auth.mutation.ts create mode 100644 apps/wiki/services/coin/coin.api.ts create mode 100644 apps/wiki/services/coin/coin.mutation.ts create mode 100644 apps/wiki/services/coin/coin.query.ts create mode 100644 apps/wiki/services/docs/docs.api.ts create mode 100644 apps/wiki/services/docs/docs.mutation.ts create mode 100644 apps/wiki/services/docs/docs.query.ts create mode 100644 apps/wiki/services/history/history.api.ts create mode 100644 apps/wiki/services/history/history.query.ts create mode 100644 apps/wiki/services/like/like.api.ts create mode 100644 apps/wiki/services/like/like.mutation.ts create mode 100644 apps/wiki/services/like/like.query.ts create mode 100644 apps/wiki/services/user/user.api.ts create mode 100644 apps/wiki/services/user/user.query.ts create mode 100644 apps/wiki/storage/index.ts create mode 100644 apps/wiki/styles/document.css.ts create mode 100644 apps/wiki/styles/flex.ts create mode 100644 apps/wiki/styles/font.ts create mode 100644 apps/wiki/styles/index.ts create mode 100644 apps/wiki/styles/screen.ts create mode 100644 apps/wiki/styles/theme.ts create mode 100644 apps/wiki/tsconfig.json create mode 100644 apps/wiki/types/coinRank.interface.ts create mode 100644 apps/wiki/types/contribute.interface.ts create mode 100644 apps/wiki/types/contributeDocs.interface.ts create mode 100644 apps/wiki/types/createDocsType.interface.ts create mode 100644 apps/wiki/types/docsItem.interface.ts create mode 100644 apps/wiki/types/docsList.interface.ts create mode 100644 apps/wiki/types/docsListItem.interface.ts create mode 100644 apps/wiki/types/history.interface.ts create mode 100644 apps/wiki/types/historyList.interface.ts create mode 100644 apps/wiki/types/index.ts create mode 100644 apps/wiki/types/modal.interface.ts create mode 100644 apps/wiki/types/styleVariants.type.ts create mode 100644 apps/wiki/types/trade.interface.ts create mode 100644 apps/wiki/types/user.interface.ts create mode 100644 apps/wiki/utils/autoClosingTag.ts create mode 100644 apps/wiki/utils/calculateCoinTier.ts create mode 100644 apps/wiki/utils/documentCompiler.ts create mode 100644 apps/wiki/utils/generateOpenGraph.ts create mode 100644 apps/wiki/utils/index.ts create mode 100644 apps/wiki/utils/isJsonString.ts create mode 100644 apps/wiki/utils/priceComma.ts create mode 100644 apps/wiki/utils/tagRemover.ts create mode 100644 configs/eslint-config-js/.eslintrc.cjs create mode 100644 configs/eslint-config-js/index.js create mode 100644 configs/eslint-config-js/no-import.js create mode 100644 configs/eslint-config-js/package.json create mode 100644 configs/eslint-config-ts/.eslintrc.cjs create mode 100644 configs/eslint-config-ts/index.js create mode 100644 configs/eslint-config-ts/no-import.js create mode 100644 configs/eslint-config-ts/package.json create mode 100644 configs/eslint-config/.eslintrc.cjs create mode 100644 configs/eslint-config/package.json create mode 100644 configs/eslint-config/react-js.js create mode 100644 configs/eslint-config/react-ts-no-import.js create mode 100644 configs/eslint-config/react-ts.js create mode 100644 configs/eslint-config/react.js create mode 100644 configs/typescript-config/base.json create mode 100644 configs/typescript-config/nextjs.json create mode 100644 configs/typescript-config/package.json create mode 100644 configs/typescript-config/react-library.json create mode 100644 package.json create mode 100644 packages/icon/.eslintrc.cjs create mode 100644 packages/icon/index.ts create mode 100644 packages/icon/package.json create mode 100644 packages/icon/src/AccidentIcon.tsx create mode 100644 packages/icon/src/ArrowIcon.tsx create mode 100644 packages/icon/src/CheckIcon.tsx create mode 100644 packages/icon/src/ClubIcon.tsx create mode 100644 packages/icon/src/CoinIcon.tsx create mode 100644 packages/icon/src/FrameIcon.tsx create mode 100644 packages/icon/src/GithubIcon.tsx create mode 100644 packages/icon/src/InstagramIcon.tsx create mode 100644 packages/icon/src/LikeIcon.tsx create mode 100644 packages/icon/src/Logo.tsx create mode 100644 packages/icon/src/MyPageIcon.tsx create mode 100644 packages/icon/src/PageNextIcon.tsx create mode 100644 packages/icon/src/PagePrevIcon.tsx create mode 100644 packages/icon/src/PartyIcon.tsx create mode 100644 packages/icon/src/PopularIcon.tsx create mode 100644 packages/icon/src/RoundLogo.tsx create mode 100644 packages/icon/src/ScrollIcon.tsx create mode 100644 packages/icon/src/SearchIcon.tsx create mode 100644 packages/icon/src/TeacherIcon.tsx create mode 100644 packages/icon/src/WalletIcon.tsx create mode 100644 packages/icon/src/WriteIcon.tsx create mode 100644 packages/icon/tsconfig.json create mode 100644 particle.d.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 turbo.json diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..c5d35ab --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,45 @@ +# This config was automatically generated from your source code +# Stacks detected: cicd:github-actions:.github/workflows,deps:node:. +version: 2.1 +orbs: + node: circleci/node@5 +jobs: + build-node: + # Build node project + executor: node/default + steps: + - checkout + - node/install-packages: + cache-path: ~/project/node_modules + override-ci-command: npm install + - run: + command: npm run build + - run: + name: Create the ~/artifacts directory if it doesn't exist + command: mkdir -p ~/artifacts + # Copy output to artifacts dir + - run: + name: Copy artifacts + command: cp -R build dist public .output .next .docusaurus ~/artifacts 2>/dev/null || true + - store_artifacts: + path: ~/artifacts + destination: node-build + deploy: + # This is an example deploy job, not actually used by the workflow + docker: + - image: cimg/base:stable + steps: + # Replace this with steps to deploy to users + - run: + name: deploy + command: "#e.g. ./deploy.sh" + - run: + name: found github actions config + command: ":" +workflows: + build: + jobs: + - build-node + # - deploy: + # requires: + # - build-node diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..12c21b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*.tsx] +indent_style = space \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..544edd9 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +.eslintrc.cjs \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ad74536 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ubinquitous @5ewon @jyh071116 \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b1d89ef --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,38 @@ +Contributor Covenant Code of Conduct +Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +Our Standards + +Examples of behavior that contributes to creating a positive environment include: + + Using welcoming and inclusive language + Being respectful of differing viewpoints and experiences + Gracefully accepting constructive criticism + Focusing on what is best for the community + Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + + The use of sexualized language or imagery and unwelcome sexual attention or advances + Trolling, insulting/derogatory comments, and personal or political attacks + Public or private harassment + Publishing others' private information, such as a physical or electronic address, without explicit permission + Other conduct which could reasonably be considered inappropriate in a professional setting + +Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at team-insert@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e359aaf --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# 부마위키에 기여하기 + +우리는 커뮤니티의 모든 사람의 기여를 환영합니다.
+이 레포지토리는 다양한 나라의 언어로 이루어지며, 한국어를 중심으로 이루어집니다. + +> BSM의 모든 기여자는 우리의 행동 강령을 준수해야 합니다. +>
어떤 행동이 허용되고 허용되지 않는지 이해하려면 [전체 내용](./CODE_OF_CONDUCT.md)를 읽어보시기 바랍니다 . + +## 1. 이슈 + +다음을 통해 BSM에 기여할 수 있습니다 : + +- [버그 신고하기](https://github.com/Team-INSERT/bssm-frontend/issues/new/choose) +- [새로운 기능 요청](https://github.com/Team-INSERT/bssm-frontend/issues/issues/new/choose) +- [이미 있는 이슈를 보고](https://github.com/Team-INSERT/bssm-frontend/issues/issues) 수정해야 할 사항을 확인하세요. + +## 2. Pull Requests + +> [PR 시작하기](https://github.com/Team-INSERT/bssm-frontend/compare)
+ +나만의 PR을 올릴 수 있습니다. PR 제목은 다음 형식과 일치해야 합니다. + +``` +(template scope): + +ex) feat(calendar): 바 형식으로 조회하는 기능 추가 +``` + +> 우리는 모든 PR을 메인에 병합하기 때문에 기록의 커밋 수나 스타일에 신경 쓰지 않습니다.
+> 편안하다고 느끼는 스타일로 자유롭게 커밋해주세요. + +### commit keyword + +**유형은 다음 중 하나여야 합니다:** + +src 내 메인 로직을 포함하는 코드를 수정한 경우 : + +- feat - 새로운 기능 추가 +- fix - 새로운 기능을 추가하지 않은 수정사항 + +src 내 메인 로직을 포함하는 코드를 수정하지 않은 경우 : + +- test - 테스트 코드를 작성하거나 변경한 경우 + +그 외 : + +- chore - 기타 모든 것 ( 주석 추가 등등... ) + +### 3. etc + +코드를 작성할 때 부마위키가 추구하는 구조 자체를 건드리는 것은 유쾌하지 않습니다. +만약 더욱 가독성이 좋은 디렉터리 구조를 발견하셨다면 이슈로 제보해주세요. diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..7a6ca13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,17 @@ +name: "Bug" +description: "🤬" +labels: 버그 +body: + - type: textarea + attributes: + label: Describe + description: | + 버그에 대해서 설명해주세요! + placeholder: | + 헤더가 깨지는 버그가 발생해요 + + - type: textarea + attributes: + label: Additional + description: | + 추가로 해주실 말씀이 있나요? diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..6fd896c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,24 @@ +name: "Feature" +description: "추가할 일이 있으신가요? 📗" +body: + - type: textarea + attributes: + label: Describe + description: | + 추가할 일에 관한 설명 + placeholder: | + 요구사항을 작성해주세요 + + - type: textarea + attributes: + label: Work + description: | + [작업내용] 무슨 작업을 하셨나요? + placeholder: | + ~~이런 작업을 했습니다. + + - type: textarea + attributes: + label: Additional + description: | + 추가로 해주실 말씀이 있나요? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a991c22 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + + +## Issue Number + +## What + +## How + +## ScreenShot diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..9478330 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,43 @@ +# labeler "full" schema + +# enable labeler on issues, prs, or both. +enable: + issues: false + prs: true + +comments: + issues: | + Thanks for opening this issue! + I have applied any labels matching special text in your title and description. + + Please review the labels and make any necessary changes. + +# Labels is an object where: +# - keys are labels +# - values are objects of { include: [ pattern ], exclude: [ pattern ] } +# - pattern must be a valid regex, and is applied globally to +# title + description of issues and/or prs (see enabled config above) +# - 'include' patterns will associate a label if any of these patterns match +# - 'exclude' patterns will ignore this label if any of these patterns match +labels: + "fix": + include: + - '\bfix\b' + "refactor": + include: + - '\brefactor\b' + "docs": + include: + - '\bdocs\b' + "chore": + include: + - '\bchore\b' + "feat": + include: + - '\bfeat\b' + "test": + include: + - '\btest\b' + "ci": + include: + - '\bci\b' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..3aa42f4 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,14 @@ +name: Auto Labeler +on: + pull_request_target: + types: [opened] + +jobs: + labeler: + runs-on: ubuntu-latest + steps: + - name: Check Labels + id: labeler + uses: jimschubert/labeler-action@v2 + with: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6985d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +.eslintcache \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..c7bf915 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm lint \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..adb0820 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm run build \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d71e7ed --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +prefer-workspace-packages=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..6792410 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +21.4.0 \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bc3c228 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "tabWidth": 2, + "bracketSpacing": true, + "endOfLine": "auto", + "useTabs": false, + "arrowParens": "always" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..44a73ec --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2d7a70a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Insert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bd5cd9 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Turborepo starter + +This is an official starter Turborepo. + +## Using this example + +Run the following command: + +```sh +npx create-turbo@latest +``` + +## What's inside? + +This Turborepo includes the following packages/apps: + +### Apps and Packages + +- `docs`: a [Next.js](https://nextjs.org/) app +- `web`: another [Next.js](https://nextjs.org/) app +- `@buma/ui`: a stub React component library shared by both `web` and `docs` applications +- `@buma/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) +- `@buma/typescript-config`: `tsconfig.json`s used throughout the monorepo + +Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io) for code formatting + +### Build + +To build all apps and packages, run the following command: + +``` +cd my-turborepo +pnpm build +``` + +### Develop + +To develop all apps and packages, run the following command: + +``` +cd my-turborepo +pnpm dev +``` + +### Remote Caching + +Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. + +By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands: + +``` +cd my-turborepo +npx turbo login +``` + +This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). + +Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: + +``` +npx turbo link +``` + +## Useful Links + +Learn more about the power of Turborepo: + +- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) +- [Caching](https://turbo.build/repo/docs/core-concepts/caching) +- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) +- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering) +- [Configuration Options](https://turbo.build/repo/docs/reference/configuration) +- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference) diff --git a/apps/wiki/.eslintrc.cjs b/apps/wiki/.eslintrc.cjs new file mode 100644 index 0000000..c4a6d6a --- /dev/null +++ b/apps/wiki/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + extends: ["@buma/eslint-config/react"], +}; diff --git a/apps/wiki/apis/header.ts b/apps/wiki/apis/header.ts new file mode 100644 index 0000000..9a5e1c2 --- /dev/null +++ b/apps/wiki/apis/header.ts @@ -0,0 +1,14 @@ +import { TOKEN } from "@/constants"; +import { Storage } from "@/storage"; + +export const authorization = () => ({ + headers: { + Authorization: Storage.getItem(TOKEN.ACCESS), + }, +}); + +export const refreshToken = () => ({ + headers: { + RefreshToken: Storage.getItem(TOKEN.REFRESH), + }, +}); diff --git a/apps/wiki/apis/index.ts b/apps/wiki/apis/index.ts new file mode 100644 index 0000000..c9500d6 --- /dev/null +++ b/apps/wiki/apis/index.ts @@ -0,0 +1,24 @@ +import axios from "axios"; +import { ERROR } from "@/constants"; +import { refresh } from "@/services/auth/auth.api"; + +export const http = axios.create({ + baseURL: process.env.NEXT_PUBLIC_SERVER_URL, + timeout: 10000, +}); + +http.interceptors.response.use( + (response) => response, + async (error) => { + const request = error.config; + const { code } = error.response.data; + const isAccessTokenExpiredError = code === ERROR.TOKEN_403_2; + + if (isAccessTokenExpiredError && !request.sent) { + request.sent = true; + request.headers.Authorization = await refresh(); + return http(request); + } + return Promise.reject(error); + }, +); diff --git a/apps/wiki/app/(docs)/[classify]/DocsList.tsx b/apps/wiki/app/(docs)/[classify]/DocsList.tsx new file mode 100644 index 0000000..6fd96c6 --- /dev/null +++ b/apps/wiki/app/(docs)/[classify]/DocsList.tsx @@ -0,0 +1,53 @@ +"use client"; + +import Accordion from "@/components/Accordion"; +import { FC } from "react"; +import { tagRemover } from "@/utils"; +import { useDate } from "@/hooks"; +import Image from "next/image"; +import Link from "next/link"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { docsQuery } from "@/services/docs/docs.query"; +import Container from "@/components/Container"; +import { CLASSIFY } from "@/record"; +import * as styles from "./style.css"; + +const DocsList: FC<{ classify: string }> = ({ classify }) => { + const { formatDate } = useDate(); + const { data: docsList } = useSuspenseQuery(docsQuery.list(classify)); + const docsType = classify.toUpperCase(); + + return ( + + {docsList.keys.map((key: string) => ( + + {docsList.data[key].map((docs) => ( + +
+
+

{docs.title}

+ + 최근 수정일 ·  + {formatDate(docs.lastModifiedAt)} + +
+

{tagRemover(docs.simpleContents)} ...

+
+ {docs.thumbnail && ( + thumbnail + )} + + ))} +
+ ))} +
+ ); +}; + +export default DocsList; diff --git a/apps/wiki/app/(docs)/[classify]/page.tsx b/apps/wiki/app/(docs)/[classify]/page.tsx new file mode 100644 index 0000000..a5ac887 --- /dev/null +++ b/apps/wiki/app/(docs)/[classify]/page.tsx @@ -0,0 +1,33 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { docsQuery } from "@/services/docs/docs.query"; +import { generateOpenGraph } from "@/utils"; +import { Metadata } from "next"; +import { CLASSIFY } from "@/record"; +import DocsList from "./DocsList"; + +interface PageProps { + params: { + classify: string; + }; +} + +export const generateMetadata = async ({ params: { classify } }: PageProps): Promise => { + return generateOpenGraph({ + title: CLASSIFY[classify.toUpperCase()], + description: `교내의 ${CLASSIFY[classify]}들을 모아둔 페이지입니다.`, + }); +}; + +const Page = async ({ params: { classify } }: PageProps) => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(docsQuery.list(classify)); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/(docs)/[classify]/style.css.ts b/apps/wiki/app/(docs)/[classify]/style.css.ts new file mode 100644 index 0000000..074662a --- /dev/null +++ b/apps/wiki/app/(docs)/[classify]/style.css.ts @@ -0,0 +1,69 @@ +import { flex, font, theme, screen } from "@/styles"; +import { style } from "@vanilla-extract/css"; + +export const container = style({ + borderBottom: `1px solid ${theme.gray}`, + cursor: "pointer", + padding: "0 10px", + ...flex.BETWEEN, + + ":hover": { + opacity: 0.8, + background: theme.hover, + }, +}); + +export const docs = style({ + width: "78%", + height: "100px", + // padding: "0 10px", + gap: "8px", + ...flex.COLUMN_HORIZONTAL, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + width: "100%", + }, + }, +}); + +export const titleBox = style({ + gap: "8px", + ...flex.VERTICAL, +}); + +export const lastModifiedAt = style({ + color: theme.boldgray, + ...font.p2, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + ...font.p4, + }, + }, +}); + +export const title = style({ + color: theme.primary, + ...font.H4, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + ...font.H6, + }, + }, +}); + +export const simpleContents = style({ + ...font.caption, +}); + +export const thumbnail = style({ + objectFit: "cover", + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + display: "none", + }, + }, +}); diff --git a/apps/wiki/app/(docs)/docs/[title]/Docs.tsx b/apps/wiki/app/(docs)/docs/[title]/Docs.tsx new file mode 100644 index 0000000..06ce627 --- /dev/null +++ b/apps/wiki/app/(docs)/docs/[title]/Docs.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { FC, Suspense } from "react"; +import DOMPurify from "isomorphic-dompurify"; +import Link from "next/link"; +import { + useQuery, + useQueryClient, + useSuspenseQueries, + useSuspenseQuery, +} from "@tanstack/react-query"; +import { docsQuery } from "@/services/docs/docs.query"; +import { likeQuery } from "@/services/like/like.query"; +import Container from "@/components/Container"; +import { LikeIcon } from "@buma/icon"; +import { useUser } from "@/hooks"; +import Toastify from "@/components/Toastify"; +import { toast } from "react-toastify"; +import { useCreateLikeMutation, useDeleteLikeMutation } from "@/services/like/like.mutation"; +import FrameEncoder from "@/components/FrameEncoder"; +import { documentCompiler } from "@/utils"; +import { CLASSIFY } from "@/record"; +import { EditorType } from "@/enum"; +import * as styles from "./style.css"; + +const Docs: FC<{ title: string; frameNameList: Array }> = ({ title, frameNameList }) => { + const frameQueryList = frameNameList.map((frame) => docsQuery.title(frame)); + const frameList = useSuspenseQueries({ queries: frameQueryList }).map(({ data }) => data); + const { data: docs } = useSuspenseQuery(docsQuery.title(title)); + const { isLoggedIn } = useUser(); + const queryClient = useQueryClient(); + + const { data: like } = useQuery(likeQuery.likeCount(title)); + const { data: isILike } = useQuery(likeQuery.isILike(docs.id)); + const { mutate: createLike } = useCreateLikeMutation(); + const { mutate: cancelLike } = useDeleteLikeMutation(); + + const handleLikeToggleClick = () => { + const onSuccessToggleLike = () => { + queryClient.invalidateQueries(likeQuery.isILike(docs.id)); + queryClient.invalidateQueries(likeQuery.likeCount(title)); + }; + if (!isLoggedIn) return toast(); + if (isILike) return cancelLike(docs.id, { onSuccess: onSuccessToggleLike }); + createLike(docs.id, { onSuccess: onSuccessToggleLike }); + }; + + const sanitizeData = () => ({ + __html: DOMPurify.sanitize(documentCompiler(docs.contents)), + }); + + return ( + + +
+

+ 문의를 통해 본인 문서의 기재되길 원치않는 특정 내용을 즉시 삭제할 수 있습니다. +
+ 문서 기재로 발생한 이슈에 대해 부마위키 팀은 아무런 책임을 지지 않으며, 수사 기관에 편집 + 기록과 관련된 데이터를 제공할 수 있습니다. +

+ +
+ {/** 문서 분류가 틀이면 틀이 문서 자체이기에 보여주고, 아니라면 틀 리스트 + 문서 */} + {docs.docsType === CLASSIFY.틀 ? ( + + ) : ( + (function DocsComponent() { + return ( + <> + {frameList + .filter((frame) => frame && frame.docsType === CLASSIFY.틀) + .map((frame) => ( + + ))} +
+ + ); + })() + )} +
+

문서 기여자

+
    + {docs.contributors.map((contributor) => ( + + {contributor.nickName} + + ))} +
+
+ + + ); +}; + +export default Docs; diff --git a/apps/wiki/app/(docs)/docs/[title]/page.tsx b/apps/wiki/app/(docs)/docs/[title]/page.tsx new file mode 100644 index 0000000..4b03f3d --- /dev/null +++ b/apps/wiki/app/(docs)/docs/[title]/page.tsx @@ -0,0 +1,53 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { docsQuery } from "@/services/docs/docs.query"; +import { likeQuery } from "@/services/like/like.query"; +import { Metadata } from "next"; +import { generateOpenGraph } from "@/utils"; +import { notFound } from "next/navigation"; +import Docs from "./Docs"; + +interface PageProps { + params: { + title: string; + }; +} + +export const generateMetadata = async ({ params: { title } }: PageProps): Promise => { + try { + const queryClient = getQueryClient(); + const data = await queryClient.fetchQuery(docsQuery.title(title)); + return generateOpenGraph({ + title: data.title, + description: data.contents, + }); + } catch { + // prefetch 단계에서 오류가 발생했다면 해당 문서는 Not Found + notFound(); + } +}; + +const Page = async ({ params: { title } }: PageProps) => { + const queryClient = getQueryClient(); + Promise.all([ + await queryClient.prefetchQuery(docsQuery.title(title)), + await queryClient.prefetchQuery(likeQuery.likeCount(title)), + ]); + /** + * contents에서 frame list를 얻기 위해 값을 반환하는 fetchQuery 사용 + * include();와 match되는 글자 탐색 + * "include();" 삭제 (틀 이름만 남을 수 있도록) + */ + const { contents } = await queryClient.fetchQuery(docsQuery.title(title)); + const matchesFrameFormatList = contents.match(/include\((.*?)\);/g) || []; + const frameList = matchesFrameFormatList.map((match) => match.replace(/include\(|\);/g, "")); + await Promise.all(frameList.map((frame) => queryClient.prefetchQuery(docsQuery.title(frame)))); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/(docs)/docs/[title]/style.css.ts b/apps/wiki/app/(docs)/docs/[title]/style.css.ts new file mode 100644 index 0000000..d3edee3 --- /dev/null +++ b/apps/wiki/app/(docs)/docs/[title]/style.css.ts @@ -0,0 +1,74 @@ +import { flex, font, theme, screen } from "@/styles"; +import { style } from "@vanilla-extract/css"; + +export const container = style({ + width: "100%", + gap: "24px", + ...flex.COLUMN_FLEX, +}); + +export const header = style({ + width: "100%", + marginBottom: "40px", + ...flex.BETWEEN, +}); + +export const body = style({ + width: "100%", + whiteSpace: "pre-wrap", + ...font.p1, +}); + +export const likeButton = style({ + gap: "6px", + cursor: "pointer", + ...font.H6, + ...flex.VERTICAL, +}); + +export const contributorsBox = style({ + width: "fit-content", + gap: "12px", + marginLeft: "auto", + ...flex.COLUMN_FLEX, +}); + +export const contributorTitle = style({ + color: theme.boldgray, + marginLeft: "auto", + ...font.H6, +}); + +export const contributorList = style({ + width: "30vw", + marginLeft: "auto", + flexWrap: "wrap", + gap: "12px", + ...flex.VERTICAL, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + width: "100%", + }, + }, +}); + +export const contributor = style({ + ...font.p3, + marginLeft: "auto", + + ":hover": { + textDecoration: "underline", + }, +}); + +export const warning = style({ + color: theme.red, + ...font.H6, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + ...font.p4, + }, + }, +}); diff --git a/apps/wiki/app/(docs)/teacher/page.tsx b/apps/wiki/app/(docs)/teacher/page.tsx new file mode 100644 index 0000000..6a3d988 --- /dev/null +++ b/apps/wiki/app/(docs)/teacher/page.tsx @@ -0,0 +1,29 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { docsQuery } from "@/services/docs/docs.query"; +import { generateOpenGraph } from "@/utils"; +import DocsList from "../[classify]/DocsList"; + +export const metadata = generateOpenGraph({ + title: "선생님", + description: `교내의 선생님들을 모아둔 페이지입니다.`, +}); + +const Page = async () => { + const queryClient = getQueryClient(); + Promise.all( + teacherClassifyList.map((docsType) => queryClient.prefetchQuery(docsQuery.list(docsType))), + ); + + return ( + + {teacherClassifyList.map((classify) => ( + + ))} + + ); +}; + +const teacherClassifyList = ["teacher", "major_teacher", "mentor_teacher"]; + +export default Page; diff --git a/apps/wiki/app/(user)/ContritbuteDocsList.tsx b/apps/wiki/app/(user)/ContritbuteDocsList.tsx new file mode 100644 index 0000000..5eae353 --- /dev/null +++ b/apps/wiki/app/(user)/ContritbuteDocsList.tsx @@ -0,0 +1,26 @@ +import Accordion from "@/components/Accordion"; +import { ContributeDocsType } from "@/types"; +import Link from "next/link"; + +import { useDate } from "@/hooks"; +import * as styles from "./style.css"; + +const ContritbuteDocsList = ({ contributes }: { contributes: Array }) => { + const { formatDate } = useDate(); + return ( + + {contributes.map((contribute) => ( + + {contribute.title}#{contribute.versionDocsId} + + + ))} + + ); +}; + +export default ContritbuteDocsList; diff --git a/apps/wiki/app/(user)/LikeDocsList.tsx b/apps/wiki/app/(user)/LikeDocsList.tsx new file mode 100644 index 0000000..94bad5f --- /dev/null +++ b/apps/wiki/app/(user)/LikeDocsList.tsx @@ -0,0 +1,23 @@ +import Accordion from "@/components/Accordion"; +import { ContributeDocsType } from "@/types"; +import Link from "next/link"; +import { CLASSIFY } from "@/record"; +import * as styles from "./style.css"; + +const LikeDocsList = ({ likeList }: { likeList: Array }) => { + return ( + + {likeList.map((docs) => ( + + {docs.title} ({CLASSIFY[docs.docsType]}) + + ))} + + ); +}; + +export default LikeDocsList; diff --git a/apps/wiki/app/(user)/mypage/MyPage.tsx b/apps/wiki/app/(user)/mypage/MyPage.tsx new file mode 100644 index 0000000..a309d74 --- /dev/null +++ b/apps/wiki/app/(user)/mypage/MyPage.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Accordion from "@/components/Accordion"; +import Container from "@/components/Container"; +import { useLogoutMutation } from "@/services/auth/auth.mutation"; +import { userQuery } from "@/services/user/user.query"; +import { useQuery } from "@tanstack/react-query"; +import { particle, 조사 } from "auto-particle"; +import { CLASSIFY, ROLE } from "@/record"; +import Link from "next/link"; +import ContritbuteDocsList from "../ContritbuteDocsList"; +import * as styles from "../style.css"; +import LikeDocsList from "../LikeDocsList"; + +const MyPage = () => { + const { data: user } = useQuery(userQuery.my()); + const { data: likeList } = useQuery(userQuery.like()); + const isLoggedIn = user && likeList; + const { mutate } = useLogoutMutation(); + + if (!isLoggedIn) + return ( + + + 로그인 후 이용해주세요. + + + ); + + return ( + + + {particle(user.nickName).word(조사.은_는)} 부마위키의 {ROLE[user.authority]} + 이다. + + + + + + ); +}; + +export default MyPage; diff --git a/apps/wiki/app/(user)/mypage/page.tsx b/apps/wiki/app/(user)/mypage/page.tsx new file mode 100644 index 0000000..4335fc8 --- /dev/null +++ b/apps/wiki/app/(user)/mypage/page.tsx @@ -0,0 +1,13 @@ +import { generateOpenGraph } from "@/utils"; +import MyPage from "./MyPage"; + +export const metadata = generateOpenGraph({ + title: "마이페이지", + description: "부마위키의 마이페이지입니다.", +}); + +const Page = () => { + return ; +}; + +export default Page; diff --git a/apps/wiki/app/(user)/style.css.ts b/apps/wiki/app/(user)/style.css.ts new file mode 100644 index 0000000..bca50a9 --- /dev/null +++ b/apps/wiki/app/(user)/style.css.ts @@ -0,0 +1,37 @@ +import { flex, font, theme } from "@/styles"; +import { style } from "@vanilla-extract/css"; + +export const contributeBox = style({ + padding: "18px", + borderBottom: `1px solid ${theme.gray}`, + cursor: "pointer", + color: theme.primary, + ...font.H5, + ...flex.COLUMN_FLEX, + + ":hover": { + backgroundColor: theme.hover, + }, +}); + +export const modifiedAt = style({ + color: theme.boldgray, + ...font.p2, +}); + +export const link = style({ + color: theme.link, + + ":hover": { + textDecoration: "underline", + }, +}); + +export const button = style({ + width: "fit-content", + borderRadius: "4px", + padding: "6px 12px", + backgroundColor: theme.primary, + color: theme.white, + ...font.H6, +}); diff --git a/apps/wiki/app/(user)/user/[id]/User.tsx b/apps/wiki/app/(user)/user/[id]/User.tsx new file mode 100644 index 0000000..ccc2931 --- /dev/null +++ b/apps/wiki/app/(user)/user/[id]/User.tsx @@ -0,0 +1,26 @@ +"use client"; + +import Accordion from "@/components/Accordion"; +import { FC } from "react"; +import { 조사, particle } from "auto-particle"; +import Container from "@/components/Container"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { userQuery } from "@/services/user/user.query"; +import { CLASSIFY, ROLE } from "@/record"; +import ContritbuteDocsList from "../../ContritbuteDocsList"; + +const User: FC<{ id: number }> = ({ id }) => { + const { data: user } = useSuspenseQuery(userQuery.id(id)); + + return ( + + + {particle(user.nickName).word(조사.은_는)} 부마위키의 {ROLE[user.authority]} + 이다. + + + + ); +}; + +export default User; diff --git a/apps/wiki/app/(user)/user/[id]/page.tsx b/apps/wiki/app/(user)/user/[id]/page.tsx new file mode 100644 index 0000000..c678ef0 --- /dev/null +++ b/apps/wiki/app/(user)/user/[id]/page.tsx @@ -0,0 +1,36 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { userQuery } from "@/services/user/user.query"; +import { Metadata } from "next"; +import { generateOpenGraph } from "@/utils"; +import { CLASSIFY } from "@/record"; +import User from "./User"; + +interface PageProps { + params: { + id: number; + }; +} + +export const generateMetadata = async ({ params: { id } }: PageProps): Promise => { + const queryClient = getQueryClient(); + const { nickName, authority } = await queryClient.fetchQuery(userQuery.id(id)); + + return generateOpenGraph({ + title: nickName, + description: `부마위키 - ${nickName} (${CLASSIFY[authority]})`, + }); +}; + +const Page = async ({ params: { id } }: PageProps) => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(userQuery.id(id)); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/coin/Coin.tsx b/apps/wiki/app/coin/Coin.tsx new file mode 100644 index 0000000..cc5ef1c --- /dev/null +++ b/apps/wiki/app/coin/Coin.tsx @@ -0,0 +1,265 @@ +"use client"; + +import Container from "@/components/Container"; +import { ChangeEvent, useState } from "react"; +import { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { coinQuery } from "@/services/coin/coin.query"; +import Image from "next/image"; +import { useModal } from "@/hooks"; +import { toast } from "react-toastify"; +import Toastify from "@/components/Toastify"; +import { priceComma } from "@/utils"; +import { + useBuyCoinMutation, + useCreateCoinWalletMutation, + useDailyRewardMutation, + useSellMutation, +} from "@/services/coin/coin.mutation"; +import { AxiosError, isAxiosError } from "axios"; +import Accordion from "@/components/Accordion"; +import Link from "next/link"; +import { PartyIcon, WalletIcon } from "@buma/icon"; +import * as styles from "./style.css"; +import Graph from "./Graph"; +import TradeHistory from "./TradeHistory"; + +const tradeText: Record< + string, + { + trade: string; + before: string; + period: string; + } +> = { + BUY: { + trade: "매수", + before: "머니", + period: "₩", + }, + SELL: { + trade: "매도", + before: "코인", + period: "", + }, +}; + +const Coin = () => { + const queryClient = useQueryClient(); + const { data: market, refetch } = useSuspenseQuery(coinQuery.price()); + const { data: wallet, error } = useQuery(coinQuery.myWallet()); + + const { mutate: dailyReward } = useDailyRewardMutation(); + const { mutate: buy } = useBuyCoinMutation(); + const { mutate: sell } = useSellMutation(); + const { mutate: signup } = useCreateCoinWalletMutation(); + + const { openConfirm } = useModal(); + const [tradeMode, setTradeMode] = useState("BUY"); + const [requestAmount, setRequestAmount] = useState(0); + + if (isAxiosError(error) && error.response?.data.status === 404) { + openConfirm({ + icon: , + content: "지금 바로 부마코인을 시작해보세요!\n기본지원금 1000만원을 드려요 😎", + onConfirm: signup, + }); + return
코인 계정 생성 후 이용해주세요.
; + } + + if (!wallet) return
로그인 후 이용해주세요.
; + + const totalMoney = market.price * wallet.coin + wallet.money; + const maxAmountMoney = Math.floor(wallet.money / market.price); + const tradeBeforeLeftMoney = wallet.money - requestAmount * market.price; + const tradeRequestMoney = wallet.money - tradeBeforeLeftMoney; + + const maxAmountCoin = wallet.coin; + const tradeBeforeLeftCoin = wallet.coin - requestAmount; + const tradeRequestCoin = requestAmount; + + const handleTradeSuccess = () => ({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["query.myWallet"] }); + queryClient.invalidateQueries({ queryKey: ["query.coinTrade", wallet.id] }); + setRequestAmount(0); + toast(); + }, + }); + + const handleBuyTradeButtonClick = () => { + if (!requestAmount) return toast(); + openConfirm({ + content: `${priceComma(requestAmount)}주를 주당 ${priceComma(market.price)}원에 매수합니다.`, + onConfirm: () => buy({ marketPrice: market.price, requestAmount }, handleTradeSuccess()), + }); + }; + + const handleSellTradeButtonClick = () => { + if (!requestAmount) return toast(); + openConfirm({ + content: `${priceComma(requestAmount)}주를 주당 ${priceComma(market.price)}원에 매도합니다.`, + onConfirm: () => sell({ marketPrice: market.price, requestAmount }, handleTradeSuccess()), + }); + }; + + const handleRequestAmountChange = ({ target: { value } }: ChangeEvent) => { + const amount = Number(value); + if (Number.isNaN(amount)) return; + if (tradeMode === "BUY" && amount * market.price > wallet.money) return; + if (tradeMode === "SELL" && amount > wallet.coin) return; + setRequestAmount(amount); + }; + + const handleDailyRewardClick = () => { + dailyReward(undefined, { + onSuccess: (res) => { + toast(); + queryClient.invalidateQueries({ queryKey: ["query.myWallet"] }); + }, + onError: (err) => { + if (err instanceof AxiosError) { + toast(); + } + }, + }); + }; + + return ( + +

+ ※ 코인 가격이 0이 되면 상장 폐지되어 보유 중이던 코인이 삭제됩니다.※ +

+
+
+ + 자산 랭킹 + + +
+
+
+
보유 총액
+
+ +
{priceComma(totalMoney)}
+
+
+
+
보유 코인
+
+ bumacoin +
{priceComma(wallet.coin)}
+
+
+
+
보유 머니
+
+ bumacoin +
{priceComma(wallet.money)}
+
+
+
+
+
+
+
+
{ + setTradeMode("BUY"); + setRequestAmount(0); + }} + > + BMC 매수 +
+
{ + setTradeMode("SELL"); + setRequestAmount(0); + }} + > + BMC 매도 +
+
+
+

가격

+ + 1 BMC  =  ₩{priceComma(market.price)} + +
+
+

{tradeText[tradeMode].trade} 가능

+ + {priceComma(tradeMode === "BUY" ? maxAmountMoney : maxAmountCoin)}주 + +
+
+

{tradeText[tradeMode].trade} 수량

+ {tradeMode === "BUY" ? ( + + ) : ( + + )} + + + 총 {priceComma(tradeMode === "BUY" ? maxAmountMoney : maxAmountCoin)}주를{" "} + {tradeText[tradeMode].trade}할 수 있어요 + +
+
+
총 거래 {tradeText[tradeMode].before}
+ + {tradeText[tradeMode].period} + {priceComma(tradeMode === "BUY" ? tradeRequestMoney : tradeRequestCoin)} + +
+
+
거래 후 보유 {tradeText[tradeMode].before}
+ + {tradeText[tradeMode].period} + {priceComma(tradeMode === "BUY" ? tradeBeforeLeftMoney : tradeBeforeLeftCoin)} + +
+ {tradeMode === "SELL" && ( +
+
총 매도 이익
+ ₩{priceComma(requestAmount * market.price)} +
+ )} + {tradeMode === "BUY" ? ( + + ) : ( + + )} +
+
+ { + refetch(); + }} + updatedAt={market.startedTime} + marketPrice={market.price} + /> + + + +
+ ); +}; + +export default Coin; diff --git a/apps/wiki/app/coin/Graph.tsx b/apps/wiki/app/coin/Graph.tsx new file mode 100644 index 0000000..715362f --- /dev/null +++ b/apps/wiki/app/coin/Graph.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useSuspenseQuery } from "@tanstack/react-query"; +import { FC, useEffect, useState } from "react"; +import Image from "next/image"; +import { Line } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Filler, + Legend, +} from "chart.js"; +import dayjs from "dayjs"; +import { coinQuery } from "@/services/coin/coin.query"; +import { priceComma } from "@/utils"; +import { useDate } from "@/hooks"; +import * as styles from "./style.css"; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Filler, + Title, + Tooltip, + Legend, +); + +const options = { + responsive: true, + pointStyle: "circle", + pointBorderWidth: 0, + cubicInterpolationMode: "monotone", + plugins: { legend: { display: false }, title: { display: false } }, + interaction: { intersect: false }, +}; + +const cycleList = [ + { name: "전체", id: "full" }, + { name: "보름", id: "halfMonth" }, + { name: "일주일", id: "week" }, + { name: "하루", id: "day" }, + { name: "12시간", id: "halfDay" }, + { name: "3시간", id: "threeHours" }, +]; + +interface GraphProps { + updatedAt: Date; + marketPrice: number; + refetch: () => void; +} + +const Graph: FC = ({ updatedAt, marketPrice, refetch }) => { + const { formatDate } = useDate(); + const differenceInSeconds = dayjs().diff(dayjs(updatedAt), "second"); + const remainingSeconds = 3 * 60 - differenceInSeconds; + + const [cycle, setCycle] = useState("threeHours"); + const { data: coin, refetch: graphRefetch } = useSuspenseQuery(coinQuery.graph(cycle)); + + const labels = coin.map(({ startedTime }: { startedTime: Date }) => + dayjs(startedTime).format("M/D H:m"), + ); + const data = coin.map(({ price }: { price: string }) => price); + + useEffect(() => { + setTimeout(() => { + refetch(); + graphRefetch(); + }, remainingSeconds * 1000); + }, [updatedAt]); + + return ( +
+
+
+ bumacoin +
+ ₩{priceComma(marketPrice)} + {formatDate(updatedAt)} + 3분마다 업데이트됩니다. +
+
+
+ {cycleList.map((cycleItem) => { + const className = + cycle === cycleItem.id ? styles.category.ENABLED : styles.category.DISABLED; + return ( + + ); + })} +
+
+ +
+ ); +}; + +export default Graph; diff --git a/apps/wiki/app/coin/TradeHistory.css.ts b/apps/wiki/app/coin/TradeHistory.css.ts new file mode 100644 index 0000000..c0a709d --- /dev/null +++ b/apps/wiki/app/coin/TradeHistory.css.ts @@ -0,0 +1,74 @@ +import { flex, font, theme, screen } from "@/styles"; +import { StyleVariantsType } from "@/types"; +import { style, styleVariants } from "@vanilla-extract/css"; + +export const tradeListBox = style({ + width: "100%", + padding: "16px", + borderBottom: `1px solid ${theme.gray}`, + cursor: "pointer", + gap: "10px", + ...flex.COLUMN_FLEX, + + ":hover": { + backgroundColor: theme.hover, + }, +}); + +export const hgroup = style({ + gap: "8px", + ...flex.VERTICAL, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + ...flex.COLUMN_CENTER, + }, + }, +}); + +export const tradeId = style({ + color: theme.primary, + ...font.H5, +}); + +export const createdAt = style({ + color: theme.boldgray, + ...font.p2, +}); + +export const informationText = style({ + cursor: "pointer", + width: "fit-content", + gap: "4px", + ...font.btn2, + ...flex.VERTICAL, +}); + +export const informationBox = style({ + gap: "18px", + ...flex.VERTICAL, +}); + +export const cancelButton = style({ + width: "fit-content", + padding: "6px 14px", + backgroundColor: theme.red, + borderRadius: "6px", + color: theme.white, + ...font.btn3, +}); + +export const tradeStatusCircleBase = style({ + width: "16px", + height: "16px", + borderRadius: "999px", +}); + +export const tradeStatusCircle = styleVariants({ + BUYING: [tradeStatusCircleBase, { backgroundColor: theme.insert }], + SELLING: [tradeStatusCircleBase, { backgroundColor: theme.insert }], + BOUGHT: [tradeStatusCircleBase, { backgroundColor: theme.buy }], + SOLD: [tradeStatusCircleBase, { backgroundColor: theme.sell }], + CANCELLED: [tradeStatusCircleBase, { backgroundColor: theme.gray }], + DELISTING: [tradeStatusCircleBase, { backgroundColor: theme.black }], +}); diff --git a/apps/wiki/app/coin/TradeHistory.tsx b/apps/wiki/app/coin/TradeHistory.tsx new file mode 100644 index 0000000..69fa8b1 --- /dev/null +++ b/apps/wiki/app/coin/TradeHistory.tsx @@ -0,0 +1,71 @@ +import { coinQuery } from "@/services/coin/coin.query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { FC } from "react"; +import { priceComma } from "@/utils"; +import Image from "next/image"; +import { useCancelTradeMutation } from "@/services/coin/coin.mutation"; +import { toast } from "react-toastify"; +import { useDate } from "@/hooks"; +import { TRADE } from "@/record"; +import Toastify from "@/components/Toastify"; +import * as styles from "./TradeHistory.css"; + +const TradeHistory: FC<{ id: number }> = ({ id }) => { + const { formatDate } = useDate(); + const { data: tradeList, isSuccess } = useQuery(coinQuery.trade(id)); + const { mutate } = useCancelTradeMutation(); + const queryClient = useQueryClient(); + + const handleCancelTradeClick = (tradeId: number) => { + mutate(tradeId, { + onSuccess: () => { + toast(); + queryClient.invalidateQueries({ queryKey: ["query.graph"] }); + queryClient.invalidateQueries({ queryKey: ["query.myWallet"] }); + queryClient.invalidateQueries({ queryKey: ["query.coinTrade", id] }); + queryClient.invalidateQueries({ queryKey: ["query.price"] }); + }, + }); + }; + return ( +
+ {isSuccess && + tradeList.map((trade) => ( +
+
+
+

+ {TRADE[trade.tradeStatus]}#{trade.id} +

+ {trade.tradedTime && ( + + 거래일 ·  + {formatDate(trade.tradedTime)} + + )} +
+
+ + money + {priceComma(trade.coinPrice)} + + + money + {priceComma(trade.coinCount)} + +
+ + 총 거래 금액 · {priceComma(trade.usedMoney)} + + {["BUYING", "SELLING"].includes(trade.tradeStatus) && ( +
handleCancelTradeClick(trade.id)} className={styles.cancelButton}> + 취소 +
+ )} +
+ ))} +
+ ); +}; + +export default TradeHistory; diff --git a/apps/wiki/app/coin/page.tsx b/apps/wiki/app/coin/page.tsx new file mode 100644 index 0000000..d9278e4 --- /dev/null +++ b/apps/wiki/app/coin/page.tsx @@ -0,0 +1,28 @@ +import { coinQuery } from "@/services/coin/coin.query"; + +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import { generateOpenGraph } from "@/utils"; +import getQueryClient from "../getQueryClient"; +import Coin from "./Coin"; + +export const metadata = generateOpenGraph({ + title: "코인", + description: `부마코인 투자 페이지입니다.`, +}); + +const Page = async () => { + const queryClient = getQueryClient(); + const graphList = ["full", "halfMonth", "week", "day", "halfDay", "threeHours"].map((cycle) => + queryClient.prefetchQuery(coinQuery.graph(cycle)), + ); + + await Promise.all([queryClient.prefetchQuery(coinQuery.price()), ...graphList]); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/coin/rank/CoinRanking.tsx b/apps/wiki/app/coin/rank/CoinRanking.tsx new file mode 100644 index 0000000..63f720a --- /dev/null +++ b/apps/wiki/app/coin/rank/CoinRanking.tsx @@ -0,0 +1,68 @@ +"use client"; + +import Container from "@/components/Container"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { coinQuery } from "@/services/coin/coin.query"; +import Image from "next/image"; +import Link from "next/link"; +import { priceComma, calculateCoinTier } from "@/utils"; +import { WalletIcon } from "@buma/icon"; +import * as styles from "./style.css"; + +const CoinRanking = () => { + const { data: rankingList } = useSuspenseQuery(coinQuery.rank()); + const rankingListMax = rankingList.length; + + return ( + + + 뒤로가기 + +
    + {rankingList.map((ranking, index) => { + const rank = index + 1; + const tierStyle = rank <= 2 ? rank : "default"; + + return ( + + tier +
    +
    +

    #{rank}

    + + {ranking.username} + +
    + + {priceComma(ranking.totalMoney)}₩ + +
    +
    + moneyicon + {priceComma(ranking.money)}₩ +
    +
    + moneyicon + {priceComma(ranking.coin)}주 +
    +
    +
    + + ); + })} +
+
+ ); +}; + +export default CoinRanking; diff --git a/apps/wiki/app/coin/rank/page.tsx b/apps/wiki/app/coin/rank/page.tsx new file mode 100644 index 0000000..ef8e57b --- /dev/null +++ b/apps/wiki/app/coin/rank/page.tsx @@ -0,0 +1,18 @@ +import getQueryClient from "@/app/getQueryClient"; +import { coinQuery } from "@/services/coin/coin.query"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; + +import CoinRanking from "./CoinRanking"; + +const Page = async () => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(coinQuery.rank()); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/coin/rank/style.css.ts b/apps/wiki/app/coin/rank/style.css.ts new file mode 100644 index 0000000..3b5f46b --- /dev/null +++ b/apps/wiki/app/coin/rank/style.css.ts @@ -0,0 +1,92 @@ +import { flex, font, theme } from "@/styles"; +import { StyleVariantsType } from "@/types"; +import { style, styleVariants } from "@vanilla-extract/css"; + +export const rankingBox = style({ + width: "100%", + gap: "6px", + marginTop: "20px", + ...flex.COLUMN_FLEX, +}); + +export const backButton = style({ + padding: "8px 18px", + width: "fit-content", + borderRadius: "6px", + backgroundColor: theme.primary, + color: theme.white, + ...font.H6, +}); + +export const rankingListItem = style({ + width: "100%", + padding: "16px 22px", + boxShadow: "0 0 20px 0 #00000011", + gap: "14px", + cursor: "pointer", + ...flex.VERTICAL, +}); + +export const informationBox = style({ + width: "100%", + ...flex.COLUMN_FLEX, +}); + +export const rankingListItemHGroup = style({ + width: "100%", + gap: "12px", + ...flex.VERTICAL, +}); + +const rankingListItemRankTextBase = style({ + color: theme.primary, + ...font.H4, +}); + +export const rankingListItemRankText = styleVariants({ + 1: [rankingListItemRankTextBase, { ...font.H2 }], + 2: [rankingListItemRankTextBase, { ...font.H3 }], + default: [rankingListItemRankTextBase], +}); + +const rankingListItemNameTextBase = style({ + color: theme.primary, + ...font.H5, +}); + +export const rankingListItemNameText = styleVariants({ + 1: [rankingListItemNameTextBase, { ...font.H3 }], + 2: [rankingListItemNameTextBase, { ...font.H4 }], + default: [rankingListItemNameTextBase], +}); + +export const rankingListItemInformation = style({ + width: "100%", + gap: "6px", + ...flex.COLUMN_FLEX, +}); + +export const rankingListItemBody = style({ + width: "20%", + gap: "8px", + color: theme.primary, + ...flex.VERTICAL, + ...font.btn3, +}); + +const tierBase = style({ + height: "auto", +}); + +export const tier = styleVariants({ + 1: [tierBase, { width: "80px" }], + 2: [tierBase, { width: "70px" }], + 3: [tierBase, { width: "60px" }], + default: [tierBase, { width: "50px" }], +}); + +export const totalMoney = style({ + gap: "6px", + ...flex.VERTICAL, + ...font.H6, +}); diff --git a/apps/wiki/app/coin/style.css.ts b/apps/wiki/app/coin/style.css.ts new file mode 100644 index 0000000..798cf71 --- /dev/null +++ b/apps/wiki/app/coin/style.css.ts @@ -0,0 +1,185 @@ +import { flex, font, theme, screen } from "@/styles"; +import { StyleVariantsType } from "@/types"; +import { style, styleVariants } from "@vanilla-extract/css"; + +export const informationContainer = style({ + width: "100%", + height: "fit-content", + gap: "6vw", + padding: "10px 0", + ...flex.BETWEEN, +}); + +export const utilityBox = style({ + gap: "12px", + ...flex.VERTICAL, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + ...flex.COLUMN_FLEX, + }, + }, +}); + +export const moneyBox = style({ + ...flex.COLUMN_FLEX, +}); + +export const moneyName = style({ + marginLeft: "auto", + ...font.p2, +}); + +export const moneyAmount = style({ + gap: "6px", + color: theme.primary, + ...flex.VERTICAL, + ...font.H4, +}); + +export const tradeContainer = style({ + gap: "10px", + borderBottom: `1px solid ${theme.gray}`, + ...flex.COLUMN_FLEX, +}); + +export const tradeBox = style({ + width: "100%", + gap: "24px", + paddingBottom: "24px", + ...flex.COLUMN_FLEX, +}); + +export const tradeHeader = style({ + width: "100%", + borderBottom: `1px solid ${theme.gray}`, + ...flex.VERTICAL, +}); + +export const tradeToggleBase = style({ + width: "12%", + padding: "4px 0", + cursor: "pointer", + ...font.H6, + ...flex.CENTER, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + width: "30%", + }, + }, +}); + +export const tradeToggle = styleVariants({ + BUY: [tradeToggleBase, { borderBottom: `3px solid ${theme.buy}`, color: theme.buy }], + SELL: [tradeToggleBase, { borderBottom: `3px solid ${theme.sell}`, color: theme.sell }], + DISABLED: [tradeToggleBase, { borderBottom: `3px solid transparent`, color: theme.gray }], +}); + +export const tradeFieldBox = style({ + gap: "14px", + ...flex.VERTICAL, +}); + +export const tradeName = style({ + color: theme.boldgray, + ...font.H6, +}); + +export const tradeItem = style({ + ...font.H5, +}); + +export const tradeDescription = style({ + color: theme.boldgray, + ...font.btn3, +}); + +export const tradeInput = style({ + border: `1px solid ${theme.gray}`, + borderRadius: "4px", + padding: "4px 10px", + width: "80px", + textAlign: "right", + ...font.p2, +}); + +export const tradeButton = style({ + padding: "8px 20px", + borderRadius: "4px", + width: "fit-content", + backgroundColor: theme.primary, + color: theme.white, + ...font.btn2, +}); + +export const tradeInformation = style({ + ...font.H6, +}); + +export const chartContainer = style({ + width: "100%", + height: "100%", + backgroundColor: "#f2f3f7", + margin: "20px 0", + padding: "26px", + borderRadius: "4px", + gap: "14px", + ...flex.COLUMN_FLEX, +}); + +export const chartHeader = style({ + gap: "12px", + ...flex.BETWEEN, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + ...flex.COLUMN_FLEX, + }, + }, +}); + +export const chartCoinBox = style({ + gap: "12px", + ...flex.VERTICAL, +}); + +export const chartCoinInfoBox = style({ + ...flex.COLUMN_FLEX, +}); + +export const chartCoinTitle = style({ + color: theme.primary, + listStyle: "comma-separated", + ...font.H2, +}); + +export const chartCoinDate = style({ + color: theme.gray, + ...font.btn2, +}); + +export const chartTitle = style({ + ...font.H3, +}); + +export const categoryBox = style({ + flexWrap: "wrap", + ...flex.FLEX, +}); + +export const categoryBase = style({ + padding: "8px 14px", + cursor: "pointer", + ...font.btnBold, +}); + +export const category = styleVariants({ + DISABLED: [categoryBase], + ENABLED: [categoryBase, { backgroundColor: theme.gray }], +}); + +export const warningText = style({ + color: theme.red, + ...font.H5, +}); diff --git a/apps/wiki/app/coin/trade/[accountId]/TradeHistory.tsx b/apps/wiki/app/coin/trade/[accountId]/TradeHistory.tsx new file mode 100644 index 0000000..0e00b69 --- /dev/null +++ b/apps/wiki/app/coin/trade/[accountId]/TradeHistory.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { FC } from "react"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { coinQuery } from "@/services/coin/coin.query"; +import { priceComma } from "@/utils"; +import Image from "next/image"; +import Container from "@/components/Container"; +import { useDate } from "@/hooks"; +import { TRADE } from "@/record"; +import * as styles from "../../TradeHistory.css"; + +const Trade: FC<{ accountId: number }> = ({ accountId }) => { + const { formatDate } = useDate(); + const { data: tradeList } = useSuspenseQuery(coinQuery.trade(accountId)); + return ( + + {tradeList.map((trade) => ( +
+
+
+

+ {TRADE[trade.tradeStatus]}#{trade.id} +

+ {trade.tradedTime && ( + + 거래일 ·  + {formatDate(trade.tradedTime)} + + )} +
+
+ + money + {priceComma(trade.coinPrice)} + + + money + {priceComma(trade.coinCount)} + +
+ + 총 거래 금액 · {priceComma(trade.usedMoney)} + +
+ ))} + + ); +}; + +export default Trade; diff --git a/apps/wiki/app/coin/trade/[accountId]/page.tsx b/apps/wiki/app/coin/trade/[accountId]/page.tsx new file mode 100644 index 0000000..6330d36 --- /dev/null +++ b/apps/wiki/app/coin/trade/[accountId]/page.tsx @@ -0,0 +1,29 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { coinQuery } from "@/services/coin/coin.query"; +import { generateOpenGraph } from "@/utils"; +import TradeHistory from "./TradeHistory"; + +interface PageProps { + params: { + accountId: string; + }; +} + +export const metadata = generateOpenGraph({ + title: "코인 거래 내역", + description: `부마코인 거래 내역 페이지입니다.`, +}); + +const Page = async ({ params: { accountId } }: PageProps) => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(coinQuery.trade(Number(accountId))); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/create/page.tsx b/apps/wiki/app/create/page.tsx new file mode 100644 index 0000000..0dbe56e --- /dev/null +++ b/apps/wiki/app/create/page.tsx @@ -0,0 +1,14 @@ +import EditorContainer from "@/components/Editor"; +import { EditorType } from "@/enum"; +import { generateOpenGraph } from "@/utils"; + +export const metadata = generateOpenGraph({ + title: "문서 생성", + description: "부마위키 문서 생성 페이지입니다.", +}); + +const Page = () => { + return ; +}; + +export default Page; diff --git a/apps/wiki/app/edit/[title]/page.tsx b/apps/wiki/app/edit/[title]/page.tsx new file mode 100644 index 0000000..a46a8bb --- /dev/null +++ b/apps/wiki/app/edit/[title]/page.tsx @@ -0,0 +1,36 @@ +import getQueryClient from "@/app/getQueryClient"; +import EditorContainer from "@/components/Editor"; +import { EditorType } from "@/enum"; +import { docsQuery } from "@/services/docs/docs.query"; +import { generateOpenGraph } from "@/utils"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import { Metadata } from "next"; + +interface PageProps { + params: { + title: string; + }; +} + +export const generateMetadata = async ({ params: { title } }: PageProps): Promise => { + const queryClient = getQueryClient(); + const data = await queryClient.fetchQuery(docsQuery.title(title)); + + return generateOpenGraph({ + title: `문서 편집#${data.title}`, + description: `${data.title} 문서 편집 페이지입니다.`, + }); +}; + +const Page = async ({ params: { title } }: PageProps) => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(docsQuery.title(title)); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/getQueryClient.tsx b/apps/wiki/app/getQueryClient.tsx new file mode 100644 index 0000000..cdd22cc --- /dev/null +++ b/apps/wiki/app/getQueryClient.tsx @@ -0,0 +1,6 @@ +import { QueryClient } from "@tanstack/react-query"; +import { cache } from "react"; + +// 서버사이드에서 React Query prefetch를 진행하기 위함 +const getQueryClient = cache(() => new QueryClient()); +export default getQueryClient; diff --git a/apps/wiki/app/globals.css b/apps/wiki/app/globals.css new file mode 100644 index 0000000..c555176 --- /dev/null +++ b/apps/wiki/app/globals.css @@ -0,0 +1,168 @@ +@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"); + +* { + font-family: + "Pretendard", + Pretendard, + -apple-system, + BlinkMacSystemFont, + system-ui, + Roboto, + "Helvetica Neue", + "Segoe UI", + "Apple SD Gothic Neo", + "Noto Sans KR", + "Malgun Gothic", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + sans-serif; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + @media screen and (min-width: 1026px) and (max-width: 1440px) { + font-size: 90%; + } + + @media screen and (min-width: 769px) and (max-width: 1025px) { + font-size: 80%; + } + + @media screen and (min-width: 541px) and (max-width: 768px) { + font-size: 70%; + } + + @media screen and (min-width: 301px) and (max-width: 540px) { + font-size: 60%; + } + + @media screen and (max-width: 300px) { + font-size: 50%; + } +} + +body { + overflow-x: hidden; +} + +ul, +li { + list-style: none; +} + +p { + display: inline-block; +} + +a { + display: inline-block; + text-decoration: none; + color: inherit; +} + +label { + cursor: pointer; +} + +input, +textarea { + -moz-user-select: auto; + -webkit-user-select: auto; + -ms-user-select: auto; + user-select: auto; + border: none; + outline: none; + resize: none; +} + +input:focus { + outline: none; +} + +button { + outline: none; + border: none; + background: none; + padding: 0; + cursor: pointer; +} + +/* wiki */ + +.frame_td { + width: 200px; + height: 50px; + background-color: white; + border: solid 4px #274168; +} + +.frame_table { + border-collapse: collapse; + border: solid 4px #274168; + background-color: #ccc; + margin: auto; + text-align: center; + padding: 10px; + width: 100%; +} + +.frame_caption { + background-color: #274168; + color: white; + font-weight: 700; + padding: 10px; + font-size: 20px; + text-align: center; + cursor: pointer; + display: flex; + justify-content: center; + align-content: center; +} + +.frame_fold { + font-size: 1.25rem; + font-weight: 600; +} + +.frame_caption::marker { + content: ""; +} + +.frame_details { + margin-bottom: 1em; + transition: all linear 1.5s; + max-height: 10em; + overflow: hidden; +} + +.frame_details[open] { + max-height: 500em; +} + +.frame_details[close] { + max-height: auto; +} + +/* toastify */ + +.toastify .Toastify__toast { + background-color: white; + top: 40px; + right: 80px; + width: 400px; + color: #274168; + border-radius: 0px; + + @media (max-width: 600px) { + top: 50px; + width: 50vw; + height: 20px; + margin-top: 10px; + right: 10px; + margin-left: auto; + border-radius: 5px; + } +} diff --git a/apps/wiki/app/history/[title]/History.tsx b/apps/wiki/app/history/[title]/History.tsx new file mode 100644 index 0000000..5b02cad --- /dev/null +++ b/apps/wiki/app/history/[title]/History.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Suspense } from "react"; +import Link from "next/link"; +import Container from "@/components/Container"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { historyQuery } from "@/services/history/history.query"; +import { useDate } from "@/hooks"; +import * as styles from "./style.css"; + +const History = ({ title }: { title: string }) => { + const { formatDate } = useDate(); + const { data: history } = useSuspenseQuery(historyQuery.list(title)); + + return ( + + + {history.versionDocsResponseDto.map((docsHistory) => ( + +
+

#{docsHistory.index}

+ +
+ 작성자 · {docsHistory.nickName} + + ))} +
+
+ ); +}; + +export default History; diff --git a/apps/wiki/app/history/[title]/detail/[id]/HistoryDetail.tsx b/apps/wiki/app/history/[title]/detail/[id]/HistoryDetail.tsx new file mode 100644 index 0000000..d79650c --- /dev/null +++ b/apps/wiki/app/history/[title]/detail/[id]/HistoryDetail.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { FC } from "react"; +import Link from "next/link"; +import Container from "@/components/Container"; +import { VersionDifferent } from "@/enum"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { historyQuery } from "@/services/history/history.query"; +import * as styles from "./style.css"; + +interface HistoryType { + operation: string; + text: string; +} + +const HistoryDetail: FC<{ id: number; title: string }> = ({ id, title }) => { + const { data: history } = useSuspenseQuery(historyQuery.detail({ id: id - 1, title })); + + return ( + +
+ + 작성자 · {history.versionDocs.nickName} + +
    + {history.diff.map((dif: HistoryType, historyId: number) => { + const operationIcon = (() => { + switch (dif.operation) { + case VersionDifferent.INSERT: + return "+"; + case VersionDifferent.DELETE: + return "-"; + case VersionDifferent.EQUAL: + return; + default: + return dif.operation; + } + })(); + return ( +
  • + {operationIcon} +

    {dif.text}

    +
  • + ); + })} +
+
+
+ ); +}; + +export default HistoryDetail; diff --git a/apps/wiki/app/history/[title]/detail/[id]/page.tsx b/apps/wiki/app/history/[title]/detail/[id]/page.tsx new file mode 100644 index 0000000..0a3a564 --- /dev/null +++ b/apps/wiki/app/history/[title]/detail/[id]/page.tsx @@ -0,0 +1,33 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { historyQuery } from "@/services/history/history.query"; +import { generateOpenGraph } from "@/utils"; +import { Metadata } from "next"; +import HistoryDetail from "./HistoryDetail"; + +interface PageProps { + params: { + title: string; + id: number; + }; +} + +export const generateMetadata = async ({ params: { title, id } }: PageProps): Promise => { + const decodedTitle = decodeURI(title); + return generateOpenGraph({ + title: `역사#${decodedTitle}_${id}`, + description: `${decodedTitle} 문서의 ${id}번째 역사입니다.`, + }); +}; + +const Page = async ({ params }: PageProps) => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(historyQuery.detail(params)); + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/history/[title]/detail/[id]/style.css.ts b/apps/wiki/app/history/[title]/detail/[id]/style.css.ts new file mode 100644 index 0000000..0d59ac0 --- /dev/null +++ b/apps/wiki/app/history/[title]/detail/[id]/style.css.ts @@ -0,0 +1,54 @@ +import { flex, font, theme } from "@/styles"; +import { StyleVariantsType } from "@/types"; +import { style, styleVariants } from "@vanilla-extract/css"; + +export const container = style({ + width: "100%", + gap: "24px", + ...flex.COLUMN_FLEX, +}); + +export const author = style({ + ...font.H6, + + ":hover": { + textDecoration: "underline", + }, +}); + +export const historyBox = style({ + ...flex.COLUMN_FLEX, +}); + +export const historyContent = style({ + width: "100%", + ...flex.VERTICAL, +}); + +const historyBase = style({ + width: "100%", + padding: "6px 8px", + gap: "12px", + minHeight: "20px", + whiteSpace: "pre-wrap", + opacity: 0.7, + ...flex.VERTICAL, +}); + +export const history = styleVariants({ + INSERT: [historyBase, { background: theme.insert }], + DELETE: [historyBase, { background: theme.delete }], + EQUAL: [historyBase, { background: theme.equal }], +}); + +const historyOperationBase = style({ + width: "20px", + height: "100%", + ...flex.CENTER, +}); + +export const historyOperation = styleVariants({ + INSERT: [historyOperationBase, { background: theme.insert }], + DELETE: [historyOperationBase, { background: theme.delete }], + EQUAL: [historyOperationBase, { background: theme.equal }], +}); diff --git a/apps/wiki/app/history/[title]/page.tsx b/apps/wiki/app/history/[title]/page.tsx new file mode 100644 index 0000000..ad101ee --- /dev/null +++ b/apps/wiki/app/history/[title]/page.tsx @@ -0,0 +1,33 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import getQueryClient from "@/app/getQueryClient"; +import { historyQuery } from "@/services/history/history.query"; +import { Metadata } from "next"; +import { generateOpenGraph } from "@/utils"; +import History from "./History"; + +interface PageProps { + params: { + title: string; + }; +} + +export const generateMetadata = async ({ params: { title } }: PageProps): Promise => { + const decodedTitle = decodeURI(title); + return generateOpenGraph({ + title: `역사#${decodedTitle}`, + description: `${decodedTitle} 문서의 역사입니다.`, + }); +}; + +const Page = async ({ params: { title } }: PageProps) => { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(historyQuery.list(title)); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/wiki/app/history/[title]/style.css.ts b/apps/wiki/app/history/[title]/style.css.ts new file mode 100644 index 0000000..553b881 --- /dev/null +++ b/apps/wiki/app/history/[title]/style.css.ts @@ -0,0 +1,36 @@ +import { flex, font, theme } from "@/styles"; +import { style } from "@vanilla-extract/css"; + +export const historyBox = style({ + width: "100%", + padding: "10px", + borderBottom: `1px solid ${theme.gray}`, + cursor: "pointer", + gap: "8px", + ...flex.COLUMN_FLEX, + + ":hover": { + backgroundColor: theme.hover, + }, +}); + +export const hgroup = style({ + gap: "14px", + ...flex.VERTICAL, +}); + +export const historyId = style({ + color: theme.primary, + ...font.H4, +}); + +export const createdAt = style({ + color: theme.boldgray, + ...font.p1, +}); + +export const author = style({ + cursor: "pointer", + width: "fit-content", + ...font.H6, +}); diff --git a/apps/wiki/app/layout.css.ts b/apps/wiki/app/layout.css.ts new file mode 100644 index 0000000..9c07dba --- /dev/null +++ b/apps/wiki/app/layout.css.ts @@ -0,0 +1,35 @@ +import { theme, flex, screen } from "@/styles"; +import { style } from "@vanilla-extract/css"; + +export const container = style({ + width: "100%", + minHeight: "100svh", + backgroundColor: theme.background, + paddingTop: "52px", + ...flex.FLEX, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + display: "inline", + }, + }, +}); + +export const aside = style({ + height: "100%", + position: "sticky", + top: "70px", + width: "300px", + gap: "12px", + ...flex.COLUMN_FLEX, + + "@media": { + [`screen and (max-width: ${screen.phone})`]: { + top: "0", + position: "static", + width: "100%", + gap: "0", + marginBottom: "100px", + }, + }, +}); diff --git a/apps/wiki/app/layout.tsx b/apps/wiki/app/layout.tsx new file mode 100644 index 0000000..0b4cb1d --- /dev/null +++ b/apps/wiki/app/layout.tsx @@ -0,0 +1,49 @@ +import "./globals.css"; +import Header from "@/components/Header"; +import Board from "@/components/Board"; +import Aside from "@/components/Aside"; +import Footer from "@/components/Footer"; +import Popular from "@/components/Popular"; +import Modal from "@/components/(modal)/Modal"; +import { FC, PropsWithChildren, ReactNode } from "react"; +import ScrollButton from "@/components/ScrollButton"; +import { generateOpenGraph } from "@/utils"; +import "react-toastify/dist/ReactToastify.css"; +import * as styles from "./layout.css"; +import Providers from "./providers"; + +export const metadata = generateOpenGraph({ + title: "역사의 고서", + description: "우리의 손으로 써내려 나가는 역사의 고서, 부마위키", +}); + +const Main: FC = ({ children }) => ( +
+ {children} + {/** side components */} +
+); + +export default function RootLayout({ + children, +}: Readonly<{ + children: ReactNode; +}>) { + return ( + + + + +
+
{children}
+