diff --git a/package-lock.json b/package-lock.json index 2456b2a1..7ed267f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2752,15 +2752,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tanstack/query-core": { "version": "5.54.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.54.1.tgz", @@ -2785,6 +2776,15 @@ "react": "^18 || ^19" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", diff --git a/public/icons/dalaemfit.svg b/public/icons/dalaemfit.svg index efd40e6b..ad1e2ad3 100644 --- a/public/icons/dalaemfit.svg +++ b/public/icons/dalaemfit.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/public/icons/workation.svg b/public/icons/workation.svg index a6f60222..96e67bb0 100644 --- a/public/icons/workation.svg +++ b/public/icons/workation.svg @@ -1,15 +1,15 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/app/components/Badge/Badge.tsx b/src/app/components/Badge/Badge.tsx new file mode 100644 index 00000000..88169e41 --- /dev/null +++ b/src/app/components/Badge/Badge.tsx @@ -0,0 +1,13 @@ +interface BadgeProps { + children: string; +} + +const Badge = ({ children }: BadgeProps) => { + return ( +
+ {children} +
+ ); +}; + +export default Badge; diff --git a/src/app/components/BoxSelect/BoxSelect.tsx b/src/app/components/BoxSelect/BoxSelect.tsx new file mode 100644 index 00000000..782b700f --- /dev/null +++ b/src/app/components/BoxSelect/BoxSelect.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { ChangeEvent, useState } from 'react'; + +/** + * BoxSelect component + * @param {string} title - 제목 + * @param {string} subTitle - 부제목 + */ + +interface BoxSelectProps { + title: string; + subTitle: string; +} + +const BoxSelect = ({ title = '', subTitle = '' }: BoxSelectProps) => { + const [isSelected, setIsSelected] = useState(false); + + const handleCheckboxChange = (event: ChangeEvent) => { + setIsSelected(event.target.checked); + }; + + return ( + + ); +}; + +export default BoxSelect; diff --git a/src/app/components/Chip/Chip.tsx b/src/app/components/Chip/Chip.tsx new file mode 100644 index 00000000..f2fa50fe --- /dev/null +++ b/src/app/components/Chip/Chip.tsx @@ -0,0 +1,21 @@ +interface ChipProps { + state?: 'default' | 'active'; + children: string; +} + +const stateClasses = { + default: 'bg-var-gray-200 text-var-bg-gray-900', + active: 'bg-var-gray-900 text-white', +}; + +const Chip = ({ state = 'default', children }: ChipProps) => { + return ( + + {children} + + ); +}; + +export default Chip; diff --git a/src/app/components/Chip/InfoChip.tsx b/src/app/components/Chip/InfoChip.tsx new file mode 100644 index 00000000..e0d76ddb --- /dev/null +++ b/src/app/components/Chip/InfoChip.tsx @@ -0,0 +1,21 @@ +interface InfoChipProps { + type?: 'date' | 'time'; + children: string; +} + +const typeClasses = { + date: 'text-white', + time: 'text-var-orange-600', +}; + +const InfoChip = ({ type = 'date', children }: InfoChipProps) => { + return ( + + {children} + + ); +}; + +export default InfoChip; diff --git a/src/app/components/Chip/StateChip.tsx b/src/app/components/Chip/StateChip.tsx new file mode 100644 index 00000000..d822f614 --- /dev/null +++ b/src/app/components/Chip/StateChip.tsx @@ -0,0 +1,32 @@ +import { IconCheck } from '@/public/icons'; + +interface StateChipProps { + state: 'scheduled' | 'done' | 'confirmed' | 'pending'; // 이용 예정 | 이용 완료 | 개설 확정 | 개설 대기 +} + +const stateClasses = { + scheduled: 'bg-var-orange-100 text-var-orange-600', + done: 'bg-var-gray-200 text-var-gray-500', + confirmed: 'bg-white text-var-orange-500 border border-var-orange-100', + pending: 'bg-white text-var-gray-500 border border-var-gray-200', +}; + +const stateContents = { + scheduled: '이용 예정', + done: '이용 완료', + confirmed: '개설 확정', + pending: '개설 대기', +}; + +const StateChip = ({ state }: StateChipProps) => { + return ( + + {state === 'confirmed' ? : null} + {stateContents[state]} + + ); +}; + +export default StateChip; diff --git a/src/app/components/Chip/TimeChip.tsx b/src/app/components/Chip/TimeChip.tsx new file mode 100644 index 00000000..cde516a9 --- /dev/null +++ b/src/app/components/Chip/TimeChip.tsx @@ -0,0 +1,23 @@ +interface TimeChipProps { + state?: 'default' | 'active' | 'disabled'; + children: string; +} + +const stateClasses = { + default: + 'bg-var-gray-50 border border-var-gray-200 text-var-gray-900 cursor-pointer', + active: 'bg-var-gray-900 text-white cursor-pointer', + disabled: 'bg-var-gray-200 text-var-gray-400 cursor-default', +}; + +const TimeChip = ({ state = 'default', children }: TimeChipProps) => { + return ( + + {children} + + ); +}; + +export default TimeChip; diff --git a/src/app/components/Filter/FilterList.tsx b/src/app/components/Filter/FilterList.tsx new file mode 100644 index 00000000..16a6259b --- /dev/null +++ b/src/app/components/Filter/FilterList.tsx @@ -0,0 +1,24 @@ +import { IconCaret } from '@/public/icons'; + +interface FilterListProps { + state?: 'default' | 'active'; + children: string; +} + +const stateClasses = { + default: 'border border-var-gray-100 bg-var-gray-50 text-var-gray-800', + active: 'text-var-gray-50 bg-var-gray-900', +}; + +const FilterList = ({ state = 'default', children }: FilterListProps) => { + return ( +
+ {children} + +
+ ); +}; + +export default FilterList; diff --git a/src/app/components/Filter/FilterSort.tsx b/src/app/components/Filter/FilterSort.tsx new file mode 100644 index 00000000..a563ae21 --- /dev/null +++ b/src/app/components/Filter/FilterSort.tsx @@ -0,0 +1,24 @@ +import { IconSort } from '@/public/icons'; + +interface FilterSortProps { + state?: 'default' | 'active'; + children: string; +} + +const stateClasses = { + default: 'border border-var-gray-100 bg-var-gray-50 text-var-gray-800', + active: 'text-var-gray-50 bg-var-gray-900', +}; + +const FilterSort = ({ state = 'default', children }: FilterSortProps) => { + return ( +
+ + {children} +
+ ); +}; + +export default FilterSort; diff --git a/src/app/components/Gnb/Gnb.tsx b/src/app/components/Gnb/Gnb.tsx new file mode 100644 index 00000000..659ab889 --- /dev/null +++ b/src/app/components/Gnb/Gnb.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { Logo } from '@/public/images'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import UserStatus from './UserStatus'; +import TopTab from '../Tab/TopTab'; +import Badge from '../Badge/Badge'; + +/* @todo 웹 스토리지에 저장된 찜한 모임의 length를 추출하는 방식으로 변경 예정 */ +const favoriteGroups = '12'; + +//@todo pathname 정해질 시 추가 예정 +const navList = [ + { + name: '모임 찾기', + link: '#', + }, + { + name: '찜한 모임', + link: '#', + }, + { + name: '모든 리뷰', + link: '#', + }, +]; + +const Gnb = () => { + const pathname = usePathname(); + + /* 찜한 모임 배지 렌더링 함수 + nav.name이 '찜한 모임'이고 웹스토리지에 저장된 '찜한 모임'의 개수가 1개 이상일 시 Badge 렌더링 */ + const renderBadge = (name: string) => { + return name === '찜한 모임' && Number(favoriteGroups) > 0 ? ( +
+ {favoriteGroups} +
+ ) : null; + }; + + return ( +
+
+ + +
+
+ ); +}; + +export default Gnb; diff --git a/src/app/components/Gnb/UserStatus.tsx b/src/app/components/Gnb/UserStatus.tsx new file mode 100644 index 00000000..dd50a219 --- /dev/null +++ b/src/app/components/Gnb/UserStatus.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { Profile } from '@/public/images'; +import Link from 'next/link'; +import { useState, useRef, useEffect } from 'react'; + +/* user을 null로 설정하면 로그아웃된 상황을 볼 수 있습니다. */ +const user = { + name: 'test name', +}; + +const options = [{ name: '마이페이지' }, { name: '로그아웃' }]; + +const UserStatus = () => { + const [isOpen, setIsOpen] = useState(false); + const dropDownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropDownRef.current && + !dropDownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + <> + {user ? ( +
+ setIsOpen((prev) => !prev)} + /> + {isOpen && ( +
    + {options.map((option, index) => ( +
  • + {option.name} +
  • + ))} +
+ )} +
+ ) : ( + + {/* @todo 임시 경로입니다. */} +
로그인
+ + )} + + ); +}; + +export default UserStatus; diff --git a/src/app/components/commons/input/Input.tsx b/src/app/components/Input/Input.tsx similarity index 99% rename from src/app/components/commons/input/Input.tsx rename to src/app/components/Input/Input.tsx index b00f47e6..b2f473c8 100644 --- a/src/app/components/commons/input/Input.tsx +++ b/src/app/components/Input/Input.tsx @@ -1,4 +1,5 @@ 'use client'; + import { forwardRef, InputHTMLAttributes } from 'react'; // @todo: 추후 파일 분리예정 diff --git a/src/app/components/commons/input/InputText.tsx b/src/app/components/Input/InputText.tsx similarity index 99% rename from src/app/components/commons/input/InputText.tsx rename to src/app/components/Input/InputText.tsx index fa5dbe83..841b608b 100644 --- a/src/app/components/commons/input/InputText.tsx +++ b/src/app/components/Input/InputText.tsx @@ -1,4 +1,5 @@ 'use client'; + import { ChangeEvent } from 'react'; import { InputStyles } from './Input'; diff --git a/src/app/components/Tab/Tab.tsx b/src/app/components/Tab/Tab.tsx new file mode 100644 index 00000000..1db8bace --- /dev/null +++ b/src/app/components/Tab/Tab.tsx @@ -0,0 +1,32 @@ +import { IconDalaemfit, IconWorkation } from '@/public/icons'; + +interface TabProps { + type: 'workation' | 'dalaemfit'; + isActive: boolean; + onClick: () => void; +} + +const Tab = ({ type, isActive, onClick }: TabProps) => { + const styles = isActive + ? 'text-var-gray-900 first-of-type:before:right-0 last-of-type:before:left-0' + : 'text-var-gray-400 first-of-type:before:-right-full last-of-type:before:-left-full'; + + return ( +
+ {type === 'workation' ? ( + <> + 워케이션 + + ) : ( + <> + 달램핏 + + )} +
+ ); +}; + +export default Tab; diff --git a/src/app/components/Tab/TopTab.tsx b/src/app/components/Tab/TopTab.tsx new file mode 100644 index 00000000..6b44455e --- /dev/null +++ b/src/app/components/Tab/TopTab.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; + +interface TopTabProps { + isActive?: boolean; + children: ReactNode; +} + +const TopTab = ({ isActive, children }: TopTabProps) => { + return ( +
+ {children} +
+ ); +}; + +export default TopTab; diff --git a/src/app/components/Tag/Tag.tsx b/src/app/components/Tag/Tag.tsx new file mode 100644 index 00000000..67accc95 --- /dev/null +++ b/src/app/components/Tag/Tag.tsx @@ -0,0 +1,24 @@ +import { IconAlarm } from '@/public/icons'; + +interface TagProps { + size?: 'large' | 'small'; + children: string; +} + +const sizeClasses = { + large: 'pr-16 rounded-tr-[22px]', + small: 'pr-[10px]', +}; + +const Tag = ({ size = 'large', children }: TagProps) => { + return ( +
+ + {children} +
+ ); +}; + +export default Tag; diff --git a/tailwind.config.ts b/tailwind.config.ts index 262106f8..63a2651b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -18,6 +18,10 @@ const config: Config = { './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { + screens: { + md: { min: '768px' }, + lg: { min: '1024px' }, + }, extend: { spacing: PX_ENTRIES, fontFamily: { @@ -55,6 +59,15 @@ const config: Config = { 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, + fontSize: { + 12: ['1.2rem', '1.6rem'], + 14: ['1.4rem', '2rem'], + 16: ['1.6rem', '2.4rem'], + 18: ['1.8rem', '2.8rem'], + 20: ['2.0rem', '2.8rem'], + 24: ['2.4rem', '3.2rem'], + 30: ['3rem', '3.6rem'], + }, }, }, plugins: [],