Skip to content

Commit

Permalink
[#37] CSS 속성 편집 기능 및 CSS 코드 생성 기능 추가 (#113)
Browse files Browse the repository at this point in the history
* ✨ feat: css 카테고리 타입 정의

* ✨ feat: css 카테고리를 정의하는 객체 추가

* ✨ feat: css 카테고리 속성 편집창 정적 ui 구현

* 🙀 chore: 파일 구조 변경에 따른 import문 변경

* 🔥 remove: 파일 이동에 따른 파일 삭제

* ✨ feat: dev 브랜치에서 pull 후 변경사항 적용 및 툴팁 구현 (미완성)

* ✨ feat: css 툴팁 컴포넌트 추가 (미완성)

* ✨ feat: 마우스 좌표에 따른 css 툴팁 위치 계산 로직 추가 (미완성)

* ✨ feat: css 툴팁 컴포넌트 구현

* ✨ feat: debounce 메소드 구현

* ✨ feat: 브라우저 resize 이벤트 핸들링 후 innerWidth, innerHeight를 리턴하는 커스텀훅 추가

* ✨ feat: css 속성 편집 관련 커스텀 훅 제작

* ✨ feat: css 속성 툴팁 렌더링 위치 관련 로직을 제공하는 커스텀 훅 제공

* 🙀 chore: index.ts re-export 추가

* 🎨 style: input type=color 스타일 조정

* 🚚 rename: cssCategoryList 소스파일 shared/util 폴더로 이동

* 🚚 rename: tooltip 관련 커스텀 훅 소스파일 이름 변경

* 🙀 chore: useRef를 사용하는 부분 삭제

* 🔨 refactor: CssPropsSelectBox에서 header 컴포넌트 분리

* 🔨 refactor: CssPropsSelectBox에서 카테고리를 선택하는 부분 CategoryBar 컴포넌트로 분리

* 🔨 refactor: 카테고리 선택 버튼 컴포넌트로 분리

* 🔨 refactor: CssPropsSelectBox에서 CSS 옵션을 부여하는 부분 분리

* 🔨 refactor: CSS 옵션을 부여하는 컴포넌트 리스트에서 재사용 컴포넌트  분리

* ✨ feat: css 옵션 부여 관련 기능 커스텀훅 제작

* ✨ feat: Css 속성 설정 관련 상태 변수들을 전역변수화 시킴

* ✨ feat: tootlip 좌표 관련 상태 변수들을 전역변수화 시킴

* 🔥 remove: 불필요한 파일 삭제

* 🔨 refactor: CssPropsSelectBox 컴포넌트 분리 완료

* 🙀 chore: import문 변경

* 🙀 chore: 새롭게 추가된 컴포넌트 및 커스텀 훅 index.ts를 통해 re-export

* ✨ feat: 여러 개의 클래스에 css 옵션을 적용하도록 상태 변수 자료 구조 변경

* 🙀 chore: key 속성 부여

* ✨ feat: class명이 빈 문자열이거나 클래스를 선택해주세요 일 시 속성 편집이 불가능하게 변경

* 🙀 chore: isChecked, cssOption 상태변수로 전역 상태 변수값을 조정하도록 변경

* ✨ feat: css 속성의 체크 여부 isChecked, css 속성에 부여된 값 cssOption 상태변수 추가 및 초기화 로직 추가

* ✨ feat: 워크스페이스 페이지 렌더링 시 전역 상태 초기화 진행

* ✨ feat: css 코드 생성 메소드 작성

* 🙀 chore: 변환하기 버튼에 css 코드 생성 메소드 추가

* 🐛 fix: selected로 값 조작 시 react로 DOM 조작 시 문제가 발생할 수 있는 경고 해결

* 🐛 fix: class이름이 빈 문자열일 때도 css코드가 생성되던 문제 해결

* 🙀 chore: 불필요한 코드 삭제

* 🐛 fix: 빌드 오류 수종
  • Loading branch information
lee0jae330 authored Nov 21, 2024
1 parent 561cc66 commit 3d5028e
Show file tree
Hide file tree
Showing 27 changed files with 781 additions and 4 deletions.
8 changes: 8 additions & 0 deletions apps/client/src/app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,11 @@
.tabSelected {
@apply text-bold-md flex-1 rounded-t-lg bg-blue-500 py-3 text-white;
}

input[type='color']::-webkit-color-swatch {
border-radius: 100%;
border: none;
}
input[type='color']::-webkit-color-swatch-wrapper {
padding: 0;
}
3 changes: 3 additions & 0 deletions apps/client/src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export { RedoButton } from './workspace/RedoButton';
export { UndoButton } from './workspace/UndoButton';
export { SaveButton } from './workspace/SaveButton';
export { WorkspaceNameInput } from './workspace/WorkspaceNameInput';
export { CssTooltip } from './workspace/CssTooltip';
export { CssOptionItem } from './workspace/CssOptionItem';
export { CssCategoryButton } from './workspace/CssCategoryButton';
23 changes: 23 additions & 0 deletions apps/client/src/entities/workspace/CssCategoryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TcssCategory, TcssCategoryItem } from '@/shared/types';

import { useCssPropsStore } from '@/shared/store';

type CssCategoryButtonProps = {
cssCategory: {
category: TcssCategory;
items: TcssCategoryItem[];
};
};

export const CssCategoryButton = ({ cssCategory }: CssCategoryButtonProps) => {
const { selectedCssCategory, setSelectedCssCategory } = useCssPropsStore();
return (
<button
key={cssCategory.category}
onClick={() => setSelectedCssCategory(cssCategory.category)}
className={`text-bold-sm flex cursor-pointer rounded px-3 py-2.5 text-gray-200 ${selectedCssCategory === cssCategory.category && 'text-gray-black bg-yellow-500'}`}
>
{cssCategory.category}
</button>
);
};
115 changes: 115 additions & 0 deletions apps/client/src/entities/workspace/CssOptionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useCssOptionItem, useCssOptions, useCssTooltip } from '@/shared/hooks';

import { CssTooltip } from '@/entities';
import Question from '@/shared/assets/question.svg?react';
import { TcssCategoryItem } from '@/shared/types';
import { useCssPropsStore } from '@/shared/store';

type CssOptionItemProps = {
cssItem: TcssCategoryItem;
index: number;
};

export const CssOptionItem = ({ cssItem, index }: CssOptionItemProps) => {
const { totalCssPropertyObj, currentCssClassName } = useCssPropsStore();
const { handleCssPropertyCheckboxChange, handleCssOptionChange, handleColorChange } =
useCssOptions();

const {
cssOptionValue,
isHover,
indexOfHover,
isChecked,
cssOption,
handleMouseEnter,
handleEnterKey,
handleMouseLeave,
handleChangeInputValue,
} = useCssOptionItem(cssItem);

const { leftX, topY } = useCssTooltip();

return (
<div
className={`flex h-[66px] w-full flex-shrink-0 items-center justify-between rounded-lg px-4 ${
totalCssPropertyObj[currentCssClassName] &&
totalCssPropertyObj[currentCssClassName].checkedCssPropertyObj[cssItem.label]
? 'bg-yellow-500'
: 'bg-gray-50'
} `}
>
<div className="flex items-center gap-5">
<input
type="checkbox"
checked={isChecked}
onChange={() => handleCssPropertyCheckboxChange(cssItem.label, isChecked, cssOption)}
title={cssItem.label}
className="h-5 w-5 appearance-none rounded border border-gray-100 bg-center bg-no-repeat checked:bg-white checked:bg-[url('@/shared/assets/check.svg')]"
disabled={
currentCssClassName.length === 0 || currentCssClassName === '클래스를 선택해주세요'
}
/>
<div className="flex items-center gap-2">
<p className="text-semibold-md text-gray-black max-w-36 border-gray-100">
{cssItem.label}
</p>
<Question
onMouseEnter={(e) => handleMouseEnter(e, index)}
onMouseLeave={handleMouseLeave}
/>
<CssTooltip
description={cssItem.description}
isOpen={isHover && indexOfHover === index}
leftX={leftX}
topY={topY}
/>
</div>
</div>
{cssItem.type === 'select' && (
<select
id={cssItem.label}
className="bg-gray-white focus:ring-gray-black text-semibold-md focus:border-gray-black w-[120px] truncate rounded-lg border border-gray-100 px-2 py-1 outline-none"
onChange={(e) => handleCssOptionChange(cssItem.label, e.target.value)}
value={cssOption}
disabled={
currentCssClassName.length === 0 || currentCssClassName === '클래스를 선택해주세요'
}
>
{cssItem.option?.map((option) => (
<option id={option} value={option} key={option}>
{option}
</option>
))}
</select>
)}
{cssItem.type === 'input' && (
<input
type="text"
className="text-semibold-md focus:border-gray-black placeholder:text-semibold-sm w-28 rounded-lg border border-gray-100 px-2 py-1 placeholder-gray-100 focus:border focus:outline-none"
placeholder="값을 입력하세요"
onBlur={(e) => handleCssOptionChange(cssItem.label, e.target.value)}
onKeyDown={(e) => handleEnterKey(cssItem.label, e)}
value={cssOptionValue}
onChange={handleChangeInputValue}
disabled={
currentCssClassName.length === 0 || currentCssClassName === '클래스를 선택해주세요'
}
/>
)}
{cssItem.type === 'color' && (
<div className="flex items-center gap-4">
<p>{cssOption}</p>
<input
type="color"
onChange={(e) => handleColorChange(cssItem.label, e.target.value)}
value={cssOption}
className="h-5 w-5 cursor-pointer appearance-none bg-transparent"
disabled={
currentCssClassName.length === 0 || currentCssClassName === '클래스를 선택해주세요'
}
/>
</div>
)}
</div>
);
};
23 changes: 23 additions & 0 deletions apps/client/src/entities/workspace/CssTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createPortal } from 'react-dom';

type CssTooltipProps = {
description: string;
isOpen: boolean;
leftX: number;
topY: number;
};

export const CssTooltip = ({ description, isOpen, leftX, topY }: CssTooltipProps) => {
if (!isOpen) {
return null;
}
return createPortal(
<div
className={`text-gray-white fixed left-0 top-0 rounded-3xl ${topY >= 0 ? 'rounded-tl-none' : 'rounded-bl-none'} bg-green-500 px-3 py-2`}
style={{ left: `${leftX + 18}px`, top: topY >= 0 ? `${topY + 8}px` : `${-topY}px` }}
>
<p>{description}</p>
</div>,
document.body
);
};
8 changes: 7 additions & 1 deletion apps/client/src/pages/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import { WorkspaceContent, WorkspacePageHeader } from '@/widgets';

import { Loading } from '@/shared/ui';
import { NotFound } from '@/pages/NotFound';
import { useCssPropsStore } from '@/shared/store';
import { useEffect } from 'react';
import { useGetWorkspace } from '@/shared/hooks';
import { useParams } from 'react-router-dom';

export const WorkspacePage = () => {
const { workspaceId } = useParams();

const { resetCssPropsStore } = useCssPropsStore();
const { isPending, isError } = useGetWorkspace(workspaceId as string);

useEffect(() => {
resetCssPropsStore();
}, []);

if (isError) {
return <NotFound />;
}
Expand Down
84 changes: 84 additions & 0 deletions apps/client/src/shared/hooks/css/useCssOptionItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useCssPropsStore, useCssTooltipStore } from '@/shared/store';
import { useEffect, useState } from 'react';

import { TcssCategoryItem } from '@/shared/types';
import { useCssOptions } from '@/shared/hooks';

export const useCssOptionItem = (cssItem: TcssCategoryItem) => {
const { handleCssOptionChange } = useCssOptions();
const { setOffsetX, setOffsetY } = useCssTooltipStore();
const { currentCssClassName, totalCssPropertyObj } = useCssPropsStore();

const [cssOptionValue, setCssOptionValue] = useState<string>('');
const [isHover, setIsHover] = useState<boolean>(false);
const [indexOfHover, setIndexOfHover] = useState<number>(-1);

const [isChecked, setIsChecked] = useState<boolean>(false);
const [cssOption, setCssOption] = useState<string>('');

useEffect(() => {
if (totalCssPropertyObj[currentCssClassName]) {
setCssOptionValue(totalCssPropertyObj[currentCssClassName].cssOptionObj[cssItem.label] || '');
}
}, [currentCssClassName, totalCssPropertyObj, cssItem.label]);

useEffect(() => {
if (!totalCssPropertyObj[currentCssClassName]) {
setIsChecked(false);
setCssOption(
cssItem.type === 'select' ? cssItem.option![0] : cssItem.type === 'color' ? '#000000' : ''
);
return;
}
setIsChecked(
totalCssPropertyObj[currentCssClassName].checkedCssPropertyObj[cssItem.label] ?? false
);
if (!totalCssPropertyObj[currentCssClassName].cssOptionObj[cssItem.label]) {
setCssOption(cssItem.type === 'select' ? cssItem.option![0] : '');
return;
}
setCssOption(totalCssPropertyObj[currentCssClassName].cssOptionObj[cssItem.label]);
}, [totalCssPropertyObj, currentCssClassName]);

/**
* @description 엔터키 입력시 스타일 프로퍼티 변경 이벤트 핸들러
*/
const handleEnterKey = (property: string, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleCssOptionChange(property, e.currentTarget.value);
e.currentTarget.blur();
e.preventDefault();
}
};

/**
* @description 마우스 엔터 이벤드 핸들러
*/
const handleMouseEnter = (e: React.MouseEvent<SVGElement, MouseEvent>, index: number) => {
setIsHover(true);
setIndexOfHover(index);
setOffsetX(e.currentTarget.getBoundingClientRect().x);
setOffsetY(e.currentTarget.getBoundingClientRect().y);
};

const handleMouseLeave = () => {
setIsHover(false);
setIndexOfHover(-1);
};

const handleChangeInputValue = (e: React.ChangeEvent<HTMLInputElement>) => {
setCssOptionValue(e.target.value);
};

return {
cssOptionValue,
isHover,
indexOfHover,
isChecked,
cssOption,
handleEnterKey,
handleMouseEnter,
handleMouseLeave,
handleChangeInputValue,
};
};
35 changes: 35 additions & 0 deletions apps/client/src/shared/hooks/css/useCssOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { debounce } from '@/shared/utils';
import { useCallback } from 'react';
import { useCssPropsStore } from '@/shared/store';

export const useCssOptions = () => {
const { setCheckedCssPropertyObj, setCssOptionObj, currentCssClassName } = useCssPropsStore();

const handleCssPropertyCheckboxChange = (
property: string,
isChecked: boolean,
cssOption: string
) => {
setCheckedCssPropertyObj(currentCssClassName, property, !isChecked);
if (!isChecked) {
setCssOptionObj(currentCssClassName, property, cssOption);
}
};

const handleCssOptionChange = (property: string, value: string) => {
setCssOptionObj(currentCssClassName, property, value);
};

const handleColorChange = useCallback(
debounce((property: string, value: string) => {
handleCssOptionChange(property, value);
}, 200),
[handleCssOptionChange]
);

return {
handleCssPropertyCheckboxChange,
handleCssOptionChange,
handleColorChange,
};
};
21 changes: 21 additions & 0 deletions apps/client/src/shared/hooks/css/useCssTooltip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCssTooltipStore } from '@/shared/store';
import { useEffect } from 'react';
import { useWindowSize } from '@/shared/hooks';

export const useCssTooltip = () => {
const { leftX, topY, offsetX, offsetY, setLeftX, setTopY } = useCssTooltipStore();

const { screenWidth, screenHeight } = useWindowSize();

useEffect(() => {
const tooltipHeight = 40;
setLeftX(offsetX);
if (offsetY + tooltipHeight > screenHeight) {
setTopY(-offsetY + tooltipHeight); // 높이를 벗어나는 것임
} else {
setTopY(offsetY);
}
}, [offsetX, offsetY, screenWidth, screenHeight]);

return { leftX, topY };
};
22 changes: 22 additions & 0 deletions apps/client/src/shared/hooks/css/useWindowSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';

import { debounce } from '@/shared/utils';

export const useWindowSize = () => {
const [screenWidth, setScreenWidth] = useState<number>(window.innerWidth);
const [screenHeight, setScreenHeight] = useState<number>(window.innerHeight);

useEffect(() => {
const handleResize = debounce(() => {
setScreenWidth(window.innerWidth);
setScreenHeight(window.innerHeight);
}, 200);

window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

return { screenWidth, setScreenWidth, screenHeight, setScreenHeight };
};
5 changes: 5 additions & 0 deletions apps/client/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export { useGetWorkspaceList } from './queries/useGetWorkspaceList';
export { useGetWorkspace } from './queries/useGetWorkspace';
export { useUpdateWorkspaceName } from './queries/useUpdateWorkspaceName';
export { useDeleteWorkspace } from './queries/useDeleteWorkspace';

export { useWindowSize } from './css/useWindowSize';
export { useCssTooltip } from './css/useCssTooltip';
export { useCssOptions } from './css/useCssOptions';
export { useCssOptionItem } from './css/useCssOptionItem';
2 changes: 2 additions & 0 deletions apps/client/src/shared/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { useLoadingStore } from './useLoadingStore';
export { useModalStore } from './useModalStore';
export { useWorkspaceStore } from './useWorkspaceStore';
export { useCssPropsStore } from './useCssPropsStore';
export { useCssTooltipStore } from './useCssTooptipStore';
export { useClassBlockStore } from './useClassBlockStore';
Loading

0 comments on commit 3d5028e

Please sign in to comment.