diff --git a/.github/workflows/web-client-deploy-development.yml b/.github/workflows/web-client-deploy-development.yml index f9d1f240..1408fb30 100644 --- a/.github/workflows/web-client-deploy-development.yml +++ b/.github/workflows/web-client-deploy-development.yml @@ -11,28 +11,27 @@ jobs: steps: - uses: actions/checkout@v4 - # TODO: 패키지 구조 변경 및 s3 배포에 따라 변경해야함 - # - uses: actions/setup-node@v4 - # with: - # node-version: '20' - # cache: 'yarn' + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + # @see https://github.com/cypress-io/github-action#yarn-modern + cache-dependency-path: ./yarn.lock - # - name: build - # run: | - # rm ./apps/web/.env && mv ./apps/web/.env.dev ./apps/web/.env - # yarn set version 3.8.1 - # yarn install --immutable --immutable-cache - # yarn web codegen - # yarn web build + - name: build + run: | + rm ./websites/poolc.org/.env && mv ./websites/poolc.org/.env.dev ./websites/poolc.org/.env + yarn set version 4.9.2 + yarn install --immutable + yarn workspace @dialga/poolc.org build - # - name: scp - # uses: appleboy/scp-action@v0.1.7 - # with: - # host: ${{ secrets.SSH_HOST }} - # username: ${{ secrets.SSH_USERNAME }} - # key: ${{ secrets.SSH_PEM_KEY }} - # port: ${{ secrets.SSH_PORT }} - # source: ./apps/web/build/* - # # TODO: 패키지 구조 변경에 따라 target 디렉토리도 변경해야함 - # target: ~/dialga/apps/web-client/build - # strip_components: 3 + - name: scp + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PEM_KEY }} + port: ${{ secrets.SSH_PORT }} + source: ./websites/poolc.org/build/* + target: ~/k8s/dialga/build + strip_components: 3 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..09c06f5d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.19.3 \ No newline at end of file diff --git a/websites/poolc.org/.env b/websites/poolc.org/.env index a4f9d7de..ee3b77ac 100644 --- a/websites/poolc.org/.env +++ b/websites/poolc.org/.env @@ -1,3 +1,3 @@ -VITE_API_BASE_URL=https://api.poolc.org -VITE_FILE_URL=https://api.poolc.org +VITE_API_BASE_URL=https://dev.poolc.org/api +VITE_FILE_URL=https://dev.poolc.org/api VITE_MAX_FILE_SIZE=50000000 # 50mb diff --git a/websites/poolc.org/package.json b/websites/poolc.org/package.json index dff3ac7b..1309449e 100644 --- a/websites/poolc.org/package.json +++ b/websites/poolc.org/package.json @@ -10,7 +10,7 @@ "sync:type": "yarn dlx typesync", "check:type": "tsc", "check:type:watch": "yarn check:type --watch", - "codegen": "openapi -i https://api.poolc.org/v2/api-docs -o src/lib/api-v2/__generated__ --useUnionTypes --useOptions -c axios", + "codegen": "dotenv -e ./.env -- bash -c 'openapi -i $VITE_API_BASE_URL/v2/api-docs -o src/lib/api-v2/__generated__ --useUnionTypes --useOptions -c axios'", "postinstall": "yarn codegen", "lint": "eslint . && prettier . --check --ignore-path .gitignore", "format": "eslint . --fix && prettier . --write --ignore-path .gitignore" @@ -20,6 +20,7 @@ }, "devDependencies": { "@types/node": "^18.15.3", + "dotenv-cli": "^10.0.0", "eslint": "^8.57.0", "openapi-typescript-codegen": "^0.28.0", "prettier": "^3.1.1", diff --git a/websites/poolc.org/src/components/header/Menus/Menus.tsx b/websites/poolc.org/src/components/header/Menus/Menus.tsx index c3a906a4..2f060c39 100644 --- a/websites/poolc.org/src/components/header/Menus/Menus.tsx +++ b/websites/poolc.org/src/components/header/Menus/Menus.tsx @@ -75,6 +75,11 @@ const Menus = ({ visible: isLogin, content: 'Room', }, + { + to: `/${MENU.MY_PAGE}#pks`, + visible: isLogin, + content: 'K8s', + }, { to: `/${MENU.APPLY}`, visible: !isLogin || (isLogin && !isAuthorizedRole(role)), diff --git a/websites/poolc.org/src/components/header/Notification/Notification.tsx b/websites/poolc.org/src/components/header/Notification/Notification.tsx index 3dbaaa50..743f23eb 100644 --- a/websites/poolc.org/src/components/header/Notification/Notification.tsx +++ b/websites/poolc.org/src/components/header/Notification/Notification.tsx @@ -222,7 +222,7 @@ export default function Notification() { return (
- + + + + } + > + + + + {baekjoon.data && } + + + + 얻은 뱃지 + + 모든 뱃지보기 > + + + {badge?.data && badge.data.length > 0 ? ( + + {badge.data.map((el, idx) => ( + + ))} + + ) : ( + 아직 뱃지가 없습니다. + )} + + + 나의 메뉴 + + item.link ? ( + + +
+ {item.icon} + {item.title} +
+ + +
+ ) : ( + +
+ {item.icon} + {item.title} +
+ +
+ ) + } + /> +
+ + PKS (PoolC Kubernetes Service) + + + + ); +} + +const PKS_ID = 'pks'; + +const useStyles = createStyles(({ css }) => ({ + whiteBlock: css` + box-sizing: border-box; + padding: 30px 20px; + `, + wrapper: css` + width: 100%; + box-sizing: border-box; + `, + fullWidth: css` + width: 100%; + `, + link: css` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + text-decoration: inherit; + cursor: pointer; + `, + linkInner: css` + display: flex; + align-items: center; + gap: 20px; + `, + userName: css` + font-size: 24px; + font-weight: 700; + position: relative; + + &:before { + position: absolute; + content: ''; + width: 100%; + height: 7px; + background-color: #47be9b; + opacity: 0.5; + bottom: 0; + left: 0; + } + `, + grassTitle: css` + display: flex; + align-items: center; + gap: 10px; + `, + badgeTitle: css` + display: flex; + align-items: center; + justify-content: space-between; + `, + badgeLink: css` + font-size: 12px; + color: #9d9893 !important; + display: flex; + align-items: center; + gap: 5px; + `, + badgeButton: css` + display: flex; + height: auto; + min-width: auto; + padding: 0; + border: 2px solid transparent; + &.active { + border-color: #47be9b; + } + `, + badge: css` + border: 2px solid #47be9b; + `, +})); diff --git a/websites/poolc.org/src/components/my-page/MyPagePKSSection.tsx b/websites/poolc.org/src/components/my-page/MyPagePKSSection.tsx new file mode 100644 index 00000000..2f32fc7a --- /dev/null +++ b/websites/poolc.org/src/components/my-page/MyPagePKSSection.tsx @@ -0,0 +1,140 @@ +import { CopyOutlined, CheckOutlined, ArrowRightOutlined, SettingTwoTone, BookTwoTone, DeploymentUnitOutlined, EyeTwoTone, QuestionCircleFilled } from '@ant-design/icons'; +import { Button, Popover, Space, Typography, List } from 'antd'; +import { createStyles } from 'antd-style'; +import { ReactNode } from 'react'; +import useCopy from '~/hooks/useCopy'; +import MyPageArgoCDForm from './MyPageArgoCDForm'; + +export default function MyPagePKSSection({ jwtToken }: { jwtToken: string }) { + const { styles } = useStyles(); + const { isCopied, copy } = useCopy(); + + const linkList: { + title: ReactNode; + icon: JSX.Element; + link: string; + }[] = [ + { + title: 'kubectl 빠른 설정', + icon: , + link: 'https://github.com/PoolC/PKS-docs/tree/main/docs/user-guides', + }, + { + title: '전체 문서', + icon: , + link: 'https://github.com/PoolC/PKS-docs/tree/main', + }, + { + title: ( + + Argo CD + }> + + + + ), + icon: , + link: 'http://argocd.dev.poolc.org', + }, + { + title: '모니터링', + icon: , + link: 'http://mon.dev.poolc.org', + }, + ]; + + const code = dedent`kubectl config set-credentials pks --token=${jwtToken} + kubectl config set-cluster pks --server="https://165.132.131.121:6443" --insecure-skip-tls-verify=true + kubectl config set-context pks --cluster=pks --user=pks + kubectl config use-context pks`; + + return ( + + ( + + +
+ {item.icon} + {item.title} +
+ +
+
+ )} + /> +
+
+ Command +
+
{code}
+
+
+ ); +} + +const useStyles = createStyles(({ css }) => ({ + container: css({ + width: '100%', + }), + fullWidth: css({ + width: '100%', + }), + link: css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + }), + linkInner: css({ + display: 'flex', + alignItems: 'center', + gap: '20px', + }), + codeBlock: css({ + width: '100%', + border: '1px solid rgba(100, 100, 100, 0.2)', + borderRadius: '6px', + overflow: 'hidden', + }), + codeHeader: css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 12px', + background: 'rgba(150, 150, 150, 0.1)', + borderBottom: '1px solid rgba(100, 100, 100, 0.2)', + }), + codeLabel: css({ + fontSize: '12px', + fontWeight: '500', + color: 'rgba(0, 0, 0, 0.7)', + letterSpacing: '0.5px', + }), + code: css({ + whiteSpace: 'pre !important', + margin: '0 !important', + padding: '12px', + fontFamily: 'SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace', + background: 'rgba(250, 250, 250, 0.5)', + overflowX: 'auto', + }), +})); + +function dedent(text: TemplateStringsArray, ...args: unknown[]) { + let raw = text.raw[0]; + + for (let i = 0; i < args.length; i++) { + raw += String(args[i]) + text.raw[i + 1]; + } + + return raw + .split('\n') + .map((line) => line.trim()) + .join('\n'); +} diff --git a/websites/poolc.org/src/hooks/useCopy.ts b/websites/poolc.org/src/hooks/useCopy.ts new file mode 100644 index 00000000..fbca7179 --- /dev/null +++ b/websites/poolc.org/src/hooks/useCopy.ts @@ -0,0 +1,37 @@ +import { useState, useCallback } from 'react'; + +export default function useCopy() { + const [isCopied, setIsCopied] = useState(false); + + const copy = useCallback(async (text: string) => { + if (navigator.clipboard) { + await navigator.clipboard.writeText(text); + } else { + legacyCopy(text); + } + + setIsCopied(true); + + setTimeout(() => { + setIsCopied(false); + }, 1000); + }, []); + + return { + isCopied, + copy, + }; +} + +function legacyCopy(text: string) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); +} diff --git a/websites/poolc.org/src/lib/api-v2/queryKey.ts b/websites/poolc.org/src/lib/api-v2/queryKey.ts index 589ae5f1..bf8ceff4 100644 --- a/websites/poolc.org/src/lib/api-v2/queryKey.ts +++ b/websites/poolc.org/src/lib/api-v2/queryKey.ts @@ -43,4 +43,7 @@ export const queryKey = { all: ['notification.all'] as const, unread: ['notification.unread'] as const, }, + kubernetes: { + me: ['kubernetes.me'] as const, + }, }; diff --git a/websites/poolc.org/src/lib/api-v2/useAppSuspenseQueries.ts b/websites/poolc.org/src/lib/api-v2/useAppSuspenseQueries.ts index b244d3dd..26cf65e3 100644 --- a/websites/poolc.org/src/lib/api-v2/useAppSuspenseQueries.ts +++ b/websites/poolc.org/src/lib/api-v2/useAppSuspenseQueries.ts @@ -1,3 +1,3 @@ import { useSuspenseQueries } from '@tanstack/react-query'; -export const useAppSuspeneseQueries = useSuspenseQueries; +export const useAppSuspenseQueries = useSuspenseQueries; diff --git a/websites/poolc.org/src/pages/my-page/MyPage.tsx b/websites/poolc.org/src/pages/my-page/MyPage.tsx index 6bcb9ff4..623b5105 100644 --- a/websites/poolc.org/src/pages/my-page/MyPage.tsx +++ b/websites/poolc.org/src/pages/my-page/MyPage.tsx @@ -1,266 +1,36 @@ -import { Avatar, Button, List, Popover, Space, Typography } from 'antd'; import { createStyles } from 'antd-style'; -import { Link } from 'react-router-dom'; -import { ArrowRightOutlined, EditTwoTone, MessageTwoTone, QuestionCircleFilled, StarTwoTone, UserOutlined } from '@ant-design/icons'; +import { Suspense } from 'react'; +import { useLocation } from 'react-router'; import { Block, WhiteBlock } from '~/styles/common/Block.styles'; -import { BadgeControllerService, BaekjoonControllerService, MemberControllerService, queryKey, useAppMutation, useAppQueries } from '~/lib/api-v2'; -import { MENU } from '~/constants/menus'; -import MyPageGrassSection from '~/components/my-page/MyPageGrassSection'; -import { queryClient } from '~/lib/utils/queryClient'; -import { getProfileImageUrl } from '~/lib/utils/getProfileImageUrl'; -import getFileUrl from '~/lib/utils/getFileUrl'; - -const useStyles = createStyles(({ css }) => ({ - whiteBlock: css` - box-sizing: border-box; - padding: 30px 20px; - `, - wrapper: css` - width: 100%; - box-sizing: border-box; - `, - fullWidth: css` - width: 100%; - `, - link: css` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - text-decoration: inherit; - cursor: pointer; - `, - linkInner: css` - display: flex; - align-items: center; - gap: 20px; - `, - userName: css` - font-size: 24px; - font-weight: 700; - position: relative; - - &:before { - position: absolute; - content: ''; - width: 100%; - height: 7px; - background-color: #47be9b; - opacity: 0.5; - bottom: 0; - left: 0; - } - `, - grassTitle: css` - display: flex; - align-items: center; - gap: 10px; - `, - badgeTitle: css` - display: flex; - align-items: center; - justify-content: space-between; - `, - badgeLink: css` - font-size: 12px; - color: #9d9893 !important; - display: flex; - align-items: center; - gap: 5px; - `, - badgeButton: css` - display: flex; - height: auto; - min-width: auto; - padding: 0; - border: 2px solid transparent; - &.active { - border-color: #47be9b; - } - `, - badge: css` - border: 2px solid #47be9b; - `, -})); +import MyPageContainer from '~/components/my-page/MyPageContainer'; +import Skeleton from '~/components/common/Skeleton'; export default function MyPage() { - const { styles, cx } = useStyles(); - - const listData: { - title: string; - icon: JSX.Element; - link?: string; - onClick?: () => void; - }[] = [ - { - title: '회원 정보 수정', - icon: , - link: '/my-info', - }, - { - title: '내가 쓴 글', - icon: , - link: `/${MENU.MY_PAGE}/${MENU.MY_PAGE_MY_POSTS}`, - }, - { - title: '내가 스크랩한 글', - icon: , - link: `/${MENU.MY_PAGE}/${MENU.MY_PAGE_MY_SCRAPS}`, - }, - { - title: '쪽지', - icon: , - link: `/${MENU.MESSAGE}`, - }, - ]; + const { styles } = useStyles(); + const location = useLocation(); - const [{ data: myHour }, { data: me }, { data: badge }, { data: baekjoon }] = useAppQueries({ - queries: [ - { - queryKey: queryKey.member.hour, - queryFn: MemberControllerService.getMyActivityTimeUsingGet, - }, - { - queryKey: queryKey.member.me, - queryFn: MemberControllerService.getMeUsingGet, - }, - { - queryKey: queryKey.badge.badge, - queryFn: BadgeControllerService.getMyBadgeUsingGet, - }, - { - queryKey: queryKey.baekjoon.baekjoon, - queryFn: BaekjoonControllerService.getMyBaekjoonUsingGet, - }, - ], - }); - - const { mutate: selectBadge } = useAppMutation({ - mutationFn: BadgeControllerService.selectBadgeUsingPost, - }); - - const onBadgeButtonClick = (id: number) => { - if (me?.badge?.id === id) { - return; - } - - selectBadge( - { - badgeId: id, - }, - { - onSuccess() { - queryClient.invalidateQueries({ - queryKey: queryKey.member.me, - }); - }, - }, - ); - }; + const locationHash = location.hash.replace(/^#/, ''); return ( - - - - - - {me?.name}님 - {me?.badge && } - - {me?.introduction} - - - - 나의 활동시간 - {myHour?.hour ?? 0}시간 - {/* - {myHour?.hour ?? 0}시간 / {activityMinimumHour}시간 - - */} - - - - 풀씨 잔디 - - - 풀씨-백준 익스텐션을 설치하고 -
- 백준문제를 풀면 풀씨 잔디를 심을 수 있습니다. -
- - - -
- } - > - - - - {baekjoon?.data && } -
- - - 얻은 뱃지 - - 모든 뱃지보기 > - - - {badge?.data && badge.data.length > 0 ? ( - - {badge.data.map((el) => ( - - ))} - - ) : ( - 아직 뱃지가 없습니다. - )} - - - 나의 메뉴 - - item.link ? ( - - -
- {item.icon} - {item.title} -
- - -
- ) : ( - -
- {item.icon} - {item.title} -
- -
- ) - } - /> -
- +
+ }> + + +
); } + +const useStyles = createStyles(({ css }) => ({ + whiteBlock: css` + box-sizing: border-box; + padding: 30px 20px; + `, + wrapper: css` + width: 100%; + box-sizing: border-box; + `, +})); diff --git a/websites/poolc.org/vite.config.ts b/websites/poolc.org/vite.config.ts index fe65e0e7..da03864c 100644 --- a/websites/poolc.org/vite.config.ts +++ b/websites/poolc.org/vite.config.ts @@ -4,7 +4,6 @@ import path from 'path'; export default defineConfig(({ mode }) => { // @see https://stackoverflow.com/a/66389044 - // process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; process.env = Object.assign(process.env, loadEnv(mode, process.cwd())); return { diff --git a/yarn.lock b/yarn.lock index 5e5b6563..e594f5a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1697,6 +1697,7 @@ __metadata: dependencies: "@dialga/react-editor": "workspace:^" "@types/node": "npm:^18.15.3" + dotenv-cli: "npm:^10.0.0" eslint: "npm:^8.57.0" openapi-typescript-codegen: "npm:^0.28.0" prettier: "npm:^3.1.1" @@ -4515,6 +4516,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + "csstype@npm:^3.0.2, csstype@npm:^3.1.3": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -4851,6 +4863,43 @@ __metadata: languageName: node linkType: hard +"dotenv-cli@npm:^10.0.0": + version: 10.0.0 + resolution: "dotenv-cli@npm:10.0.0" + dependencies: + cross-spawn: "npm:^7.0.6" + dotenv: "npm:^17.1.0" + dotenv-expand: "npm:^11.0.0" + minimist: "npm:^1.2.6" + bin: + dotenv: cli.js + checksum: 10c0/c469e65167fc3d1ef7bc6f90c8b7a0accd245ad1cdb73da8c72b32ddb308550dc4d5bfcdae964ab1f8a247957d756afc7b050d0f88fa5868c05ff6d3dfb4c1ba + languageName: node + linkType: hard + +"dotenv-expand@npm:^11.0.0": + version: 11.0.7 + resolution: "dotenv-expand@npm:11.0.7" + dependencies: + dotenv: "npm:^16.4.5" + checksum: 10c0/d80b8a7be085edf351270b96ac0e794bc3ddd7f36157912939577cb4d33ba6492ebee349d59798b71b90e36f498d24a2a564fb4aa00073b2ef4c2a3a49c467b1 + languageName: node + linkType: hard + +"dotenv@npm:^16.4.5": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc + languageName: node + linkType: hard + +"dotenv@npm:^17.1.0": + version: 17.2.2 + resolution: "dotenv@npm:17.2.2" + checksum: 10c0/be66513504590aff6eccb14167625aed9bd42ce80547f4fe5d195860211971a7060949b57108dfaeaf90658f79e40edccd3f233f0a978bff507b5b1565ae162b + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1"