diff --git a/.eslintrc b/.eslintrc index 7df4352d..480ad3c1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,8 @@ "sourceType": "module", "ecmaFeatures": { "jsx": true - } + }, + "project": "./tsconfig.app.json" }, "plugins": ["@typescript-eslint", "react", "react-hooks", "react-refresh", "check-file"], "settings": { diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index f8170565..05b6b2af 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,23 +4,21 @@ about: 버그를 제보합니다. title: 'fix: 이슈 제목' labels: '' assignees: '' - --- ## 🐛 버그 설명 - ## 🔄 재현 방법 + 1. 2. 3. ## ✅ 예상 동작 - ## ❌ 실제 동작 - ## 💡 참고 사항 (Optional) + - 브라우저/환경: - 스크린샷: diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index ceef5680..bc339a01 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -4,13 +4,14 @@ about: 새로운 기능을 제안합니다. title: 'type: 이슈 제목' labels: '' assignees: '' - --- ## ✨ 구현할 기능 - ## 📝 TODO +- [] +- [] +- [] ## 💡 참고 사항 (Optional) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e5b91cb7..7b6ffdf2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,9 @@ ## ✨ 변경 내용 -어떤 기능/수정이 포함되었는지 간단 요약 +- +- +- ## 💡 참고 사항 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b917b3ad..1893900c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,23 +1,98 @@ # Copilot 코드 리뷰 지침 -## 언어 +> 한글로 리뷰해주세요 -- 한글로 리뷰해주세요 +--- + +## 기술 스택 + +- **Framework:** React 18 + TypeScript +- **Build:** Vite +- **Styling:** Tailwind CSS v4 +- **Routing:** React Router + +--- ## 네이밍 컨벤션 -- 폴더명: `kebab-case` -- 일반 파일(.ts): `camelCase` -- 컴포넌트 파일(.tsx): `PascalCase` -- 변수/함수명: `camelCase` -- 상수명: `UPPER_SNAKE_CASE` -- 컴포넌트명: `PascalCase` -- 페이지 컴포넌트: `PascalCase + Page` (예: `MainPage`) +| 대상 | 규칙 | 예시 | +| -------------------- | ------------------- | ----------------- | +| 폴더명 | `kebab-case` | `user-profile` | +| 일반 파일 (.ts) | `camelCase` | `useAuth.ts` | +| 컴포넌트 파일 (.tsx) | `PascalCase` | `UserCard.tsx` | +| 변수/함수명 | `camelCase` | `getUserName` | +| 상수명 | `UPPER_SNAKE_CASE` | `MAX_RETRY_COUNT` | +| 컴포넌트명 | `PascalCase` | `UserCard` | +| 페이지 컴포넌트 | `PascalCase + Page` | `MainPage` | +| 타입/인터페이스 | `PascalCase` | `UserProps` | + +--- + +## 폴더 구조 + +``` +src/ +├── components/ # 재사용 컴포넌트 +│ └── layout/ # 레이아웃 컴포넌트 +├── constants/ # 상수 +├── hooks/ # 커스텀 훅 +├── pages/ # 페이지 컴포넌트 +├── router/ # 라우터 설정 +├── styles/ # 글로벌 스타일, 디자인 토큰 +└── utils/ # 유틸 함수 +``` + +--- ## Export 규칙 -- 컴포넌트: `export default` -- 유틸 함수: `named export` +- **컴포넌트:** `export function` (named export) +- **페이지:** `export default` +- **유틸 함수:** `named export` +- **타입:** `export type` / `export interface` + +--- + +## 스타일링 + +### Tailwind CSS + +- `style` 속성보다 Tailwind 유틸리티 클래스를 우선 사용해주세요 +- 색상은 디자인 시스템 팔레트를 사용해주세요 (`text-gray-600`, `bg-main`) + +### 디자인 토큰 + +```css +/* 색상 */ +--color-main, --color-gray-{100-900}, --color-error, --color-success + +/* 타이포그래피 */ +.text-body-m, .text-body-s, .text-caption +``` + +### 단위 + +- `px` 대신 `rem`을 사용해주세요 + +--- + +## React + +### 컴포넌트 + +- Props는 `interface`로 정의해주세요 +- 비즈니스 로직(Custom Hooks)과 UI(Component)를 분리해주세요 + +### Hooks + +- `useEffect` 의존성 배열이 올바른지 확인해주세요 +- 불필요한 리렌더링이 발생할 수 있는 코드인지 확인해주세요 + +### 라우팅 + +- `` 태그 대신 `` 또는 `useNavigate`를 사용해주세요 (SPA 동작 보장) + +--- ## 코드 품질 @@ -25,14 +100,35 @@ - 사용하지 않는 import가 있는지 확인해주세요 - TypeScript 타입이 `any`로 되어있는지 확인해주세요 - 하드코딩된 값이 상수로 분리되어야 하는지 확인해주세요 +- 매직 넘버는 의미 있는 상수명으로 분리해주세요 -## React +--- -- 컴포넌트 props는 interface로 정의해주세요 -- useEffect 의존성 배열이 올바른지 확인해주세요 -- 불필요한 리렌더링이 발생할 수 있는 코드인지 확인해주세요 +## 접근성 (Accessibility) + +- 의미 없는 `
` 대신 시맨틱 태그를 적극 사용해주세요 +- 모든 이미지(``)에는 적절한 `alt` 텍스트를 제공해주세요 +- 버튼과 링크는 명확한 레이블을 가져야 합니다 +- `role`, `aria-*` 속성을 적절히 사용해주세요 + +--- ## 보안 - API 키나 민감한 정보가 하드코딩되어 있는지 확인해주세요 - 환경변수는 `VITE_` 접두사를 사용해야 합니다 + +--- + +## Git 커밋 컨벤션 + +``` +: (#issue) + +feat: 새로운 기능 +fix: 버그 수정 +refactor: 리팩토링 +style: 코드 포맷팅 +chore: 기타 작업 +docs: 문서 수정 +``` diff --git a/index.html b/index.html index dc243c95..fc453bc0 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,12 @@ - ttorang-frontend + 또랑 + +
diff --git a/package-lock.json b/package-lock.json index 2605ff15..2f66b2d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.18", + "clsx": "^2.1.1", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.11.0", "tailwindcss": "^4.1.18", "zustand": "^5.0.9" }, @@ -68,7 +70,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2220,7 +2221,6 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2231,7 +2231,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2281,7 +2280,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -2605,7 +2603,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2920,7 +2917,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3131,6 +3127,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3228,13 +3233,25 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -3775,7 +3792,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6406,7 +6422,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6465,7 +6480,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6489,6 +6503,44 @@ "dev": true, "license": "MIT" }, + "node_modules/react-router": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", + "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", + "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", + "license": "MIT", + "dependencies": { + "react-router": "7.11.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6751,6 +6803,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7275,7 +7333,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7419,7 +7476,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7513,7 +7569,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7605,7 +7660,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7933,7 +7987,6 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 5b520eb2..b81f7ea8 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "clsx": "^2.1.1", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.11.0", "tailwindcss": "^4.1.18", "zustand": "^5.0.9" }, diff --git a/src/components/layout/GNB.tsx b/src/components/layout/GNB.tsx new file mode 100644 index 00000000..144f59b7 --- /dev/null +++ b/src/components/layout/GNB.tsx @@ -0,0 +1,42 @@ +import clsx from 'clsx'; + +import { TABS, type Tab } from '../../constants/navigation'; + +interface GNBProps { + activeTab?: Tab; + onTabChange?: (tab: Tab) => void; +} + +export function GNB({ activeTab = 'slide', onTabChange }: GNBProps) { + return ( + + ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 00000000..9dd91d49 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react'; + +interface HeaderProps { + children?: ReactNode; +} + +export function Header({ children }: HeaderProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx new file mode 100644 index 00000000..1517a961 --- /dev/null +++ b/src/components/layout/Layout.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from 'react'; + +import { Header } from './Header'; + +interface LayoutProps { + header: ReactNode; + children?: ReactNode; +} + +export function Layout({ children, header }: LayoutProps) { + return ( +
+
{header}
+
{children}
+
+ ); +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 00000000..985c4f27 --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1,3 @@ +export { GNB } from './GNB'; +export { Header } from './Header'; +export { Layout } from './Layout'; diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts new file mode 100644 index 00000000..9224f64e --- /dev/null +++ b/src/constants/navigation.ts @@ -0,0 +1,7 @@ +export const TABS = [ + { key: 'slide', label: '슬라이드' }, + { key: 'video', label: '영상' }, + { key: 'insight', label: '인사이트' }, +] as const; + +export type Tab = (typeof TABS)[number]['key']; diff --git a/src/styles/fonts.css b/src/styles/fonts.css deleted file mode 100644 index 2c2e7242..00000000 --- a/src/styles/fonts.css +++ /dev/null @@ -1 +0,0 @@ -@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css'); diff --git a/src/styles/index.css b/src/styles/index.css index e5706ed8..1da34e2c 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,5 +1,4 @@ @import "tailwindcss"; -@import "./fonts.css"; @import "./theme.css"; @import "./typography.css";