-
Notifications
You must be signed in to change notification settings - Fork 2
[DESIGN] 종소리 설정에 아코디언 UI 적용 #359
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9096fad
design: 새로운 배경 색상 추가
i-meant-to-be 20a88ae
feat: 알림 배지 컴포넌트 추가
i-meant-to-be 312513c
design: 종소리 설정에 아코디언 UI 적용
i-meant-to-be 90b21c1
refactor: NotificationBadge가 NaN 또는 음수에 대응하도록 개선
i-meant-to-be c6ebffc
feat: 아코디언 버튼에 aria 태그 추가
i-meant-to-be 79bc051
refactor: 함수형 값 업데이트 적용
i-meant-to-be 90840c2
feat: 확장 여부에 따라 아이콘 방향이 달라지도록 개선
i-meant-to-be c7597a7
refactor: 함수 최적화
i-meant-to-be 73e47a0
refactor: 불필요한 className 제거
i-meant-to-be File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
src/components/NotificationBadge/NotificationBadge.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
|
||
| 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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
|
|
@@ -146,6 +148,9 @@ export default function TimerCreationContent({ | |
| `# initSpeech: ${initSpeechType} / currentSpeech: ${currentSpeechType}`, | ||
| ); | ||
|
|
||
| // 종소리 영역 확장 여부 | ||
| const [isBellExpanded, setIsBellExpanded] = useState(false); | ||
|
|
||
| // 발언 시간 | ||
| const { minutes: initMinutes, seconds: initSeconds } = | ||
| Formatting.formatSecondsToMinutes( | ||
|
|
@@ -427,6 +432,10 @@ export default function TimerCreationContent({ | |
| [currentSpeechType], | ||
| ); | ||
|
|
||
| const handleBellExpandButtonClick = useCallback(() => { | ||
| setIsBellExpanded((prev) => !prev); | ||
| }, []); | ||
|
Contributor
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. 불필요한 함수 생성 방지 조아요 ~!!! |
||
|
|
||
| return ( | ||
| <div className="flex w-[820px] flex-col"> | ||
| {/* 헤더 */} | ||
|
|
@@ -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="횟수" | ||
| /> | ||
i-meant-to-be marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <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: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
99+ .. 디테일 !!!