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: [],