From 511c8b6db3cb66a3b630b318ab95b94b91ff2edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:09:47 +0900 Subject: [PATCH 1/9] =?UTF-8?q?refactor(pages):=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=9E=9C=EB=8D=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A4=91=EB=B3=B5=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=A7=89=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateEditPage/TemplateEditPage.tsx | 106 +++++++++---- .../TemplateUploadPage/TemplateUploadPage.tsx | 147 ++++++++++++------ 2 files changed, 173 insertions(+), 80 deletions(-) diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index 83b0ad0e4..468b9a6d3 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -1,7 +1,17 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { PlusIcon, PrivateIcon, PublicIcon } from '@/assets/images'; -import { Button, Input, SelectList, SourceCodeEditor, Text, CategoryDropdown, TagInput, Toggle } from '@/components'; +import { + Button, + Input, + SelectList, + SourceCodeEditor, + Text, + CategoryDropdown, + TagInput, + LoadingBall, + Toggle, +} from '@/components'; import { useInput, useSelectList, useToast } from '@/hooks'; import { useAuth } from '@/hooks/authentication'; import { useCategory } from '@/hooks/category'; @@ -46,6 +56,8 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { const tagProps = useTag(initTags); const [visibility, setVisibility] = useState(template.visibility); + const [isSaving, setIsSaving] = useState(false); + const isSavingRef = useRef(false); const { currentOption: currentFile, linkedElementRefs: sourceCodeRefs, handleSelectOption } = useSelectList(); @@ -53,40 +65,25 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { const { failAlert } = useToast(); - const validateTemplate = () => { - if (!title) { - return '제목을 입력해주세요'; - } - - if (sourceCodes.filter(({ filename }) => !filename || filename.trim() === '').length) { - return '파일명을 입력해주세요'; - } - - if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { - return '소스코드 내용을 입력해주세요'; - } - - return ''; - }; - const handleCancelButton = () => { toggleEditButton(); }; const handleSaveButtonClick = async () => { + if (isSavingRef.current) { + return; + } + if (validateTemplate()) { failAlert(validateTemplate()); return; } - const orderedSourceCodes = sourceCodes.map((sourceCode, index) => ({ - ...sourceCode, - ordinal: index + 1, - })); + isSavingRef.current = true; + setIsSaving(true); - const createSourceCodes = orderedSourceCodes.filter((sourceCode) => !sourceCode.id); - const updateSourceCodes = orderedSourceCodes.filter((sourceCode) => sourceCode.id); + const { createSourceCodes, updateSourceCodes } = generateProcessedSourceCodes(); const templateUpdate: TemplateEditRequest = { id: template.id, @@ -105,7 +102,43 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { toggleEditButton(); } catch (error) { console.error('Failed to update template:', error); + } finally { + isSavingRef.current = false; + setIsSaving(false); + } + }; + + const validateTemplate = () => { + if (!title) { + return '제목을 입력해주세요'; + } + + if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { + return '소스코드 내용을 입력해주세요'; } + + return ''; + }; + + const isFilenameEmpty = (filename: string) => !filename.trim(); + + const generateUniqueFilename = () => `file_${Math.random().toString(36).substring(2, 10)}.txt`; + + const generateProcessedSourceCodes = () => { + const processSourceCodes = sourceCodes.map((sourceCode, index) => { + const { filename } = sourceCode; + + return { + ...sourceCode, + ordinal: index + 1, + filename: isFilenameEmpty(filename) ? generateUniqueFilename() : filename, + }; + }); + + const createSourceCodes = processSourceCodes.filter((sourceCode) => !sourceCode.id); + const updateSourceCodes = processSourceCodes.filter((sourceCode) => sourceCode.id); + + return { createSourceCodes, updateSourceCodes }; }; return ( @@ -167,14 +200,23 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { - - - 취소 - - - + {isSaving ? ( + + ) : ( + + + 취소 + + + + )} {error && Error: {error.message}} diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx index 2e8f33d79..b1503c04c 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx @@ -1,7 +1,17 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { PlusIcon, PrivateIcon, PublicIcon } from '@/assets/images'; -import { Button, CategoryDropdown, Input, SelectList, SourceCodeEditor, TagInput, Text, Toggle } from '@/components'; +import { + Button, + CategoryDropdown, + Input, + LoadingBall, + SelectList, + SourceCodeEditor, + TagInput, + Text, + Toggle, +} from '@/components'; import { useCustomNavigate, useInput, useSelectList, useToast } from '@/hooks'; import { useAuth } from '@/hooks/authentication'; import { useCategory } from '@/hooks/category'; @@ -50,36 +60,58 @@ const TemplateUploadPage = () => { const tagProps = useTag([]); const [visibility, setVisibility] = useState(DEFAULT_TEMPLATE_VISIBILITY); + const [isSaving, setIsSaving] = useState(false); + const isSavingRef = useRef(false); const { currentOption: currentFile, linkedElementRefs: sourceCodeRefs, handleSelectOption } = useSelectList(); const { mutateAsync: uploadTemplate, error } = useTemplateUploadMutation(); - const validateTemplate = () => { - if (!title) { - return '제목을 입력해주세요'; - } + const handleCancelButton = () => { + navigate(-1); + }; - if (sourceCodes.filter(({ filename }) => !filename || filename.trim() === '').length) { - return '파일명을 입력해주세요'; + const handleSaveButtonClick = async () => { + if (isSavingRef.current) { + return; } - if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { - return '소스코드 내용을 입력해주세요'; + if (!canSaveTemplate()) { + return; } - return ''; - }; + isSavingRef.current = true; + setIsSaving(true); - const handleCancelButton = () => { - navigate(-1); + try { + const processedSourceCodes = generateProcessedSourceCodes(); + + const newTemplate: TemplateUploadRequest = { + title, + description, + sourceCodes: processedSourceCodes, + thumbnailOrdinal: 1, + categoryId: categoryProps.currentValue.id, + tags: tagProps.tags, + visibility, + }; + + const response = await uploadTemplate(newTemplate); + + if (response.ok) { + trackTemplateSaveSuccess(); + } + } finally { + isSavingRef.current = false; + setIsSaving(false); + } }; - const handleSaveButtonClick = async () => { + const canSaveTemplate = (): boolean => { if (categoryProps.isCategoryQueryFetching) { failAlert('카테고리 목록을 불러오는 중입니다. 잠시 후 다시 시도해주세요.'); - return; + return false; } const errorMessage = validateTemplate(); @@ -87,35 +119,45 @@ const TemplateUploadPage = () => { if (errorMessage) { failAlert(errorMessage); - return; + return false; } - const orderedSourceCodes = sourceCodes.map( - (sourceCode, index): SourceCodes => ({ - ...sourceCode, - ordinal: index + 1, - }), - ); - - const newTemplate: TemplateUploadRequest = { - title, - description, - sourceCodes: orderedSourceCodes, - thumbnailOrdinal: 1, - categoryId: categoryProps.currentValue.id, - tags: tagProps.tags, - visibility, - }; + return true; + }; - const response = await uploadTemplate(newTemplate); + const validateTemplate = () => { + if (!title) { + return '제목을 입력해주세요'; + } - if (response.ok) { - trackClickTemplateSave({ - templateTitle: title, - sourceCodeCount: sourceCodes.length, - visibility, - }); + if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { + return '소스코드 내용을 입력해주세요'; } + + return ''; + }; + + const isFilenameEmpty = (filename: string) => !filename.trim(); + + const generateUniqueFilename = () => `file_${Math.random().toString(36).substring(2, 10)}.txt`; + + const generateProcessedSourceCodes = (): SourceCodes[] => + sourceCodes.map((sourceCode, index): SourceCodes => { + const { filename } = sourceCode; + + return { + ...sourceCode, + ordinal: index + 1, + filename: isFilenameEmpty(filename) ? generateUniqueFilename() : filename, + }; + }); + + const trackTemplateSaveSuccess = () => { + trackClickTemplateSave({ + templateTitle: title, + sourceCodeCount: sourceCodes.length, + visibility, + }); }; return ( @@ -177,14 +219,23 @@ const TemplateUploadPage = () => { - - - 취소 - - - + {isSaving ? ( + + ) : ( + + + 취소 + + + + )} {error && Error: {error.message}} From e8e77f0a859c79caaad20812915a6f296d824b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:11:50 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor(pages):=20=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=84=A4=EC=A0=95=20radio=20UI=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateEditPage.style.ts | 20 +++++++++-- .../TemplateEditPage/TemplateEditPage.tsx | 33 +++++++++---------- .../TemplateUploadPage.style.ts | 20 +++++++++-- .../TemplateUploadPage/TemplateUploadPage.tsx | 33 +++++++++---------- frontend/src/service/constants.ts | 7 +++- 5 files changed, 72 insertions(+), 41 deletions(-) diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts b/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts index e35b328e8..b0f2c57a4 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts @@ -23,10 +23,24 @@ export const MainContainer = styled.div` margin-top: 3rem; `; -export const CategoryAndVisibilityContainer = styled.div` +export const VisibilityContainer = styled.div` display: flex; - align-items: flex-end; - justify-content: space-between; + gap: 2rem; +`; + +export const VisibilityButton = styled.button` + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; +`; + +export const Radio = styled.div<{ isSelected: boolean }>` + width: 1rem; + height: 1rem; + background-color: ${({ theme, isSelected }) => + isSelected ? theme.color.light.primary_500 : theme.color.light.secondary_200}; + border-radius: 100%; `; export const CancelButton = styled(Button)` diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index 468b9a6d3..bf69256d3 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from 'react'; -import { PlusIcon, PrivateIcon, PublicIcon } from '@/assets/images'; +import { PlusIcon } from '@/assets/images'; import { Button, Input, @@ -10,7 +10,6 @@ import { CategoryDropdown, TagInput, LoadingBall, - Toggle, } from '@/components'; import { useInput, useSelectList, useToast } from '@/hooks'; import { useAuth } from '@/hooks/authentication'; @@ -18,7 +17,7 @@ import { useCategory } from '@/hooks/category'; import { useTag, useSourceCode } from '@/hooks/template'; import { useTemplateEditMutation } from '@/queries/templates'; import { useTrackPageViewed } from '@/service/amplitude'; -import { TEMPLATE_VISIBILITY } from '@/service/constants'; +import { TEMPLATE_VISIBILITY, convertToKorVisibility } from '@/service/constants'; import { ICON_SIZE } from '@/style/styleConstants'; import { theme } from '@/style/theme'; import type { Template, TemplateEditRequest } from '@/types'; @@ -65,6 +64,10 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { const { failAlert } = useToast(); + const handleVisibility = (visibility: TemplateVisibility) => () => { + setVisibility(visibility); + }; + const handleCancelButton = () => { toggleEditButton(); }; @@ -144,20 +147,7 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { return ( - - - , - , - ]} - optionSliderColor={[undefined, theme.color.light.triadic_primary_800]} - selectedOption={visibility} - switchOption={setVisibility} - /> - + @@ -200,6 +190,15 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { + + {TEMPLATE_VISIBILITY.map((el) => ( + + + {convertToKorVisibility[el]} + + ))} + + {isSaving ? ( ) : ( diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts index e35b328e8..b0f2c57a4 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts @@ -23,10 +23,24 @@ export const MainContainer = styled.div` margin-top: 3rem; `; -export const CategoryAndVisibilityContainer = styled.div` +export const VisibilityContainer = styled.div` display: flex; - align-items: flex-end; - justify-content: space-between; + gap: 2rem; +`; + +export const VisibilityButton = styled.button` + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; +`; + +export const Radio = styled.div<{ isSelected: boolean }>` + width: 1rem; + height: 1rem; + background-color: ${({ theme, isSelected }) => + isSelected ? theme.color.light.primary_500 : theme.color.light.secondary_200}; + border-radius: 100%; `; export const CancelButton = styled(Button)` diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx index b1503c04c..10832e1cd 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from 'react'; -import { PlusIcon, PrivateIcon, PublicIcon } from '@/assets/images'; +import { PlusIcon } from '@/assets/images'; import { Button, CategoryDropdown, @@ -10,7 +10,6 @@ import { SourceCodeEditor, TagInput, Text, - Toggle, } from '@/components'; import { useCustomNavigate, useInput, useSelectList, useToast } from '@/hooks'; import { useAuth } from '@/hooks/authentication'; @@ -18,7 +17,7 @@ import { useCategory } from '@/hooks/category'; import { useSourceCode, useTag } from '@/hooks/template'; import { useTemplateUploadMutation } from '@/queries/templates'; import { trackClickTemplateSave, useTrackPageViewed } from '@/service/amplitude'; -import { DEFAULT_TEMPLATE_VISIBILITY, TEMPLATE_VISIBILITY } from '@/service/constants'; +import { DEFAULT_TEMPLATE_VISIBILITY, TEMPLATE_VISIBILITY, convertToKorVisibility } from '@/service/constants'; import { ICON_SIZE } from '@/style/styleConstants'; import { theme } from '@/style/theme'; import { TemplateUploadRequest } from '@/types'; @@ -67,6 +66,10 @@ const TemplateUploadPage = () => { const { mutateAsync: uploadTemplate, error } = useTemplateUploadMutation(); + const handleVisibility = (visibility: TemplateVisibility) => () => { + setVisibility(visibility); + }; + const handleCancelButton = () => { navigate(-1); }; @@ -163,20 +166,7 @@ const TemplateUploadPage = () => { return ( - - - , - , - ]} - optionSliderColor={[undefined, theme.color.light.triadic_primary_800]} - selectedOption={visibility} - switchOption={setVisibility} - /> - + @@ -219,6 +209,15 @@ const TemplateUploadPage = () => { + + {TEMPLATE_VISIBILITY.map((el) => ( + + + {convertToKorVisibility[el]} + + ))} + + {isSaving ? ( ) : ( diff --git a/frontend/src/service/constants.ts b/frontend/src/service/constants.ts index faf8513a8..a020d8ec5 100644 --- a/frontend/src/service/constants.ts +++ b/frontend/src/service/constants.ts @@ -1,4 +1,9 @@ export const VISIBILITY_PUBLIC = 'PUBLIC'; export const VISIBILITY_PRIVATE = 'PRIVATE'; export const DEFAULT_TEMPLATE_VISIBILITY = VISIBILITY_PUBLIC; -export const TEMPLATE_VISIBILITY = [VISIBILITY_PRIVATE, VISIBILITY_PUBLIC] as const; +export const TEMPLATE_VISIBILITY = [VISIBILITY_PUBLIC, VISIBILITY_PRIVATE] as const; + +export const convertToKorVisibility = { + PUBLIC: '전체 공개', + PRIVATE: '비공개', +}; From 8571a69e32636b2c7bf7b0cd55a6f24bb8204a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:27:23 +0900 Subject: [PATCH 3/9] =?UTF-8?q?refactor(src):=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=ED=95=A8=EC=88=98(validateTemplate,=20gen?= =?UTF-8?q?erateUniqueFilename)=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateEditPage/TemplateEditPage.tsx | 24 ++++++++----------- .../TemplateUploadPage/TemplateUploadPage.tsx | 20 +++------------- .../src/service/generateUniqueFilename.ts | 3 +++ frontend/src/service/validates.ts | 13 ++++++++++ 4 files changed, 29 insertions(+), 31 deletions(-) create mode 100644 frontend/src/service/generateUniqueFilename.ts diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index bf69256d3..c99871ff3 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -18,6 +18,8 @@ import { useTag, useSourceCode } from '@/hooks/template'; import { useTemplateEditMutation } from '@/queries/templates'; import { useTrackPageViewed } from '@/service/amplitude'; import { TEMPLATE_VISIBILITY, convertToKorVisibility } from '@/service/constants'; +import { generateUniqueFilename, isFilenameEmpty } from '@/service/generateUniqueFilename'; +import { validateTemplate } from '@/service/validates'; import { ICON_SIZE } from '@/style/styleConstants'; import { theme } from '@/style/theme'; import type { Template, TemplateEditRequest } from '@/types'; @@ -77,9 +79,7 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { return; } - if (validateTemplate()) { - failAlert(validateTemplate()); - + if (!canSaveTemplate()) { return; } @@ -111,22 +111,18 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { } }; - const validateTemplate = () => { - if (!title) { - return '제목을 입력해주세요'; - } + const canSaveTemplate = () => { + const errorMessage = validateTemplate(title, sourceCodes); + + if (errorMessage) { + failAlert(errorMessage); - if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { - return '소스코드 내용을 입력해주세요'; + return false; } - return ''; + return true; }; - const isFilenameEmpty = (filename: string) => !filename.trim(); - - const generateUniqueFilename = () => `file_${Math.random().toString(36).substring(2, 10)}.txt`; - const generateProcessedSourceCodes = () => { const processSourceCodes = sourceCodes.map((sourceCode, index) => { const { filename } = sourceCode; diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx index 10832e1cd..5297b2002 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx @@ -18,6 +18,8 @@ import { useSourceCode, useTag } from '@/hooks/template'; import { useTemplateUploadMutation } from '@/queries/templates'; import { trackClickTemplateSave, useTrackPageViewed } from '@/service/amplitude'; import { DEFAULT_TEMPLATE_VISIBILITY, TEMPLATE_VISIBILITY, convertToKorVisibility } from '@/service/constants'; +import { generateUniqueFilename, isFilenameEmpty } from '@/service/generateUniqueFilename'; +import { validateTemplate } from '@/service/validates'; import { ICON_SIZE } from '@/style/styleConstants'; import { theme } from '@/style/theme'; import { TemplateUploadRequest } from '@/types'; @@ -117,7 +119,7 @@ const TemplateUploadPage = () => { return false; } - const errorMessage = validateTemplate(); + const errorMessage = validateTemplate(title, sourceCodes); if (errorMessage) { failAlert(errorMessage); @@ -128,22 +130,6 @@ const TemplateUploadPage = () => { return true; }; - const validateTemplate = () => { - if (!title) { - return '제목을 입력해주세요'; - } - - if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { - return '소스코드 내용을 입력해주세요'; - } - - return ''; - }; - - const isFilenameEmpty = (filename: string) => !filename.trim(); - - const generateUniqueFilename = () => `file_${Math.random().toString(36).substring(2, 10)}.txt`; - const generateProcessedSourceCodes = (): SourceCodes[] => sourceCodes.map((sourceCode, index): SourceCodes => { const { filename } = sourceCode; diff --git a/frontend/src/service/generateUniqueFilename.ts b/frontend/src/service/generateUniqueFilename.ts new file mode 100644 index 000000000..56aa1188f --- /dev/null +++ b/frontend/src/service/generateUniqueFilename.ts @@ -0,0 +1,3 @@ +export const isFilenameEmpty = (filename: string) => !filename.trim(); + +export const generateUniqueFilename = () => `file_${Math.random().toString(36).substring(2, 10)}.txt`; diff --git a/frontend/src/service/validates.ts b/frontend/src/service/validates.ts index 29377781f..a8150e7de 100644 --- a/frontend/src/service/validates.ts +++ b/frontend/src/service/validates.ts @@ -1,3 +1,4 @@ +import { SourceCodes } from '@/types'; import { getByteSize } from '@/utils'; export const validateName = (name: string) => { @@ -91,3 +92,15 @@ export const validateEmail = (email: string) => { return ''; }; + +export const validateTemplate = (title: string, sourceCodes: SourceCodes[]) => { + if (!title) { + return '제목을 입력해주세요'; + } + + if (sourceCodes.filter(({ content }) => !content || content.trim() === '').length) { + return '소스코드 내용을 입력해주세요'; + } + + return ''; +}; From fbf192bf957c5c2ddc93829135f4a94ed8c9ecad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Thu, 28 Nov 2024 22:02:29 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor(pages):=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=84=A4=EB=AA=85=20=EC=9E=85=EB=A0=A5=20UI=20Text?= =?UTF-8?q?area=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx | 9 ++++++--- .../src/pages/TemplateUploadPage/TemplateUploadPage.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index c99871ff3..3a4666c94 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -10,6 +10,7 @@ import { CategoryDropdown, TagInput, LoadingBall, + Textarea, } from '@/components'; import { useInput, useSelectList, useToast } from '@/hooks'; import { useAuth } from '@/hooks/authentication'; @@ -151,13 +152,15 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { - - + - + {sourceCodes.map((sourceCode, index) => ( { - - + - + {sourceCodes.map((sourceCode, index) => ( Date: Wed, 4 Dec 2024 17:27:08 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix(api):=20checkName=20=EC=9D=B4=20respons?= =?UTF-8?q?e=20return=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/authentication.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/authentication.ts b/frontend/src/api/authentication.ts index 89998ce5d..fccd25713 100644 --- a/frontend/src/api/authentication.ts +++ b/frontend/src/api/authentication.ts @@ -12,10 +12,10 @@ export const postLogin = async (loginInfo: LoginRequest) => { export const postLogout = async () => await apiClient.post(END_POINTS.LOGOUT, {}); -export const getLoginState = async () => apiClient.get(END_POINTS.LOGIN_CHECK); +export const getLoginState = async () => await apiClient.get(END_POINTS.LOGIN_CHECK); export const checkName = async (name: string) => { const params = new URLSearchParams({ name }); - await apiClient.get(`${END_POINTS.CHECK_NAME}?${params.toString()}`); + return await apiClient.get(`${END_POINTS.CHECK_NAME}?${params.toString()}`); }; From 3750cfc98ced4be7ad2b8c6d233267d15b55b2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:48:00 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat(src):=20Radio=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.storybook/preview.tsx | 20 ++++++++----- .../src/components/Radio/Radio.stories.tsx | 29 +++++++++++++++++++ frontend/src/components/Radio/Radio.style.ts | 23 +++++++++++++++ frontend/src/components/Radio/Radio.tsx | 24 +++++++++++++++ frontend/src/components/index.ts | 1 + .../TemplateEditPage.style.ts | 20 ------------- .../TemplateEditPage/TemplateEditPage.tsx | 15 +++++----- .../TemplateUploadPage.style.ts | 20 ------------- .../TemplateUploadPage/TemplateUploadPage.tsx | 17 +++++------ 9 files changed, 104 insertions(+), 65 deletions(-) create mode 100644 frontend/src/components/Radio/Radio.stories.tsx create mode 100644 frontend/src/components/Radio/Radio.style.ts create mode 100644 frontend/src/components/Radio/Radio.tsx diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index 0bf11a65e..f954f9d02 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -4,6 +4,8 @@ import { MemoryRouter } from 'react-router-dom'; import GlobalStyles from '../src/style/GlobalStyles'; import { AuthProvider, ToastProvider } from '../src/contexts'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider } from '@emotion/react'; +import { theme } from '../src/style/theme'; const queryClient = new QueryClient(); @@ -20,14 +22,16 @@ const preview: Preview = { decorators: [ (Story) => ( - - - - - - - - + + + + + + + + + + ), ], diff --git a/frontend/src/components/Radio/Radio.stories.tsx b/frontend/src/components/Radio/Radio.stories.tsx new file mode 100644 index 000000000..12d3c30b5 --- /dev/null +++ b/frontend/src/components/Radio/Radio.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; + +import Radio from './Radio'; + +const meta: Meta = { + title: 'Radio', + component: Radio, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const options = ['코', '드', '잽']; + const [currentValue, setCurrentValue] = useState(options[0]); + + return ( + option} + handleCurrentValue={setCurrentValue} + /> + ); + }, +}; diff --git a/frontend/src/components/Radio/Radio.style.ts b/frontend/src/components/Radio/Radio.style.ts new file mode 100644 index 000000000..f62bc5653 --- /dev/null +++ b/frontend/src/components/Radio/Radio.style.ts @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +export const RadioContainer = styled.div` + display: flex; + gap: 2rem; +`; + +export const RadioOption = styled.button` + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; +`; + +export const RadioCircle = styled.div<{ isSelected: boolean }>` + width: 1rem; + height: 1rem; + + background-color: ${({ theme, isSelected }) => + isSelected ? theme.color.light.primary_500 : theme.color.light.secondary_300}; + border: ${({ theme }) => `0.18rem solid ${theme.color.light.secondary_300}`}; + border-radius: 100%; +`; diff --git a/frontend/src/components/Radio/Radio.tsx b/frontend/src/components/Radio/Radio.tsx new file mode 100644 index 000000000..6e0d6edfa --- /dev/null +++ b/frontend/src/components/Radio/Radio.tsx @@ -0,0 +1,24 @@ +import { Text } from '@/components'; +import { theme } from '@/style/theme'; + +import * as S from './Radio.style'; + +interface Props { + options: T[]; + currentValue: T; + getOptionLabel: (option: T) => string; + handleCurrentValue: (value: T) => void; +} + +const Radio = ({ options, currentValue, getOptionLabel, handleCurrentValue }: Props) => ( + + {options.map((option) => ( + handleCurrentValue(option)}> + + {getOptionLabel(option)} + + ))} + +); + +export default Radio; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 2b2b859f3..ac3fc2af4 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -30,6 +30,7 @@ export { default as ContactUs } from './ContactUs/ContactUs'; export { default as Toggle } from './Toggle/Toggle'; export { default as ScrollTopButton } from './ScrollTopButton/ScrollTopButton'; export { default as AuthorInfo } from './AuthorInfo/AuthorInfo'; +export { default as Radio } from './Radio/Radio'; // Skeleton UI export { default as LoadingBall } from './LoadingBall/LoadingBall'; diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts b/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts index b0f2c57a4..ae844b117 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts @@ -23,26 +23,6 @@ export const MainContainer = styled.div` margin-top: 3rem; `; -export const VisibilityContainer = styled.div` - display: flex; - gap: 2rem; -`; - -export const VisibilityButton = styled.button` - display: flex; - gap: 1rem; - align-items: center; - justify-content: center; -`; - -export const Radio = styled.div<{ isSelected: boolean }>` - width: 1rem; - height: 1rem; - background-color: ${({ theme, isSelected }) => - isSelected ? theme.color.light.primary_500 : theme.color.light.secondary_200}; - border-radius: 100%; -`; - export const CancelButton = styled(Button)` background-color: white; `; diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index 3a4666c94..79858e256 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -11,6 +11,7 @@ import { TagInput, LoadingBall, Textarea, + Radio, } from '@/components'; import { useInput, useSelectList, useToast } from '@/hooks'; import { useAuth } from '@/hooks/authentication'; @@ -189,14 +190,12 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { - - {TEMPLATE_VISIBILITY.map((el) => ( - - - {convertToKorVisibility[el]} - - ))} - + convertToKorVisibility[option]} + /> {isSaving ? ( diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts index b0f2c57a4..ae844b117 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts @@ -23,26 +23,6 @@ export const MainContainer = styled.div` margin-top: 3rem; `; -export const VisibilityContainer = styled.div` - display: flex; - gap: 2rem; -`; - -export const VisibilityButton = styled.button` - display: flex; - gap: 1rem; - align-items: center; - justify-content: center; -`; - -export const Radio = styled.div<{ isSelected: boolean }>` - width: 1rem; - height: 1rem; - background-color: ${({ theme, isSelected }) => - isSelected ? theme.color.light.primary_500 : theme.color.light.secondary_200}; - border-radius: 100%; -`; - export const CancelButton = styled(Button)` background-color: white; `; diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx index 10fca9305..fc78a83ad 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx @@ -6,6 +6,7 @@ import { CategoryDropdown, Input, LoadingBall, + Radio, SelectList, SourceCodeEditor, TagInput, @@ -69,7 +70,7 @@ const TemplateUploadPage = () => { const { mutateAsync: uploadTemplate, error } = useTemplateUploadMutation(); - const handleVisibility = (visibility: TemplateVisibility) => () => { + const handleVisibility = (visibility: TemplateVisibility) => { setVisibility(visibility); }; @@ -198,14 +199,12 @@ const TemplateUploadPage = () => { - - {TEMPLATE_VISIBILITY.map((el) => ( - - - {convertToKorVisibility[el]} - - ))} - + convertToKorVisibility[option]} + /> {isSaving ? ( From 16632f8d5cb72dd463089e2dde6fffb455ffad61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:56:34 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor(src):=20usePreventDuplicateMutatio?= =?UTF-8?q?n=20hook=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/index.ts | 1 + .../src/hooks/usePreventDuplicateMutation.ts | 38 +++++++++++++++ .../TemplateEditPage/TemplateEditPage.tsx | 18 ++------ .../TemplateUploadPage/TemplateUploadPage.tsx | 46 +++++++------------ .../templates/useTemplateEditMutation.ts | 5 +- .../templates/useTemplateUploadMutation.ts | 6 +-- 6 files changed, 64 insertions(+), 50 deletions(-) create mode 100644 frontend/src/hooks/usePreventDuplicateMutation.ts diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index ee9d3cbe0..3740125b3 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -14,3 +14,4 @@ export { useSelectList } from './useSelectList'; export { useCustomNavigate } from './useCustomNavigate'; export { useQueryParams } from './useQueryParams'; export { useToast } from './useToast'; +export { usePreventDuplicateMutation } from './usePreventDuplicateMutation'; diff --git a/frontend/src/hooks/usePreventDuplicateMutation.ts b/frontend/src/hooks/usePreventDuplicateMutation.ts new file mode 100644 index 000000000..f5b5fa8d5 --- /dev/null +++ b/frontend/src/hooks/usePreventDuplicateMutation.ts @@ -0,0 +1,38 @@ +import { useMutation, UseMutationOptions, UseMutationResult, MutationFunction } from '@tanstack/react-query'; +import { useRef } from 'react'; + +type RequiredMutationFn = UseMutationOptions< + TData, + TError, + TVariables, + TContext +> & { + mutationFn: MutationFunction; +}; + +export const usePreventDuplicateMutation = ( + options: RequiredMutationFn, +): UseMutationResult => { + const isMutatingRef = useRef(false); + + const originalMutationFn = options.mutationFn; + + const preventDuplicateMutationFn: MutationFunction = async (variables: TVariables) => { + if (isMutatingRef.current) { + return undefined as TData; + } + + isMutatingRef.current = true; + + try { + return await originalMutationFn(variables); + } finally { + isMutatingRef.current = false; + } + }; + + return useMutation({ + ...options, + mutationFn: preventDuplicateMutationFn, + }); +}; diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index 79858e256..09f2d973b 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { PlusIcon } from '@/assets/images'; import { @@ -59,12 +59,10 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { const tagProps = useTag(initTags); const [visibility, setVisibility] = useState(template.visibility); - const [isSaving, setIsSaving] = useState(false); - const isSavingRef = useRef(false); const { currentOption: currentFile, linkedElementRefs: sourceCodeRefs, handleSelectOption } = useSelectList(); - const { mutateAsync: updateTemplate, error } = useTemplateEditMutation(template.id); + const { mutateAsync: updateTemplate, isPending, error } = useTemplateEditMutation(template.id); const { failAlert } = useToast(); @@ -77,17 +75,10 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { }; const handleSaveButtonClick = async () => { - if (isSavingRef.current) { - return; - } - if (!canSaveTemplate()) { return; } - isSavingRef.current = true; - setIsSaving(true); - const { createSourceCodes, updateSourceCodes } = generateProcessedSourceCodes(); const templateUpdate: TemplateEditRequest = { @@ -107,9 +98,6 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { toggleEditButton(); } catch (error) { console.error('Failed to update template:', error); - } finally { - isSavingRef.current = false; - setIsSaving(false); } }; @@ -197,7 +185,7 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { getOptionLabel={(option: TemplateVisibility) => convertToKorVisibility[option]} /> - {isSaving ? ( + {isPending ? ( ) : ( diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx index fc78a83ad..f90ecd94f 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { PlusIcon } from '@/assets/images'; import { @@ -63,12 +63,10 @@ const TemplateUploadPage = () => { const tagProps = useTag([]); const [visibility, setVisibility] = useState(DEFAULT_TEMPLATE_VISIBILITY); - const [isSaving, setIsSaving] = useState(false); - const isSavingRef = useRef(false); const { currentOption: currentFile, linkedElementRefs: sourceCodeRefs, handleSelectOption } = useSelectList(); - const { mutateAsync: uploadTemplate, error } = useTemplateUploadMutation(); + const { mutateAsync: uploadTemplate, isPending, error } = useTemplateUploadMutation(); const handleVisibility = (visibility: TemplateVisibility) => { setVisibility(visibility); @@ -79,38 +77,26 @@ const TemplateUploadPage = () => { }; const handleSaveButtonClick = async () => { - if (isSavingRef.current) { - return; - } - if (!canSaveTemplate()) { return; } - isSavingRef.current = true; - setIsSaving(true); + const processedSourceCodes = generateProcessedSourceCodes(); - try { - const processedSourceCodes = generateProcessedSourceCodes(); - - const newTemplate: TemplateUploadRequest = { - title, - description, - sourceCodes: processedSourceCodes, - thumbnailOrdinal: 1, - categoryId: categoryProps.currentValue.id, - tags: tagProps.tags, - visibility, - }; + const newTemplate: TemplateUploadRequest = { + title, + description, + sourceCodes: processedSourceCodes, + thumbnailOrdinal: 1, + categoryId: categoryProps.currentValue.id, + tags: tagProps.tags, + visibility, + }; - const response = await uploadTemplate(newTemplate); + const response = await uploadTemplate(newTemplate); - if (response.ok) { - trackTemplateSaveSuccess(); - } - } finally { - isSavingRef.current = false; - setIsSaving(false); + if (response.ok) { + trackTemplateSaveSuccess(); } }; @@ -206,7 +192,7 @@ const TemplateUploadPage = () => { getOptionLabel={(option: TemplateVisibility) => convertToKorVisibility[option]} /> - {isSaving ? ( + {isPending ? ( ) : ( diff --git a/frontend/src/queries/templates/useTemplateEditMutation.ts b/frontend/src/queries/templates/useTemplateEditMutation.ts index bee247573..bdd154013 100644 --- a/frontend/src/queries/templates/useTemplateEditMutation.ts +++ b/frontend/src/queries/templates/useTemplateEditMutation.ts @@ -1,11 +1,12 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { QUERY_KEY, editTemplate } from '@/api'; +import { usePreventDuplicateMutation } from '@/hooks'; export const useTemplateEditMutation = (id: number) => { const queryClient = useQueryClient(); - return useMutation({ + return usePreventDuplicateMutation({ mutationFn: editTemplate, onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY.TEMPLATE, id] }); diff --git a/frontend/src/queries/templates/useTemplateUploadMutation.ts b/frontend/src/queries/templates/useTemplateUploadMutation.ts index 07f883342..fc9b4ba23 100644 --- a/frontend/src/queries/templates/useTemplateUploadMutation.ts +++ b/frontend/src/queries/templates/useTemplateUploadMutation.ts @@ -1,16 +1,16 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { QUERY_KEY, postTemplate } from '@/api'; import { ApiError, HTTP_STATUS } from '@/api/Error'; -import { useToast } from '@/hooks'; +import { usePreventDuplicateMutation, useToast } from '@/hooks'; export const useTemplateUploadMutation = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); const { failAlert } = useToast(); - return useMutation({ + return usePreventDuplicateMutation({ mutationFn: postTemplate, onSuccess: (res) => { const location = res.headers.get('location'); From 8616347a2911fd42d43b424d3326d3a932b3089b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:39:24 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor(TemplateEditPage):=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20try-catch=20=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index 09f2d973b..32cc561d1 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -93,12 +93,8 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { visibility, }; - try { - await updateTemplate(templateUpdate); - toggleEditButton(); - } catch (error) { - console.error('Failed to update template:', error); - } + await updateTemplate(templateUpdate); + toggleEditButton(); }; const canSaveTemplate = () => { From f020b7670dcf017cacb740e2398fc66f37ad0313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:13:13 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor(src):=20Radio=EC=97=90=EC=84=9C=20?= =?UTF-8?q?options=20=EC=9D=84=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EB=B0=9B?= =?UTF-8?q?=EC=95=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Radio/Radio.stories.tsx | 13 +++---------- frontend/src/components/Radio/Radio.style.ts | 2 ++ frontend/src/components/Radio/Radio.tsx | 15 +++++++-------- .../pages/TemplateEditPage/TemplateEditPage.tsx | 13 ++----------- .../TemplateUploadPage/TemplateUploadPage.tsx | 13 ++----------- frontend/src/service/constants.ts | 5 ++--- frontend/src/types/template.ts | 4 ++-- 7 files changed, 20 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/Radio/Radio.stories.tsx b/frontend/src/components/Radio/Radio.stories.tsx index 12d3c30b5..e9f7e0cae 100644 --- a/frontend/src/components/Radio/Radio.stories.tsx +++ b/frontend/src/components/Radio/Radio.stories.tsx @@ -14,16 +14,9 @@ type Story = StoryObj; export const Default: Story = { render: () => { - const options = ['코', '드', '잽']; - const [currentValue, setCurrentValue] = useState(options[0]); + const options = { 코: '코', 드: '드', 잽: '잽' }; + const [currentValue, setCurrentValue] = useState('코'); - return ( - option} - handleCurrentValue={setCurrentValue} - /> - ); + return ; }, }; diff --git a/frontend/src/components/Radio/Radio.style.ts b/frontend/src/components/Radio/Radio.style.ts index f62bc5653..b38fb3a7d 100644 --- a/frontend/src/components/Radio/Radio.style.ts +++ b/frontend/src/components/Radio/Radio.style.ts @@ -6,6 +6,8 @@ export const RadioContainer = styled.div` `; export const RadioOption = styled.button` + cursor: pointer; + display: flex; gap: 1rem; align-items: center; diff --git a/frontend/src/components/Radio/Radio.tsx b/frontend/src/components/Radio/Radio.tsx index 6e0d6edfa..8b6bb8c77 100644 --- a/frontend/src/components/Radio/Radio.tsx +++ b/frontend/src/components/Radio/Radio.tsx @@ -3,19 +3,18 @@ import { theme } from '@/style/theme'; import * as S from './Radio.style'; -interface Props { - options: T[]; +interface Props { + options: Record; currentValue: T; - getOptionLabel: (option: T) => string; handleCurrentValue: (value: T) => void; } -const Radio = ({ options, currentValue, getOptionLabel, handleCurrentValue }: Props) => ( +const Radio = ({ options, currentValue, handleCurrentValue }: Props) => ( - {options.map((option) => ( - handleCurrentValue(option)}> - - {getOptionLabel(option)} + {Object.keys(options).map((optionKey) => ( + handleCurrentValue(optionKey as T)}> + + {options[optionKey as T]} ))} diff --git a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx index 32cc561d1..a927155dd 100644 --- a/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx +++ b/frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx @@ -19,7 +19,7 @@ import { useCategory } from '@/hooks/category'; import { useTag, useSourceCode } from '@/hooks/template'; import { useTemplateEditMutation } from '@/queries/templates'; import { useTrackPageViewed } from '@/service/amplitude'; -import { TEMPLATE_VISIBILITY, convertToKorVisibility } from '@/service/constants'; +import { VISIBILITY_OPTIONS } from '@/service/constants'; import { generateUniqueFilename, isFilenameEmpty } from '@/service/generateUniqueFilename'; import { validateTemplate } from '@/service/validates'; import { ICON_SIZE } from '@/style/styleConstants'; @@ -66,10 +66,6 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { const { failAlert } = useToast(); - const handleVisibility = (visibility: TemplateVisibility) => () => { - setVisibility(visibility); - }; - const handleCancelButton = () => { toggleEditButton(); }; @@ -174,12 +170,7 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { - convertToKorVisibility[option]} - /> + {isPending ? ( diff --git a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx index f90ecd94f..2923ff8b9 100644 --- a/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx +++ b/frontend/src/pages/TemplateUploadPage/TemplateUploadPage.tsx @@ -19,7 +19,7 @@ import { useCategory } from '@/hooks/category'; import { useSourceCode, useTag } from '@/hooks/template'; import { useTemplateUploadMutation } from '@/queries/templates'; import { trackClickTemplateSave, useTrackPageViewed } from '@/service/amplitude'; -import { DEFAULT_TEMPLATE_VISIBILITY, TEMPLATE_VISIBILITY, convertToKorVisibility } from '@/service/constants'; +import { DEFAULT_TEMPLATE_VISIBILITY, VISIBILITY_OPTIONS } from '@/service/constants'; import { generateUniqueFilename, isFilenameEmpty } from '@/service/generateUniqueFilename'; import { validateTemplate } from '@/service/validates'; import { ICON_SIZE } from '@/style/styleConstants'; @@ -68,10 +68,6 @@ const TemplateUploadPage = () => { const { mutateAsync: uploadTemplate, isPending, error } = useTemplateUploadMutation(); - const handleVisibility = (visibility: TemplateVisibility) => { - setVisibility(visibility); - }; - const handleCancelButton = () => { navigate(-1); }; @@ -185,12 +181,7 @@ const TemplateUploadPage = () => { - convertToKorVisibility[option]} - /> + {isPending ? ( diff --git a/frontend/src/service/constants.ts b/frontend/src/service/constants.ts index a020d8ec5..4b8e83671 100644 --- a/frontend/src/service/constants.ts +++ b/frontend/src/service/constants.ts @@ -1,9 +1,8 @@ export const VISIBILITY_PUBLIC = 'PUBLIC'; export const VISIBILITY_PRIVATE = 'PRIVATE'; export const DEFAULT_TEMPLATE_VISIBILITY = VISIBILITY_PUBLIC; -export const TEMPLATE_VISIBILITY = [VISIBILITY_PUBLIC, VISIBILITY_PRIVATE] as const; -export const convertToKorVisibility = { +export const VISIBILITY_OPTIONS = { PUBLIC: '전체 공개', PRIVATE: '비공개', -}; +} as const; diff --git a/frontend/src/types/template.ts b/frontend/src/types/template.ts index 7e82cc3fe..8fa14bdb6 100644 --- a/frontend/src/types/template.ts +++ b/frontend/src/types/template.ts @@ -1,4 +1,4 @@ -import { TEMPLATE_VISIBILITY } from '@/service/constants'; +import { VISIBILITY_OPTIONS } from '@/service/constants'; export interface SourceCodes { id?: number; @@ -57,4 +57,4 @@ export interface TemplateListItem { visibility: TemplateVisibility; } -export type TemplateVisibility = (typeof TEMPLATE_VISIBILITY)[number]; +export type TemplateVisibility = keyof typeof VISIBILITY_OPTIONS;