Skip to content

Commit

Permalink
Merge pull request #610 from woowacourse-teams/dev/fe
Browse files Browse the repository at this point in the history
[FE] 코드잽 프로덕션 v1.1.1 배포
  • Loading branch information
zangsu authored Sep 6, 2024
2 parents b6ad7d3 + f8c4fa8 commit 99eafb6
Show file tree
Hide file tree
Showing 19 changed files with 257 additions and 118 deletions.
17 changes: 11 additions & 6 deletions .github/workflows/frontend_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ jobs:
with:
node-version: 20

- name: .env.production 파일 생성
- name: 환경 파일 생성
run: |
echo "REACT_APP_API_URL=${{ secrets.REACT_APP_API_URL }}" > ${{ env.frontend-directory }}/.env.production
echo "REACT_APP_BASE_URL=${{ secrets.REACT_APP_BASE_URL }}" >> ${{ env.frontend-directory }}/.env.production
if [ "${{ github.ref_name }}" == "main" ]; then
echo "REACT_APP_API_URL=${{ secrets.REACT_APP_API_URL }}" > ${{ env.frontend-directory }}/.env.production
echo "REACT_APP_BASE_URL=${{ secrets.REACT_APP_BASE_URL }}" >> ${{ env.frontend-directory }}/.env.production
else
echo "REACT_APP_API_URL=${{ secrets.REACT_APP_BETA_API_URL }}" > ${{ env.frontend-directory }}/.env.development
echo "REACT_APP_BASE_URL=${{ secrets.REACT_APP_BETA_BASE_URL }}" >> ${{ env.frontend-directory }}/.env.development
fi
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> ${{ env.frontend-directory }}/.env.production
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> ${{ env.frontend-directory }}/.env.production
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> ${{ env.frontend-directory }}/.env.sentry-build-plugin
Expand All @@ -41,7 +47,7 @@ jobs:
run: npm run build
working-directory: ${{ env.frontend-directory }}

- name: Artifact 업로드
- name: 아티팩트 업로드
uses: actions/upload-artifact@v4
with:
name: code-zap-front
Expand All @@ -63,8 +69,7 @@ jobs:
with:
name: code-zap-front
path: ${{ secrets.FRONT_DIRECTORY }}

- name: 파일 S3로 이동
- name: S3로 이동
run: |
if [ "${{ github.ref_name }}" == "main" ]; then
aws s3 cp --recursive ${{ secrets.FRONT_DIRECTORY }} s3://techcourse-project-2024/code-zap
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/api/customFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export const customFetch = async <T>({ url, headers, method = 'GET', body }: Pro

if (!response.ok) {
const error: CustomError = {
type: responseBody.type || 'UnknownError',
title: responseBody.title || 'Error',
type: responseBody?.type || 'UnknownError',
title: responseBody?.title || 'Error',
status: response.status,
detail: responseBody.detail || 'An error occurred',
detail: responseBody?.detail || 'An error occurred',
instance: url,
};

Expand Down
8 changes: 3 additions & 5 deletions frontend/src/api/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
TemplateListResponse,
TemplateUploadRequest,
TemplateListRequest,
CustomError,
} from '@/types';
import { SortingOption } from '@/types';
import { customFetch } from './customFetch';
Expand Down Expand Up @@ -102,16 +103,13 @@ export const getTemplate = async (id: number) => {
throw new Error(response.detail);
};

export const postTemplate = async (newTemplate: TemplateUploadRequest) => {
const response = await customFetch({
export const postTemplate = async (newTemplate: TemplateUploadRequest): Promise<void | CustomError> =>
await customFetch({
method: 'POST',
url: `${TEMPLATE_API_URL}`,
body: JSON.stringify(newTemplate),
});

return response;
};

export const editTemplate = async ({ id, template }: { id: number; template: TemplateEditRequest }): Promise<void> => {
await customFetch({
method: 'POST',
Expand Down
22 changes: 19 additions & 3 deletions frontend/src/components/CategoryEditModal/CategoryEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PencilIcon, SpinArrowIcon, TrashcanIcon } from '@/assets/images';
import { Text, Modal, Input, Flex, Button } from '@/components';
import { useCategoryNameValidation } from '@/hooks/category';
import { useCategoryDeleteMutation, useCategoryEditMutation, useCategoryUploadMutation } from '@/queries/category';
import { validateCategoryName } from '@/service/validates';
import { theme } from '@/style/theme';
import type { Category, CustomError } from '@/types';
import * as S from './CategoryEditModal.style';
Expand All @@ -14,9 +15,16 @@ interface CategoryEditModalProps {
toggleModal: () => void;
categories: Category[];
handleCancelEdit: () => void;
onDeleteCategory: (deletedIds: number[]) => void;
}

const CategoryEditModal = ({ isOpen, toggleModal, categories, handleCancelEdit }: CategoryEditModalProps) => {
const CategoryEditModal = ({
isOpen,
toggleModal,
categories,
handleCancelEdit,
onDeleteCategory,
}: CategoryEditModalProps) => {
const [editedCategories, setEditedCategories] = useState<Record<number, string>>({});
const [categoriesToDelete, setCategoriesToDelete] = useState<number[]>([]);
const [newCategories, setNewCategories] = useState<{ id: number; name: string }[]>([]);
Expand All @@ -38,6 +46,12 @@ const CategoryEditModal = ({ isOpen, toggleModal, categories, handleCancelEdit }
const isCategoryNew = (id: number) => newCategories.some((category) => category.id === id);

const handleNameInputChange = (id: number, name: string) => {
const errorMessage = validateCategoryName(name);

if (errorMessage && name.length > 0) {
return;
}

if (isCategoryNew(id)) {
setNewCategories((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category)));
} else {
Expand Down Expand Up @@ -89,7 +103,10 @@ const CategoryEditModal = ({ isOpen, toggleModal, categories, handleCancelEdit }
}

try {
await Promise.all(categoriesToDelete.map((id) => deleteCategory({ id })));
if (categoriesToDelete.length > 0) {
await Promise.all(categoriesToDelete.map((id) => deleteCategory({ id })));
onDeleteCategory(categoriesToDelete);
}

await Promise.all(
Object.entries(editedCategories).map(async ([id, name]) => {
Expand All @@ -104,7 +121,6 @@ const CategoryEditModal = ({ isOpen, toggleModal, categories, handleCancelEdit }
await Promise.all(newCategories.map((category) => postCategory({ name: category.name })));

resetState();

toggleModal();
} catch (error) {
console.error((error as CustomError).detail);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ const CategoryFilterMenu = ({ categories, onSelectCategory }: CategoryMenuProps)
}
};

const handleCategoryDelete = (deletedIds: number[]) => {
if (deletedIds.includes(selectedId)) {
setSelectedId(0);
onSelectCategory(0);
}
};

const [defaultCategory, ...userCategories] = categories.length ? categories : [{ id: 0, name: '' }];

const indexById: Record<number, number> = useMemo(() => {
Expand Down Expand Up @@ -82,6 +89,7 @@ const CategoryFilterMenu = ({ categories, onSelectCategory }: CategoryMenuProps)
toggleModal={toggleEditModal}
categories={userCategories}
handleCancelEdit={toggleEditModal}
onDeleteCategory={handleCategoryDelete}
/>
</S.CategoryContainer>
{isMenuOpen && <S.Backdrop onClick={toggleMenu} />}
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/components/Footer/Footer.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import styled from '@emotion/styled';

import { theme } from '@/style/theme';

export const FooterContainer = styled.footer`
display: flex;
gap: 2rem;
align-items: center;
justify-content: space-between;
padding-top: 4rem;
color: ${theme.color.light.secondary_500};
`;

export const ContactEmail = styled.a`
text-decoration: none;
&:hover {
text-decoration: underline;
}
`;
22 changes: 22 additions & 0 deletions frontend/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Text } from '@/components';
import * as S from './Footer.style';

const Footer = () => (
<S.FooterContainer>
<Text.Small color='inherit'>
Copyright{' '}
<Text.Small color='inherit' weight='bold'>
Codezap
</Text.Small>{' '}
© All rights reserved.
</Text.Small>
<S.ContactEmail href='mailto:[email protected]'>
<Text.Small color='inherit' weight='bold'>
문의 :
</Text.Small>{' '}
<Text.Small color='inherit'>[email protected]</Text.Small>{' '}
</S.ContactEmail>
</S.FooterContainer>
);

export default Footer;
3 changes: 2 additions & 1 deletion frontend/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { Outlet, useLocation } from 'react-router-dom';

import { Header } from '@/components';
import { Footer, Header } from '@/components';
import { useHeaderHeight } from '@/hooks/utils/useHeaderHeight';
import { NotFoundPage } from '@/pages';
import * as S from './Layout.style';
Expand Down Expand Up @@ -35,6 +35,7 @@ const Layout = () => {
)}
</QueryErrorResetBoundary>
</S.Wrapper>
<Footer />
</S.LayoutContainer>
);
};
Expand Down
30 changes: 24 additions & 6 deletions frontend/src/components/SourceCodeEditor/SourceCodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ViewUpdate } from '@codemirror/view';
import { type LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs';
import { quietlight } from '@uiw/codemirror-theme-quietlight';
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { ChangeEvent, useRef } from 'react';

import { ToastContext } from '@/contexts';
import { useCustomContext } from '@/hooks/utils';
import { validateFileName } from '@/service';
import { validateFileName, validateSourceCode } from '@/service';
import { getLanguageByFilename } from '@/utils';
import * as S from './SourceCodeEditor.style';

Expand All @@ -19,6 +20,7 @@ interface Props {

const SourceCodeEditor = ({ index, fileName, content, onChangeContent, onChangeFileName }: Props) => {
const codeMirrorRef = useRef<ReactCodeMirrorRef>(null);
const previousContentRef = useRef<string>(content);
const { failAlert } = useCustomContext(ToastContext);

const focusCodeMirror = () => {
Expand All @@ -32,19 +34,35 @@ const SourceCodeEditor = ({ index, fileName, content, onChangeContent, onChangeF
};

const handleFileNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const errorMessage = validateFileName(event.target.value);
const newFileName = event.target.value;

const errorMessage = validateFileName(newFileName);

if (errorMessage) {
failAlert(errorMessage);

return;
}

onChangeFileName(event.target.value);
onChangeFileName(newFileName);
};

const handleContentChange = (value: string) => {
onChangeContent(value);
const handleContentChange = (value: string, viewUpdate: ViewUpdate) => {
const errorMessage = validateSourceCode(value);

if (errorMessage) {
failAlert(errorMessage);

const previousContent = previousContentRef.current;
const transaction = viewUpdate.state.update({
changes: { from: 0, to: value.length, insert: previousContent },
});

viewUpdate.view.dispatch(transaction);
} else {
onChangeContent(value);
previousContentRef.current = value;
}
};

return (
Expand All @@ -53,7 +71,7 @@ const SourceCodeEditor = ({ index, fileName, content, onChangeContent, onChangeF
value={fileName}
onChange={handleFileNameChange}
placeholder={'파일명.js'}
autoFocus={index !== 0 ? true : false}
autoFocus={index !== 0}
/>
<CodeMirror
ref={codeMirrorRef}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/TemplateEdit/TemplateEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ const TemplateEdit = ({
</S.UnderlineInputWrapper>

<Input size='large' variant='text'>
<Input.TextField placeholder='설명을 입력해주세요' value={description} onChange={handleDescriptionChange} />
<Input.TextField
placeholder='이 템플릿을 언제 다시 쓸 것 같나요?'
value={description}
onChange={handleDescriptionChange}
/>
</Input>

{sourceCodes.map((sourceCode, index) => (
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { default as TemplateGrid } from './TemplateGrid/TemplateGrid';
export { default as Text } from './Text/Text';
export { default as Toast } from './Toast/Toast';
export { default as Guide } from './Guide/Guide';
export { default as Footer } from './Footer/Footer';

// Skeleton UI
export { default as LoadingBall } from './LoadingBall/LoadingBall';
14 changes: 11 additions & 3 deletions frontend/src/hooks/template/useTemplateUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ export const useTemplateUpload = () => {
};

const handleSaveButtonClick = async () => {
if (validateTemplate()) {
failAlert(validateTemplate());
const errorMessage = validateTemplate();

if (errorMessage) {
failAlert(errorMessage);

return;
}
Expand All @@ -101,7 +103,13 @@ export const useTemplateUpload = () => {
};

await uploadTemplate(newTemplate, {
onSuccess: () => {
onSuccess: (res) => {
if (res?.status === 400 || res?.status === 404) {
failAlert('템플릿 생성에 실패했습니다. 다시 한 번 시도해주세요');

return;
}

navigate(END_POINTS.MY_TEMPLATES);
},
});
Expand Down
Loading

0 comments on commit 99eafb6

Please sign in to comment.