Skip to content
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

Merged
merged 10 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions frontend/src/pages/TemplateEditPage/TemplateEditPage.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
144 changes: 92 additions & 52 deletions frontend/src/pages/TemplateEditPage/TemplateEditPage.tsx
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';
Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSaving은 따로 state을 만들지 않고 아래 useTemplateEditMutationisPending 상태를 사용해도 괜찮지 않을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Expand All @@ -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
Expand Down Expand Up @@ -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>
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/pages/TemplateUploadPage/TemplateUploadPage.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가 동일한 컴포넌트인데 중복선언 되는 것들이 조금 있는거 같아요! 아무래도 style 를 외부에서 선언하니 더 잘 보이는거 같은데
Radio 는 공용 컴포넌트로 분리해볼 수 있지 않을까요? 헤인은 어떻게 생각하세요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 생각인 것 같아요! Radio 공용 컴포넌트로 분리했습니다👍


export const CancelButton = styled(Button)`
Expand Down
Loading