Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 60 additions & 5 deletions frontend/src/components/whiteboard/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,69 @@
'use client';

// import TextPropertyBar from '@/components/whiteboard/sidebar/properties/TextPropertyBar';
import { useState } from 'react';

// 패널 컴포넌트들 임포트
import ShapePanel from '@/components/whiteboard/sidebar/panels/ShapePanel';

// TODO: 스토어 훅 임포트 필요 (추후 상태 관리 로직 교체)
// import { useWhiteboardStore } from '@/store/useWhiteboardStore';

// 사이드 바 선택된 요소 타입
type SelectionType = 'shape' | null;

export default function Sidebar() {
// TODO : store에서 selectedItemType 가져온 후 조건부 렌더링
// TODO : 상태 관리 로직 교체 필요
// 현재 : useState로 로컬 상태 관리 -> 테스트용도
const [selectionType, setSelectionType] = useState<SelectionType>('shape');

// 추후 : Store에서 선택된 요소의 데이터 구독하는 방식으로 변경 예정
// const selectedId = useWhiteboardStore((state) => state.selectedElementId);
// const elements = useWhiteboardStore((state) => state.elements);
// const updateElement = useWhiteboardStore((state) => state.updateElement);
// const selectedElement = selectedId ? elements[selectedId] : null;
// const selectionType = selectedElement ? selectedElement.type : 'none';

// TODO : 데이터 매핑
// const strokeColor = selectedElement?.strokeColor || '#000000';
// const backgroundColor = selectedElement?.backgroundColor || 'transparent';
const [strokeColor, setStrokeColor] = useState('#000000');
const [backgroundColor, setBackgroundColor] = useState('transparent');

// 선택 타입에 따른 표시될 헤더 제목
const getHeaderTitle = () => {
switch (selectionType) {
case 'shape':
return 'Shape';
default:
return '';
}
};

return (
<aside className="fixed top-20 left-4 z-40 flex flex-col gap-4">
{/* Todo : 상단의 Toolbar에서 선택된 요소에 따른 사이드바 조건부 렌더링*/}
{/* <TextPropertyBar /> */}
<aside className="absolute top-1/2 left-2 z-1 flex max-h-[calc(100vh-2rem)] w-56 -translate-y-1/2 flex-col overflow-y-auto rounded-lg border border-neutral-200 bg-white p-4 shadow-xl">
{/* Sidebar Title */}
<div className="mb-1">
<h2 className="text-lg font-bold text-neutral-800">
{getHeaderTitle()}
</h2>
</div>

{/* 패널 영역 */}
<div className="flex-1">
{/* shape */}
{selectionType === 'shape' && (
<ShapePanel
strokeColor={strokeColor}
backgroundColor={backgroundColor}
// TODO: 업데이트 함수 교체
// 변경된 값 스토어에 전달하는 방식 변경 필요
// onChangeStrokeColor={(newColor) => updateElement(selectedId, { strokeColor: newColor })}
onChangeStrokeColor={setStrokeColor}
// onChangeBackgroundColor={(newColor) => updateElement(selectedId, { backgroundColor: newColor })}
onChangeBackgroundColor={setBackgroundColor}
/>
)}
</div>
</aside>
);
}
34 changes: 34 additions & 0 deletions frontend/src/components/whiteboard/sidebar/panels/ShapePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import StrokeColorSection from '@/components/whiteboard/sidebar/sections/StrokeColorSection';
import BackgroundColorSection from '@/components/whiteboard/sidebar/sections/BackgroundColorSection';

// ShapePanel 컴포넌트
interface ShapePanelProps {
strokeColor: string;
backgroundColor: string;
onChangeStrokeColor: (color: string) => void;
onChangeBackgroundColor: (color: string) => void;
}

export default function ShapePanel({
strokeColor,
backgroundColor,
onChangeStrokeColor,
onChangeBackgroundColor,
}: ShapePanelProps) {
return (
<div className="flex flex-col gap-2">
{/* 도형 테두리 색상 설정 섹션 */}
<StrokeColorSection color={strokeColor} onChange={onChangeStrokeColor} />

{/* 도형 배경 색상 설정 섹션 */}
<BackgroundColorSection
color={backgroundColor}
onChange={onChangeBackgroundColor}
/>

{/* 도형 관련 설정 추가 */}
</div>
);
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import Section from '@/components/whiteboard/sidebar/ui/Section';
import ColorPicker from '@/components/whiteboard/sidebar/ui/ColorPicker';

// 배경 색상 설정 section
interface BackgroundColorSectionProps {
color: string;
onChange: (color: string) => void;
}

export default function BackgroundColorSection({
color,
onChange,
}: BackgroundColorSectionProps) {
return (
<Section title="Background">
<ColorPicker color={color} onChange={onChange} allowTransparent={true} />
</Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import Section from '@/components/whiteboard/sidebar/ui/Section';
import ColorPicker from '@/components/whiteboard/sidebar/ui/ColorPicker';

// 테두리 색상 설정 section
interface StrokeColorSectionProps {
color: string;
onChange: (color: string) => void;
}

export default function StrokeColorSection({
color,
onChange,
}: StrokeColorSectionProps) {
return (
<Section title="Stroke">
<ColorPicker color={color} onChange={onChange} allowTransparent={true} />
</Section>
);
}
31 changes: 31 additions & 0 deletions frontend/src/components/whiteboard/sidebar/ui/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import PaletteGrid from '@/components/whiteboard/sidebar/ui/color-picker/PaletteGrid';
import CustomColorArea from '@/components/whiteboard/sidebar/ui/color-picker/CustomColorArea';

// ColorPicker 컴포넌트의 Props 타입 정의
interface ColorPickerProps {
color: string;
onChange: (color: string) => void;
allowTransparent?: boolean;
}

export default function ColorPicker({
color,
onChange,
allowTransparent = false,
}: ColorPickerProps) {
return (
<div className="flex flex-col">
{/* 기본 색상 팔레트 */}
<PaletteGrid
currentColor={color}
onChange={onChange}
allowTransparent={allowTransparent}
/>

{/* 사용자 지정 및 최근 색상 */}
<CustomColorArea currentColor={color} onChange={onChange} />
</div>
);
}
22 changes: 22 additions & 0 deletions frontend/src/components/whiteboard/sidebar/ui/Section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

// 섹션 컴포넌트
// title : 섹션 제목
// children : 섹션 내용
interface SectionProps {
title?: string;
children: React.ReactNode;
}

export default function Section({ title, children }: SectionProps) {
return (
<div className="flex flex-col gap-2 py-2">
{title && (
<h3 className="text-xs font-bold tracking-wide text-black uppercase select-none">
{title}
</h3>
)}
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

// 색상 버튼 컴포넌트(색상 하나만 담당)
// 클릭 시 해당 색상으로 변경
// color : 색상
// label : 색상 이름
// isSelected : 현재 선택된 색상 여부
// onClick : 클릭 시 동작 함수
interface ColorButtonProps {
color: string;
label?: string;
isSelected: boolean;
onClick: () => void;
}

export default function ColorButton({
color,
label,
isSelected,
onClick,
}: ColorButtonProps) {
return (
<button
onClick={onClick}
className={`relative h-6 w-6 rounded-md border border-neutral-200 transition-all focus:outline-none ${
isSelected ? 'z-1 scale-120 ring-2 ring-sky-300' : 'hover:scale-110'
}`}
style={{ backgroundColor: color === 'transparent' ? 'white' : color }}
title={label || color}
>
{color === 'transparent' && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-full w-0.5 rotate-45 bg-red-400 opacity-50" />
</div>
)}
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';

import { useState } from 'react';

import { getColorName } from '@/utils/color';

import ColorButton from '@/components/whiteboard/sidebar/ui/color-picker/ColorButton';
import CustomColorInput from '@/components/whiteboard/sidebar/ui/color-picker/CustomColorInput';

// CustomColorArea: 사용자 지정 색상 선택 및 최근 사용 색상 관리 컴포넌트
// currentColor: 현재 선택된 색상
// onChange: 색상 변경 시 호출되는 콜백 함수
interface CustomColorAreaProps {
currentColor: string;
onChange: (color: string) => void;
}

export default function CustomColorArea({
currentColor,
onChange,
}: CustomColorAreaProps) {
// 최근 사용된 색상이 저장된 배열
const [recentColors, setRecentColors] = useState<string[]>([]);

// 색상 변경 처리 함수
const handleCustomColorChange = (newColor: string) => {
onChange(newColor);

// 최근 색상 목록 변경 로직
// - 투명 저장 제외
// - 중복 저장 방지
if (newColor !== 'transparent' && !recentColors.includes(newColor)) {
setRecentColors((prev) => {
return [newColor, ...prev].slice(0, 5);
});
}
};

return (
<div className="mt-2 flex items-center gap-1 border-t border-neutral-300 pt-2">
{/* 사용자 지정 색상 선택기 */}
<CustomColorInput
color={currentColor}
onChange={(e) => handleCustomColorChange(e.target.value)}
/>

{/* 구분선 (최근 색상 있는 경우만 표시) */}
{recentColors.length > 0 && (
<div className="mx-1 h-4 w-px bg-neutral-300" />
)}

{/* 최근 사용 색상 리스트 */}
<div className="flex gap-1.5">
{recentColors.map((color) => (
<ColorButton
key={color}
color={color}
label={getColorName(color)}
isSelected={currentColor === color}
onClick={() => handleCustomColorChange(color)}
/>
))}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';

// 사용자 색상 선택기
// color : 현재 선택된 색상
// onChange : 색상 변경 시 호출 함수
interface CustomColorInputProps {
color: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export default function CustomColorInput({
color,
onChange,
}: CustomColorInputProps) {
return (
<div className="relative h-7 w-7 cursor-pointer overflow-hidden rounded-md border border-neutral-200 shadow-sm transition-opacity hover:opacity-80">
<input
type="color"
// 투명 색상 선택 시 흰색으로 표시(오류 방지)
value={color === 'transparent' ? '#ffffff' : color}
onChange={onChange}
className="absolute top-1/2 left-1/2 h-[200%] w-[200%] -translate-x-1/2 -translate-y-1/2 cursor-pointer opacity-0"
/>

{/* 색상 선택기 버튼 그라데이션 */}
<div className="h-full w-full bg-[linear-gradient(to_bottom_right,#fca5a5,#fdba74,#fde047,#86efac,#93c5fd,#a5b4fc,#d8b4fe)]" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { FULL_PALETTE, NO_TRANSPARENT_PALETTE } from '@/constants/colors';

import { getColorName } from '@/utils/color';

import ColorButton from '@/components/whiteboard/sidebar/ui/color-picker/ColorButton';

// 색상 버튼 모음 컴포넌트(팔레트)
interface PaletteGridProps {
currentColor: string;
onChange: (color: string) => void;
allowTransparent: boolean;
}

export default function PaletteGrid({
currentColor,
onChange,
allowTransparent,
}: PaletteGridProps) {
// palette : 투명 허용 여부에 따른 데이터 배열 선택
const palette = allowTransparent ? FULL_PALETTE : NO_TRANSPARENT_PALETTE;

return (
<div className="grid grid-cols-5 gap-2">
{palette.map((color) => (
<ColorButton
key={color}
color={color}
label={getColorName(color)}
isSelected={currentColor === color}
onClick={() => onChange(color)}
/>
))}
</div>
);
}
Loading