Skip to content
33 changes: 33 additions & 0 deletions src/components/NotificationBadge/NotificationBadge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Meta, StoryObj } from '@storybook/react';
import NotificationBadge from './NotificationBadge';

const meta: Meta<typeof NotificationBadge> = {
title: 'Components/NotificationBadge',
component: NotificationBadge,
tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof NotificationBadge>;

export const WhenNoNotification: Story = {
args: {
count: 0,
},
};
export const When1Notification: Story = {
args: {
count: 1,
},
};
export const WhenMoreThan99Notification: Story = {
args: {
count: 100,
},
};
export const Default: Story = {
args: {
count: 14,
},
};
32 changes: 32 additions & 0 deletions src/components/NotificationBadge/NotificationBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import clsx from 'clsx';

interface NotificationBadgeProps {
count: number;
className?: string;
}

export default function NotificationBadge({
count,
className = '',
}: NotificationBadgeProps) {
// 음수, NaN 등 의도하지 않은 값 확인
const safeCount = Number.isFinite(count) ? Math.max(0, count) : 0;
if (safeCount === 0) {
return null;
}

const displayCount = safeCount > 99 ? '99+' : safeCount;
Copy link
Contributor

Choose a reason for hiding this comment

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

99+ .. 디테일 !!!


return (
<span
role="status"
aria-label={`알림 ${displayCount}개`}
className={clsx(
'inline-flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-semantic-error px-[4px] text-[10px] font-bold leading-none text-default-white',
className,
)}
>
{displayCount}
</span>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import clsx from 'clsx';
import TimeInputGroup from './TimeInputGroup';
import DTBell from '../../../../components/icons/Bell';
import DTAdd from '../../../../components/icons/Add';
import NotificationBadge from '../../../../components/NotificationBadge/NotificationBadge';
import DTExpand from '../../../../components/icons/Expand';

type TimerCreationOption =
| 'TIMER_TYPE'
Expand Down Expand Up @@ -146,6 +148,9 @@ export default function TimerCreationContent({
`# initSpeech: ${initSpeechType} / currentSpeech: ${currentSpeechType}`,
);

// 종소리 영역 확장 여부
const [isBellExpanded, setIsBellExpanded] = useState(false);

// 발언 시간
const { minutes: initMinutes, seconds: initSeconds } =
Formatting.formatSecondsToMinutes(
Expand Down Expand Up @@ -427,6 +432,10 @@ export default function TimerCreationContent({
[currentSpeechType],
);

const handleBellExpandButtonClick = useCallback(() => {
setIsBellExpanded((prev) => !prev);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

불필요한 함수 생성 방지 조아요 ~!!!


return (
<div className="flex w-[820px] flex-col">
{/* 헤더 */}
Expand Down Expand Up @@ -647,127 +656,163 @@ export default function TimerCreationContent({
case 'BELL':
return (
<div
className="flex w-full flex-col space-y-[16px]"
className="flex w-full flex-col space-y-[8px]"
key={`${timerType}-${index}`}
>
{/* 제목 */}
<p className="text-body w-[80px] font-medium">
종소리 설정
</p>

{/* 입력부 */}
<span className="flex w-full flex-row items-center space-x-[4px]">
{/* 벨 유형 */}
<DropdownMenu
className=""
options={bellOptions}
selectedValue={bellInput.type}
onSelect={(value: BellType) => {
setBellInput((prev) => ({
...prev,
type: value,
}));
}}
/>
<span className="w-[8px]"></span>

{/* 분, 초, 타종 횟수 */}
<input
type="number"
min={0}
max={59}
className="w-[60px] rounded-[4px] border border-default-border p-[8px]"
value={bellInput.min}
onChange={(e) =>
setBellInput((prev) => ({
...prev,
min: Math.max(
0,
Math.min(59, Number(e.target.value)),
),
}))
}
placeholder="분"
/>
<span>분</span>

<input
type="number"
min={0}
max={59}
className="w-[60px] rounded-[4px] border border-default-border p-[8px]"
value={bellInput.sec}
onChange={(e) =>
setBellInput((prev) => ({
...prev,
sec: Math.max(
0,
Math.min(59, Number(e.target.value)),
),
}))
}
placeholder="초"
/>
<span>초</span>
<span className="w-[8px]"></span>

<DTBell className="w-[24px]" />
<p>x</p>
<input
type="number"
min={1}
max={3}
className="w-[60px] rounded-[4px] border border-default-border p-[8px]"
value={bellInput.count}
onChange={(e) => {
const value = Number(e.target.value) % 10;
setBellInput((prev) => ({
...prev,
count: Math.max(1, Math.min(3, Number(value))),
}));
}}
placeholder="횟수"
/>
<span className="w-[8px]"></span>
{/* 제목 및 종소리 횟수 배지 */}
<div className="flex w-full flex-row justify-between">
<div className="relative flex items-center space-x-[8px]">
<p className="text-body w-[80px] font-medium">
종소리 설정
</p>

<NotificationBadge
count={bells.length}
className="absolute -right-[16px] -top-[4px]"
/>
</div>

<button
className="h-full"
type="button"
className="flex size-[28px] items-center justify-center rounded-[8px] bg-default-disabled/hover p-[6px] text-default-white"
onClick={handleAddBell}
aria-label={
isBellExpanded
? '종소리 설정 접기'
: '종소리 설정 펼치기'
}
onClick={handleBellExpandButtonClick}
>
<DTAdd />
<DTExpand
className={clsx(
'h-full transform rounded-full p-[8px] text-default-black transition-all duration-300 ease-in-out hover:bg-default-disabled/hover',
{
'rotate-180': isBellExpanded,
'rotate-0': !isBellExpanded,
},
)}
/>
</button>
</span>

{/* 벨 리스트 */}
<span className="flex h-[100px] w-full flex-col items-center gap-2 overflow-y-auto">
{bells.map((bell, idx) => (
<span
key={idx}
className="relative flex w-full flex-row rounded-[4px] border border-default-border bg-[#FFF2D0] px-[12px] py-[4px]"
>
<div className="flex items-center gap-1">
<p className="text-[14px]">
{BellTypeToString[bell.type]}
</p>
<p className="text-[14px]">
{bell.min}분 {bell.sec}초
</p>

<span className="w-[8px]"></span>
<DTBell className="size-[14px]" />
<span className="text-[14px]">x {bell.count}</span>
</div>
</div>

{/* 종소리 설정 영역 */}
{isBellExpanded && (
<div className="flex w-full flex-col space-y-[8px] rounded-[12px] bg-default-dim2 p-[8px]">
{/* 입력부 */}
<span className="flex w-full flex-row items-center space-x-[4px]">
{/* 벨 유형 */}
<DropdownMenu
options={bellOptions}
selectedValue={bellInput.type}
onSelect={(value: BellType) => {
setBellInput((prev) => ({
...prev,
type: value,
}));
}}
/>
<span className="w-[8px]"></span>

{/* 분, 초, 타종 횟수 */}
<input
type="number"
min={0}
max={59}
className="w-[52px] rounded-[4px] border border-default-border p-[8px]"
value={bellInput.min}
onChange={(e) =>
setBellInput((prev) => ({
...prev,
min: Math.max(
0,
Math.min(59, Number(e.target.value)),
),
}))
}
placeholder="분"
/>
<span>분</span>

<input
type="number"
min={0}
max={59}
className="w-[52px] rounded-[4px] border border-default-border p-[8px]"
value={bellInput.sec}
onChange={(e) =>
setBellInput((prev) => ({
...prev,
sec: Math.max(
0,
Math.min(59, Number(e.target.value)),
),
}))
}
placeholder="초"
/>
<span>초</span>
<span className="w-[8px]"></span>

<DTBell className="w-[24px]" />
<p>x</p>
<input
type="number"
min={1}
max={3}
className="w-[48px] rounded-[4px] border border-default-border p-[8px]"
value={bellInput.count}
onChange={(e) => {
const value = Number(e.target.value) % 10;
setBellInput((prev) => ({
...prev,
count: Math.max(1, Math.min(3, Number(value))),
}));
}}
placeholder="횟수"
/>
<span className="w-[8px]"></span>

<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-default-border"
onClick={() => handleDeleteBell(idx)}
type="button"
className="flex size-[28px] items-center justify-center rounded-[8px] bg-brand p-[6px] text-default-white transition-colors duration-200 hover:bg-brand-hover"
onClick={handleAddBell}
>
<DTClose className="size-[10px]" />
<DTAdd />
</button>
</span>
))}
</span>

{/* 벨 리스트 */}
<span className="flex h-[100px] w-full flex-col items-center gap-2 overflow-y-auto">
{bells.map((bell, idx) => (
<span
key={idx}
className="relative flex w-full flex-row rounded-[4px] border border-default-border bg-[#FFF2D0] px-[12px] py-[4px]"
>
<div className="flex items-center gap-1">
<p className="text-[14px]">
{BellTypeToString[bell.type]}
</p>
<p className="text-[14px]">
{bell.min}분 {bell.sec}초
</p>

<span className="w-[8px]"></span>
<DTBell className="size-[14px]" />
<span className="text-[14px]">
x {bell.count}
</span>
</div>

<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-default-border"
onClick={() => handleDeleteBell(idx)}
>
<DTClose className="size-[10px]" />
</button>
</span>
))}
</span>
</div>
)}
</div>
);
default:
Expand Down
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default {
'disabled/hover': '#D6D7D9',
neutral: '#A3A3A3',
dim: '#222222',
dim2: '#F2F2F2',
border: '#79747E',
timeout: '#4F4F4F',
white: '#FFFFFF',
Expand Down