Skip to content

Commit f2d576c

Browse files
Honghyeonjilee0jae330inhachoiUjaa
authored
[release] dev -> main 브랜치 merge / 1.0.1version upgrade (#24)
* 🔨 refactor: Docker 이미지 최적화를 위한 코드 리팩토링 * 🙀 chore: 오타 수정 * 🐛 fix: COPY 경로 수정 * 🐛 fix: swagger-auto 명령어 추가 * 🙀 chore: 오타수정 * 🐛 fix: backend 이미지가 base-image를 사용하지 않으므로 새로운 패키지 매니저 설치 및 추가 파일 복사하는 로직 추가 * 🐛 fix: pnpm-lock 복사 코드 추가 * 🐛 fix: 서버 패키지 설치 안되는 오류 해결하기 위해 코드 수정 * 🐛 fix: eslint 경로 체크용 package 폴더 추가 복사 * 🐛 fix: 의존성을 복사해오고 dev용을 지우는 방식으로 수정 * 🐛 fix: pnpm-workspace 파일 복사 * 🙀 chore: cicd 스크립트 오타 수정 * 🙀 chore: copy 명령어 합침 * 🙀 chore: 코드 구조 변경 * 🐛 fix: COPY 경로 수정 * 🙀 chore: 경로 오타 수정 * 🔨 refactor: 배포시 swagger-auto 안 하게 로직 수정 및 prod 종속성에 필요한 패키지 dev 종속성에서 이동 * 🙀 chore: 경로 오타 수정 * 🙀 chore: 안쓰는 라이브러리 삭제 * 🚨 !HOTFIX!: env 설정 누락된 코드 추가 * 🚨 !HOTFIX!: env 설정 누락된 코드 추가 * [#10] 홈페이지 가상스크롤 적용 (#14) * ✨ feat: scroll의 Y 좌표를 리턴하는 useScroll 커스텀 훅 추가 * 🚚 rename: hooks/css에 있던 useWindowSize 훅 폴더 위치 변경 * ✨ feat: 가상스크롤 구현 중 * 🙀 chore: 함수명 오타 수정 * ✨ feat: 가상스크롤 커스텀훅 추가 * ✨ feat: WorkspaceGrid 컴포넌트에 가상스크롤 적용 * 🙀 chore: grid의 총 높이 계산할 때 반올림 적용 * [#11] Google Analytics 적용 (#15) * ✨ feat: ga4 코드 추가 * 🔨 refactor: 로컬 환경 디버깅용 코드로 전환 * 🙀 chore: GA 디버깅 코드 삭제 * ✨ feat: 이벤트 트레이싱 * Update boolock-dev-cicd.yml * Update boolock-dev-cicd.yml * [#18] useQuery, useInfiniteQuery -> useSuspenseQuery, useSuspenseInfiniteQuery 교체 (#19) * 🔨 refactor: useQuery를 useSuspenseQuery로 교체 및 error 처러 코드 삭제 * 🔨 refactor: 로딩 UI 및 에러 처리 코드 삭제 * 🔨 refactor: suspense 적용 및 error boundy fallback에 workspace 에러 페이지 설정 * 🔨 refactor: useInfinityQuery -> useSuspenseQuery로 교체 * 🙀 chore: 400 에러와 404에러의 에러 코드 및 상태 메세지가 잘못 설정 되어 있어 올바르게 수정 * 🐛 fix: 기존 에러 발생 시 500 에러로만 응답하는 문제 해결 * 🔨 refactor: workspaceContainer에서 워크스페이스 데이터를 렌더링하고 페칭하는 부분을 분리함 * 🙀 chore: 컴포넌트 분리에 따른 코드 수정 * 🙀 chore: github action 에서 파일명 변경을 감지하지 못한 문제로 인해 파일명 변경 * 🙀 chore: 불필요한 console.log 삭제 * [#21] 홈페이지 로딩 시 스켈레톤UI에 그리드가 적용되지 않는 문제 해결, 워크스페이스 로딩 화면 조건부 렌더링 (#22) * 🐛 fix: 스켈레톤UI에 그리드가 안되는 문제 해결 * ✨ feat: 워크스페이스 로딩 시간이 0.05초 이상일 때만 로딩 화면이 렌더링되도록 변경 * 🚨 !HOTFIX!: error가 발생할 때 return 해야하는데 그렇지 않을 때 return 하여 workspace가 초기화되지않는 문제 해결 * 🚨 !HOTFIX!: workflow on 설정 main push 일 경우로 수정 --------- Co-authored-by: YEONGJAE LEE <71895560+lee0jae330@users.noreply.github.com> Co-authored-by: Gyeungil Choi <chlruddlf73@naver.com> Co-authored-by: Yujin <yujinyll9148@gmail.com> Co-authored-by: lee0jae330 <xingxing2001@cau.ac.kr>
1 parent dcd92f6 commit f2d576c

39 files changed

+507
-174
lines changed

.github/workflows/boolock-dev-cicd.yml

+8-7
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ name: ci/cd action
22

33
on:
44
push:
5-
branches: ['dev']
6-
pull_request:
7-
branches: ['dev']
5+
branches: ['main']
86

97
jobs:
108
build:
@@ -42,6 +40,7 @@ jobs:
4240
- name: Set FE .env
4341
run: |
4442
echo "VITE_SERVER_URL=http://localhost:3000" > apps/client/.env
43+
echo "VITE_MIXPANEL_TOKEN=${{ secrets.VITE_MIXPANEL_TOKEN }}" >> apps/client/.env
4544
echo "VITE_STATIC_STORAGE_URL=${{ secrets.VITE_STATIC_STORAGE_URL }}" >> apps/client/.env
4645
4746
- name: Set Nginx SSL files
@@ -98,18 +97,20 @@ jobs:
9897
password: ${{ secrets.SSH_PASSWORD }}
9998
port: ${{ secrets.SSH_PORT }}
10099
script: |
101-
cd boolock/web31-BooLock
102-
git checkout dev
103-
git pull origin dev
100+
cd boolock/refactor-web31-BooLock
101+
git checkout main
102+
git pull origin main
104103
105104
echo "MONGO_URI=${{ secrets.DEPLOY_MONGO_URI }}" > apps/server/.env
106105
echo "IS_LOCAL=false" >> apps/server/.env
107-
echo "${{ secrets.DEPLOY_SERVER_CORS_ACCEPT }}" >> apps/server/.env
106+
echo "SERVER_CORS_ACCEPT=${{ secrets.DEPLOY_SERVER_CORS_ACCEPT }}" >> apps/server/.env
108107
echo "S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}" >> apps/server/.env
109108
echo "S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}" >> apps/server/.env
110109
echo "S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }}" >> apps/server/.env
110+
echo "NODE_ENV=production" >> apps/server/.env
111111
112112
echo "VITE_SERVER_URL=${{ secrets.DEPLOY_VITE_SERVER_URL }}" > apps/client/.env
113+
echo "VITE_MIXPANEL_TOKEN=${{ secrets.VITE_MIXPANEL_TOKEN }}" >> apps/client/.env
113114
echo "VITE_STATIC_STORAGE_URL=${{ secrets.VITE_STATIC_STORAGE_URL }}" >> apps/client/.env
114115
115116
sudo docker compose pull

apps/client/Dockerfile

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
FROM base-image AS frontend-build
2+
23
WORKDIR /app/apps/client
34
COPY ./apps/client .
4-
COPY --from=base-image /app/packages /app/packages
5-
RUN pnpm install --offline --frozen-lockfile
6-
RUN pnpm run build
5+
RUN pnpm install --offline --frozen-lockfile &&\
6+
pnpm run build
77

88
FROM nginx:alpine AS frontend
9-
COPY --from=frontend-build /app/apps/client/dist /usr/share/nginx/html
9+
1010
COPY /apps/client/nginx.conf /etc/nginx/conf.d/default.conf
1111
COPY /apps/client/ssl /etc/nginx/ssl
12+
RUN chmod 644 /etc/nginx/ssl/fullchain.pem &&\
13+
chmod 600 /etc/nginx/ssl/privkey.pem &&\
14+
chown -R nginx:nginx /usr/share/nginx/html /etc/nginx/ssl
1215

13-
RUN chmod -R 755 /usr/share/nginx/html
14-
RUN chmod 644 /etc/nginx/ssl/fullchain.pem
15-
RUN chmod 600 /etc/nginx/ssl/privkey.pem
16-
RUN chown -R nginx:nginx /usr/share/nginx/html /etc/nginx/ssl
16+
COPY --from=frontend-build /app/apps/client/dist /usr/share/nginx/html/
17+
RUN chmod -R 755 /usr/share/nginx/html
1718

1819
EXPOSE 80 443
1920
CMD ["nginx", "-g", "daemon off;"]

apps/client/index.html

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
<!doctype html>
22
<html lang="ko">
33
<head>
4+
<!-- Google tag (gtag.js) -->
5+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SMXGZQRD0S"></script>
6+
<script>
7+
window.dataLayer = window.dataLayer || [];
8+
function gtag() {
9+
dataLayer.push(arguments);
10+
}
11+
gtag('js', new Date());
12+
gtag('config', 'G-SMXGZQRD0S');
13+
</script>
414
<!-- 기본 meta 태그 및 link -->
515
<meta charset="UTF-8" />
616
<link rel="icon" type="image/svg+xml" href="/images/boolock_logo.png" />
@@ -14,10 +24,7 @@
1424
<meta name="author" content="부스트캠프 9기 Web31팀" />
1525
<!-- Open Graph -->
1626
<meta property="og:title" content="BooLock" />
17-
<meta
18-
property="og:description"
19-
content="쉽게 배우는 블록 코딩"
20-
/>
27+
<meta property="og:description" content="쉽게 배우는 블록 코딩" />
2128
<meta property="og:url" content="https://boolock.site/" />
2229
<meta property="og:image" content="/images/boolock_thumnail.png" />
2330
<meta property="og:image:alt" content="블록코딩 플랫폼 BooLock의 썸네일" />

apps/client/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
"codemirror": "^6.0.1",
2323
"html2canvas": "^1.4.1",
2424
"init": "^0.1.2",
25+
"mixpanel-browser": "^2.58.0",
2526
"react": "^18.3.1",
2627
"react-dom": "^18.3.1",
2728
"react-helmet-async": "^2.0.5",
2829
"react-hot-toast": "^2.4.1",
2930
"react-router-dom": "^6.27.0",
3031
"react-spinners": "^0.14.1",
31-
"react-youtube": "^10.1.0",
3232
"storybook": "^8.4.1",
3333
"uuid": "^11.0.3",
3434
"zustand": "^5.0.1"
@@ -44,6 +44,7 @@
4444
"@storybook/react": "8.4.2",
4545
"@storybook/react-vite": "8.4.2",
4646
"@storybook/test": "8.4.2",
47+
"@types/mixpanel-browser": "^2.51.0",
4748
"@types/react": "^18.3.12",
4849
"@types/react-dom": "^18.3.1",
4950
"@types/react-router-dom": "^5.3.3",
@@ -55,7 +56,6 @@
5556
"eslint-plugin-react-refresh": "^0.4.14",
5657
"eslint-plugin-storybook": "^0.11.0",
5758
"postcss": "^8.4.47",
58-
"prop-types": "15.8.1",
5959
"tailwindcss": "^3.4.14",
6060
"vite": "^5.4.10",
6161
"vite-plugin-svgr": "^4.3.0"

apps/client/src/app/App.tsx

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { DelayedFallback, Loading } from '@/shared/ui';
2+
import { ErrorPage, WorkspaceErrorPage } from '@/pages';
13
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
2-
import { ToasterWithMax } from '@/shared/ui';
3-
import { ErrorPage } from '@/pages/ErrorPage/ErrorPage';
4-
import { lazy, Suspense } from 'react';
5-
import { Loading } from '@/shared/ui';
4+
import { Suspense, lazy } from 'react';
5+
66
import { Helmet } from 'react-helmet-async';
7+
import { ToasterWithMax } from '@/shared/ui';
78

89
// lazy 로딩
910
const HomePage = lazy(() =>
@@ -47,12 +48,19 @@ const router = createBrowserRouter([
4748
<title>BooLock - 작업 공간</title>
4849
<meta name="description" content={`워크스페이스에서 HTML과 CSS를 연습해보세요.`} />
4950
</Helmet>
50-
<Suspense fallback={<Loading />}>
51+
52+
<Suspense
53+
fallback={
54+
<DelayedFallback>
55+
<Loading />
56+
</DelayedFallback>
57+
}
58+
>
5159
<WorkspacePage />
5260
</Suspense>
5361
</>
5462
),
55-
errorElement: <ErrorPage />,
63+
errorElement: <WorkspaceErrorPage />,
5664
},
5765
{
5866
path: '*',

apps/client/src/app/main.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { App } from './App';
66
import { StrictMode } from 'react';
77
import { createRoot } from 'react-dom/client';
88
import { HelmetProvider } from 'react-helmet-async';
9+
import { initMixpanel } from '@/shared/utils';
910

1011
const container = document.getElementById('root');
1112
const root = createRoot(container!);
1213

1314
const queryClient = new QueryClient();
15+
initMixpanel();
1416

1517
root.render(
1618
<StrictMode>

apps/client/src/entities/workspace/CssCategoryButton/CssCategoryButton.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type CssCategoryButtonProps = {
1212
*/
1313
export const CssCategoryButton = ({ cssCategory }: CssCategoryButtonProps) => {
1414
const { selectedCssCategory, setSelectedCssCategory } = useCssPropsStore();
15+
1516
return (
1617
<button
1718
key={cssCategory}

apps/client/src/entities/workspace/SaveButton/SaveButton.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as Blockly from 'blockly/core';
33
import { useCssPropsStore, useResetCssStore, useWorkspaceStore } from '@/shared/store';
44

55
import { Spinner } from '@/shared/ui';
6-
import { capturePreview } from '@/shared/utils';
6+
import { capturePreview, trackEvent } from '@/shared/utils';
77
import { cssStyleToolboxConfig } from '@/shared/blockly';
88
import toast from 'react-hot-toast';
99
import { useParams } from 'react-router-dom';
@@ -25,6 +25,7 @@ export const SaveButton = () => {
2525
const [isCapture, setIsCapture] = useState<boolean>(false);
2626

2727
const handleClick = async () => {
28+
trackEvent('workspace_saved');
2829
try {
2930
const canvas = Blockly.serialization.workspaces.save(workspace!) as any;
3031
setIsCapture(true);

apps/client/src/pages/HomePage/HomePage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Banner, HomeHeader, WorkspaceContainer, WorkspaceModal } from '@/widgets';
1+
import { Banner, HomeHeader, WorkspaceModal, WorkspaceSection } from '@/widgets';
22
import { useClassBlockStore, useLoadingStore, useWorkspaceStore } from '@/shared/store';
33

44
import { Loading } from '@/shared/ui';
@@ -26,7 +26,7 @@ export const HomePage = () => {
2626
<div className="flex h-full w-full flex-col items-center">
2727
<HomeHeader />
2828
<Banner />
29-
<WorkspaceContainer />
29+
<WorkspaceSection />
3030
<WorkspaceModal />
3131
</div>
3232
</>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ErrorPage, NotFound } from '@/pages';
2+
3+
import toast from 'react-hot-toast';
4+
import { useRouteError } from 'react-router-dom';
5+
6+
export const WorkspaceErrorPage = () => {
7+
const error: any = useRouteError();
8+
const statusCode = error?.response?.statusCode || error?.status;
9+
if (statusCode === 404) {
10+
toast.error('워크스페이스 정보 불러오기 실패');
11+
return <NotFound />;
12+
}
13+
return <ErrorPage />;
14+
};

apps/client/src/pages/Workspacepage/WorkspacePage.tsx

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { ImageTagModal, CoachMark, WorkspaceContent, WorkspacePageHeader } from '@/widgets';
1+
import { CoachMark, ImageTagModal, WorkspaceContent, WorkspacePageHeader } from '@/widgets';
2+
import { useEffect, useLayoutEffect } from 'react';
23
import { useGetWorkspace, usePreventLeaveWorkspacePage } from '@/shared/hooks';
3-
import { Loading } from '@/shared/ui';
4-
import { NotFound } from '@/pages/NotFound/NotFound';
5-
import { useParams } from 'react-router-dom';
6-
import { useLayoutEffect, useEffect } from 'react';
4+
75
import { useCoachMarkStore } from '@/shared/store/useCoachMarkStore';
6+
import { useParams } from 'react-router-dom';
87

98
/**
109
*
@@ -13,7 +12,7 @@ import { useCoachMarkStore } from '@/shared/store/useCoachMarkStore';
1312
*/
1413
export const WorkspacePage = () => {
1514
const { workspaceId } = useParams();
16-
const { isPending, isError } = useGetWorkspace(workspaceId as string);
15+
useGetWorkspace(workspaceId as string);
1716
usePreventLeaveWorkspacePage();
1817
const { currentStep, isCoachMarkOpen, openCoachMark } = useCoachMarkStore();
1918
const toolboxDiv = document.querySelector('.blocklyToolboxDiv');
@@ -36,14 +35,9 @@ export const WorkspacePage = () => {
3635
}
3736
}, [currentStep, toolboxDiv]);
3837

39-
if (isError) {
40-
return <NotFound />;
41-
}
42-
4338
return (
4439
<>
4540
<div className="flex h-screen flex-col">
46-
{isPending && <Loading />}
4741
{isCoachMarkOpen && <CoachMark />}
4842
<WorkspacePageHeader />
4943
<WorkspaceContent />

apps/client/src/pages/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export { HomePage } from './HomePage/HomePage';
22
export { NotFound } from './NotFound/NotFound';
33
export { WorkspacePage } from './Workspacepage/WorkspacePage';
4+
export { ErrorPage } from './ErrorPage/ErrorPage';
5+
export { WorkspaceErrorPage } from './WorkspaceErrorPage/WorkspaceErrorPage';

apps/client/src/shared/hooks/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export { useSaveWorkspace } from './queries/useSaveWorkspace';
77
export { usePostImage } from './queries/usePostImage';
88
export { useDeleteImage } from './queries/useDeleteImage';
99

10-
export { useWindowSize } from './css/useWindowSize';
1110
export { useCssTooltip } from './css/useCssTooltip';
1211
export { useCssOptions } from './css/useCssOptions';
1312
export { useCssOptionItem } from './css/useCssOptionItem';
@@ -16,3 +15,6 @@ export { workspaceKeys } from './query-key/workspaceKeys';
1615

1716
export { usePreventLeaveWorkspacePage } from './usePreventLeaveWorkspacePage';
1817
export { useInfiniteScroll } from './useInfiniteScroll';
18+
export { useScrollPosition } from './useScrollPosition';
19+
export { useWindowSize } from './useWindowSize';
20+
export { useVirtualScroll } from './useVirtualScroll';

apps/client/src/shared/hooks/queries/useGetWorkspace.ts

+5-16
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import { createUserId, getUserId, removeCssClassNamePrefix } from '@/shared/util
33
import {
44
useClassBlockStore,
55
useCssPropsStore,
6+
useImageModalStore,
67
useResetCssStore,
78
useWorkspaceChangeStatusStore,
89
useWorkspaceStore,
9-
useImageModalStore,
1010
} from '@/shared/store';
1111

1212
import { WorkspaceApi } from '@/shared/api';
13-
import toast from 'react-hot-toast';
1413
import { useEffect } from 'react';
15-
import { useQuery } from '@tanstack/react-query';
14+
import { useSuspenseQuery } from '@tanstack/react-query';
1615
import { workspaceKeys } from '@/shared/hooks';
1716

1817
export const useGetWorkspace = (workspaceId: string) => {
@@ -24,29 +23,19 @@ export const useGetWorkspace = (workspaceId: string) => {
2423
const { resetChangedStatusState } = useWorkspaceChangeStatusStore();
2524
const { setIsResetCssChecked } = useResetCssStore();
2625
const { setInitialImageMap, setInitialImageList } = useImageModalStore();
27-
const { data, isPending, isError } = useQuery({
26+
const { data, isPending, isError } = useSuspenseQuery({
2827
queryKey: workspaceKeys.detail(workspaceId),
2928
queryFn: () => {
29+
resetChangedStatusState();
3030
return workspaceApi.getWorkspace(userId, workspaceId);
3131
},
3232
});
3333

3434
useEffect(() => {
35-
resetChangedStatusState();
36-
}, []);
37-
38-
useEffect(() => {
39-
if (isError) {
40-
toast.error('워크스페이스 정보 불러오기 실패');
41-
return;
42-
}
43-
if (!data) {
35+
if (isError || !data || !data.workspaceDto) {
4436
return;
4537
}
4638

47-
if (!data.workspaceDto) {
48-
return;
49-
}
5039
setName(data.workspaceDto.name);
5140
Object.keys(data.workspaceDto.totalCssPropertyObj).forEach((className) => {
5241
createCssClassBlock(className);

apps/client/src/shared/hooks/queries/useGetWorkspaceList.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { WorkspaceApi } from '@/shared/api';
21
import { createUserId, getUserId } from '@/shared/utils';
3-
import { useInfiniteQuery } from '@tanstack/react-query';
2+
3+
import { WorkspaceApi } from '@/shared/api';
4+
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
45
import { workspaceKeys } from '@/shared/hooks';
56
export const useGetWorkspaceList = () => {
67
const workspaceApi = WorkspaceApi();
@@ -12,7 +13,7 @@ export const useGetWorkspaceList = () => {
1213
isFetchingNextPage,
1314
isError,
1415
data: workspaceList,
15-
} = useInfiniteQuery({
16+
} = useSuspenseInfiniteQuery({
1617
queryKey: workspaceKeys.list(),
1718
queryFn: async ({ pageParam }) => {
1819
const isNewUser = !getUserId();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
export const useScrollPosition = () => {
4+
const [scrollPosition, setScrollPosition] = useState<number>(0);
5+
const ticking = useRef(false);
6+
7+
useEffect(() => {
8+
const onScroll = () => {
9+
if (!ticking.current) {
10+
requestAnimationFrame(() => {
11+
setScrollPosition(window.scrollY);
12+
ticking.current = false;
13+
});
14+
ticking.current = true;
15+
}
16+
};
17+
window.addEventListener('scroll', onScroll);
18+
19+
return () => {
20+
window.removeEventListener('scroll', onScroll);
21+
};
22+
}, []);
23+
24+
return { scrollPosition };
25+
};

0 commit comments

Comments
 (0)