-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
템플릿 생성 및 수정시 파일명 랜덤 생성 및 중복호출 막기, 공개 범위 설정 radio UI로 변경 #939
Changes from 4 commits
511c8b6
e8e77f0
8571a69
fbf192b
30abb18
24317df
3750cfc
16632f8
8616347
f020b76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,26 @@ | ||
import { useState } from 'react'; | ||
|
||
import { PlusIcon, PrivateIcon, PublicIcon } from '@/assets/images'; | ||
import { Button, Input, SelectList, SourceCodeEditor, Text, CategoryDropdown, TagInput, Toggle } from '@/components'; | ||
import { useRef, useState } from 'react'; | ||
|
||
import { PlusIcon } from '@/assets/images'; | ||
import { | ||
Button, | ||
Input, | ||
SelectList, | ||
SourceCodeEditor, | ||
Text, | ||
CategoryDropdown, | ||
TagInput, | ||
LoadingBall, | ||
Textarea, | ||
} from '@/components'; | ||
import { useInput, useSelectList, useToast } from '@/hooks'; | ||
import { useAuth } from '@/hooks/authentication'; | ||
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 { 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'; | ||
|
@@ -46,47 +58,36 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { | |
const tagProps = useTag(initTags); | ||
|
||
const [visibility, setVisibility] = useState<TemplateVisibility>(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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추가로 이렇게 중복된 Mutation 요청을 막으면 좋은 부분이 회원이나 카테고리 관련 부분에서도 필요한 기능 같은데, 제가 공유드린 내용처럼 커스텀 훅으로 재사용 가능하게 바꿔보면 어떨까요? |
||
|
||
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 handleVisibility = (visibility: TemplateVisibility) => () => { | ||
setVisibility(visibility); | ||
}; | ||
|
||
const handleCancelButton = () => { | ||
toggleEditButton(); | ||
}; | ||
|
||
const handleSaveButtonClick = async () => { | ||
if (validateTemplate()) { | ||
failAlert(validateTemplate()); | ||
if (isSavingRef.current) { | ||
return; | ||
} | ||
|
||
if (!canSaveTemplate()) { | ||
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,40 +106,61 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { | |
toggleEditButton(); | ||
} catch (error) { | ||
console.error('Failed to update template:', error); | ||
} finally { | ||
isSavingRef.current = false; | ||
setIsSaving(false); | ||
} | ||
}; | ||
|
||
const canSaveTemplate = () => { | ||
const errorMessage = validateTemplate(title, sourceCodes); | ||
|
||
if (errorMessage) { | ||
failAlert(errorMessage); | ||
|
||
return false; | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
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 ( | ||
<S.TemplateEditContainer> | ||
<S.MainContainer> | ||
<S.CategoryAndVisibilityContainer> | ||
<CategoryDropdown {...categoryProps} /> | ||
<Toggle | ||
showOptions={false} | ||
options={[...TEMPLATE_VISIBILITY]} | ||
optionAdornments={[ | ||
<PrivateIcon key={TEMPLATE_VISIBILITY[1]} width={ICON_SIZE.MEDIUM_SMALL} />, | ||
<PublicIcon key={TEMPLATE_VISIBILITY[0]} width={ICON_SIZE.MEDIUM_SMALL} />, | ||
]} | ||
optionSliderColor={[undefined, theme.color.light.triadic_primary_800]} | ||
selectedOption={visibility} | ||
switchOption={setVisibility} | ||
/> | ||
</S.CategoryAndVisibilityContainer> | ||
<CategoryDropdown {...categoryProps} /> | ||
|
||
<S.UnderlineInputWrapper> | ||
<Input size='xlarge' variant='text'> | ||
<Input.TextField placeholder='제목을 입력해주세요' value={title} onChange={handleTitleChange} /> | ||
</Input> | ||
</S.UnderlineInputWrapper> | ||
|
||
<Input size='large' variant='text'> | ||
<Input.TextField | ||
<Textarea size='medium' variant='text'> | ||
<Textarea.TextField | ||
placeholder='이 템플릿을 언제 다시 쓸 것 같나요?' | ||
minRows={1} | ||
maxRows={5} | ||
value={description} | ||
onChange={handleDescriptionChange} | ||
/> | ||
</Input> | ||
</Textarea> | ||
|
||
{sourceCodes.map((sourceCode, index) => ( | ||
<SourceCodeEditor | ||
|
@@ -167,14 +189,32 @@ const TemplateEditPage = ({ template, toggleEditButton }: Props) => { | |
|
||
<TagInput {...tagProps} /> | ||
|
||
<S.ButtonGroup> | ||
<S.CancelButton size='medium' variant='outlined' onClick={handleCancelButton}> | ||
취소 | ||
</S.CancelButton> | ||
<Button size='medium' variant='contained' onClick={handleSaveButtonClick} disabled={sourceCodes.length === 0}> | ||
저장 | ||
</Button> | ||
</S.ButtonGroup> | ||
<S.VisibilityContainer> | ||
{TEMPLATE_VISIBILITY.map((el) => ( | ||
<S.VisibilityButton key={el} onClick={handleVisibility(el)}> | ||
<S.Radio isSelected={visibility === el} /> | ||
<Text.Medium color={theme.color.light.secondary_800}>{convertToKorVisibility[el]}</Text.Medium> | ||
</S.VisibilityButton> | ||
))} | ||
</S.VisibilityContainer> | ||
|
||
{isSaving ? ( | ||
<LoadingBall /> | ||
) : ( | ||
<S.ButtonGroup> | ||
<S.CancelButton size='medium' variant='outlined' onClick={handleCancelButton}> | ||
취소 | ||
</S.CancelButton> | ||
<Button | ||
size='medium' | ||
variant='contained' | ||
onClick={handleSaveButtonClick} | ||
disabled={sourceCodes.length === 0} | ||
> | ||
저장 | ||
</Button> | ||
</S.ButtonGroup> | ||
)} | ||
|
||
{error && <Text.Medium color={theme.color.light.analogous_primary_400}>Error: {error.message}</Text.Medium>} | ||
</S.MainContainer> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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%; | ||
`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뭔가 동일한 컴포넌트인데 중복선언 되는 것들이 조금 있는거 같아요! 아무래도 style 를 외부에서 선언하니 더 잘 보이는거 같은데 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋은 생각인 것 같아요! |
||
|
||
export const CancelButton = styled(Button)` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isSaving
은 따로 state을 만들지 않고 아래useTemplateEditMutation
의isPending
상태를 사용해도 괜찮지 않을까요?