diff --git a/README.md b/README.md index 0d00d661..6935b0dd 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,39 @@ -

- Finders Logo -

- -

Finders · Web

- -

- 뷰파인더 너머, 너의 취향을 찾다
- Beyond the viewfinder, find your taste -

- -

- 필름 현상소와 필름 사용자를 잇는 웹 플랫폼
- A web platform connecting film labs and film enthusiasts -

+Image
---- - ## 🗓️ 프로젝트 기간 > **2025.12.19 ~ ing** ---- +
## 📱 프로젝트 소개 -**Finders**는 필름 카메라 사용자가 웹에서 현상소를 탐색하고, -예약과 결제까지 한 번에 처리할 수 있도록 돕는 서비스입니다. - -AI 사진 복원, 커뮤니티 피드, 필름 카메라 가이드 등 -**필름 라이프 전반을 아우르는 경험**을 제공합니다. - ---- +> **Finders**는 필름 카메라 사용자가 웹에서 현상소를 탐색하고, 예약과 결제까지 한 번에 처리할 수 있도록 돕는 서비스입니다. +> AI 사진 복원, 커뮤니티 피드, 필름 카메라 가이드 등 **필름 라이프 전반을 아우르는 경험**을 제공합니다. -## ⭐ 핵심 기능 +### ⭐ 핵심 기능 | Feature | Description | | :---------------- | :----------------------------------------------- | | 로그인 / 회원가입 | 카카오톡 기반 소셜 로그인 | | 필카 입문 101 | 필름 카메라 입문자를 위한 가이드 콘텐츠 | | AI 사진 복원 | Replicate AI 기반 사진 복원 기능 | -| 현상소 탐색 | 위치·가격·리뷰 기반 현상소 검색 | +| 현상소 보기 | 위치·가격·리뷰 기반 현상소 검색 | | 온라인 예약 | 날짜·옵션 선택을 통한 비대면 예약 | | 사진 수다 | 사진 공유 및 커뮤니티 소통 기능 | -| 현상 진행 관리 | 현상 완료 사진 관리, 배송 및 방문 수령 상태 확인 | +| 현상 관리 | 현상 완료 사진 관리, 배송 및 방문 수령 상태 확인 | ---- +Image +Image +Image +Image +Image +Image + +
+
## 👩‍💻 Web 파트 @@ -54,38 +41,257 @@ AI 사진 복원, 커뮤니티 피드, 필름 카메라 가이드 등 | :------------------------------------------------------------------: | :---------------------------------------------------------------------: | :----------------------------------------------------------------------: | :-----------------------------------------------------------------------: | | 김민석 | 김혜린 | 전병국 | 한현서 | | `Web Developer` | `Web Developer` | `Web Developer` | `Web Developer` | -| 현상소 보기
온라인 예약
현상관리 | 사진 수다 커뮤니티
현상관리 | 로그인 / 회원가입
마이페이지
현상관리 | 메인페이지
AI 사진 복원
현상관리 | +| 현상소 보기
온라인 예약
현상관리 | 사진수다 커뮤니티
현상관리 | 로그인 / 회원가입
마이페이지
현상관리 | 메인페이지
AI 사진 복원
현상관리 | + +
+ +## ⚒️ 기술 Stack + +| 카테고리 | 기술 스택 | 선정 이유 | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| **UI Library** | ![React](https://img.shields.io/badge/React-61DAFB?logo=react&logoColor=white&style=for-the-badge) | 컴포넌트 기반 구조로 재사용성과 유지보수성을 높이고, 생태계가 안정적인 SPA 환경을 구축하기 위해 선택 | +| **Language** | ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white&style=for-the-badge) | 정적 타입 기반 개발로 런타임 에러를 최소화하고, API 계약 안정성을 확보하기 위해 도입 | +| **Build Tool** | ![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white&style=for-the-badge) | 빠른 HMR과 빌드 속도를 통해 개발 생산성을 극대화하기 위해 선택 | +| **Styling** | ![TailwindCSS](https://img.shields.io/badge/Tailwind%20CSS-38B2AC?logo=tailwindcss&logoColor=white&style=for-the-badge) ![SVGR](https://img.shields.io/badge/SVGR-FFB13B?style=for-the-badge) | 유틸리티 기반 스타일링으로 일관된 디자인 시스템을 유지하고, SVG를 컴포넌트화하여 동적 제어를 가능하게 하기 위해 도입 | +| **Server State Management** | ![TanStack Query](https://img.shields.io/badge/TanStack%20Query-FF4154?logo=reactquery&logoColor=white&style=for-the-badge) | 서버 상태 캐싱, 동기화, 재요청 전략을 체계적으로 관리하여 네트워크 비용을 줄이고 UX를 개선하기 위해 사용 | +| **Global State Management** | ![Zustand](https://img.shields.io/badge/Zustand-443E38?style=for-the-badge) | 경량 전역 상태 관리로 불필요한 보일러플레이트 없이 인증/필터 상태 등을 관리하기 위해 선택 | +| **HTTP Client** | ![Axios](https://img.shields.io/badge/Axios-5A29E4?logo=axios&logoColor=white&style=for-the-badge) | 인터셉터 기반 인증 처리 및 공통 에러 핸들링 구조를 구현하기 위해 사용 | +| **Routing** | ![React Router](https://img.shields.io/badge/React%20Router-CA4245?logo=reactrouter&logoColor=white&style=for-the-badge) | SPA 환경에서 선언적 라우팅 및 레이아웃 분리를 효율적으로 관리하기 위해 도입 | +| **Package Manager** | ![pnpm](https://img.shields.io/badge/pnpm-F69220?logo=pnpm&logoColor=white&style=for-the-badge) | 디스크 공간 효율성과 의존성 충돌 방지를 위해 선택 | +| **Code Quality** | ![ESLint](https://img.shields.io/badge/ESLint-4B3263?logo=eslint&logoColor=white&style=for-the-badge) ![Prettier](https://img.shields.io/badge/Prettier-1A2C34?logo=prettier&logoColor=F7BA3E&style=for-the-badge) ![Husky](https://img.shields.io/badge/Husky-000000?logo=git&logoColor=white&style=for-the-badge) | 코드 스타일 일관성과 PR 품질 유지를 위해 자동화된 린트/포맷팅 환경을 구축 | +| **Error Monitoring** | ![Sentry](https://img.shields.io/badge/Sentry-362D59?logo=sentry&logoColor=white&style=for-the-badge) | 배포 후 발생하는 런타임 에러를 실시간 추적하고 안정적인 운영 환경을 구축하기 위해 도입 | +| **Deployment / CI** | ![Vercel](https://img.shields.io/badge/Vercel-000000?logo=vercel&logoColor=white&style=for-the-badge) ![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-2088FF?logo=githubactions&logoColor=white&style=for-the-badge) | 자동 빌드 및 배포 파이프라인을 구축하여 항상 배포 가능한 상태를 유지하기 위해 사용 | +| **Collaboration** | ![GitHub](https://img.shields.io/badge/GitHub-121011?logo=github&logoColor=white&style=for-the-badge) ![Notion](https://img.shields.io/badge/Notion-000000?logo=notion&logoColor=white&style=for-the-badge) ![Figma](https://img.shields.io/badge/Figma-F24E1E?logo=figma&logoColor=white&style=for-the-badge) ![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white&style=for-the-badge) | 디자인 협업, 문서화, 이슈 관리 및 실시간 커뮤니케이션을 위해 활용 | + +
+ +## 📁 폴더 구조 + +> 도메인 기준(`photoLab`, `photoFeed`, `photoManage`, `photoRestoration` 등)으로 기능을 분리하고, API / Query Hook / UI / Type / Store를 역할별로 분리해 유지보수성과 확장성을 확보했습니다. + +``` +└── 📁 src/ + ├── 🌐 apis/ // 도메인별 API 요청 모듈 (Barrel Pattern: index.ts) + │ ├── auth/ + │ ├── file/ + │ ├── mainPage/ + │ ├── member/ + │ ├── my/ + │ ├── photoFeed/ + │ ├── photoLab/ + │ ├── photoManage/ + │ └── photoRestoration/ + │ + ├── 🔁 hooks/ // 도메인별 Query Hook + 공통 훅 (캡슐화된 쿼리 훅 전략) + │ ├── auth/ + │ ├── common/ + │ ├── file/ + │ ├── mainPage/ + │ ├── member/ + │ ├── my/ + │ ├── photoFeed/ + │ ├── photoLab/ + │ ├── photoManage/ + │ └── photoRestoration/ + │ + ├── 🧩 components/ // UI 컴포넌트 (공통/도메인 단위 분리) + │ ├── common/ + │ ├── auth/ + │ ├── mainPage/ + │ ├── mypage/ + │ ├── photoFeed/ + │ ├── photoLab/ + │ ├── photoManage/ + │ └── photoRestoration/ + │ + ├── 📄 pages/ // 라우트 단위 페이지(진입점) + │ ├── auth/ + │ ├── developmentHistory/ + │ ├── filmCameraGuide/ + │ ├── mainPage/ + │ ├── mypage/ + │ ├── photoFeed/ + │ ├── photoLab/ + │ ├── photoManage/ + │ └── photoRestoration/ + │ + ├── 🧱 layouts/ // 레이아웃 컴포넌트 (Root/Footer/MyPage 등) + ├── 🧭 router/ // 라우팅 구성 (Router.tsx) + ├── 🗂 store/ // 전역 상태 관리 (Zustand) + │ + ├── 🧬 types/ // 도메인별 타입 정의 + ApiResponse + ├── 📚 constants/ // 상수/라벨/매핑 값 + ├── 🛠 utils/ // 순수 유틸 함수 + ├── ⚙️ lib/ // 전역 설정 (axios instance, interceptors 등) + ├── 🖼 assets/ // 폰트/아이콘/SVG/이미지 리소스 + │ + ├── 🚀 App.tsx // 앱 엔트리 UI + └── 🔥 main.tsx // 앱 엔트리 포인트 +``` + +
+ 📌 구조 설계 포인트 +
+ +- **도메인 기준 폴더링**: `photoLab`, `photoFeed`, `photoManage`, `photoRestoration` 등 서비스 기능 축을 기준으로 모듈을 분리했습니다. +- **API / Hook / UI / Type 역할 분리**: 한 도메인에서도 `apis/`, `hooks/`, `components/`, `types/`로 레이어를 분리해 변경 영향 범위를 줄였습니다. +- **Barrel Pattern**: `apis/*/index.ts`, `hooks/*/index.ts`를 활용해 외부에서는 단순한 import 경로로 사용하도록 구성했습니다. +- **Interceptors / Storage 단일화**: `lib/setUpInterceptors.ts`, `utils/tokenStorage.ts` 중심으로 인증/토큰 관리 로직을 집중시켜 중복과 취약점을 줄였습니다. + +
+ +
+ +## 🤖 기술 의사결정 (워크북 외 선택 기술) + +> 워크북 기반 학습을 넘어, 실제 서비스 수준의 안정성과 확장성을 확보하기 위해 +> 아래 기술 및 패턴을 추가 도입했습니다. +> 각 항목은 **선정 이유**와 **팀 합의 여부**를 명시합니다. + +## 1️⃣ 아키텍처 및 고도화 패턴 + +
+🔐 API 요청 동시성 제어 (Concurrency Control in Interceptors) +
+ +**선정 이유** +토큰 만료 시 여러 API 요청이 동시에 실패하면 리프레시 요청이 중복 발생하는 Race Condition 문제가 발생합니다. +이를 방지하기 위해 실패 요청을 `Pending Queue`에 저장하고, 토큰 갱신 후 대기 요청을 일괄 재시도하는 구조를 설계했습니다. + +**팀 합의** +인증 안정성은 서비스 신뢰성과 직결된다고 판단하여, 단순 재요청 방식이 아닌 **큐 기반 동시성 제어 로직을 프로젝트 표준으로 채택**했습니다. + +
+ +
+🧩 도메인 주도 서비스 계층 + Barrel Pattern +
+ +**선정 이유** +컴포넌트와 API 호출 로직의 강한 결합을 방지하기 위해 `auth`, `member`, `photoLab` 등 도메인 단위로 서비스 레이어를 분리했습니다. +또한 `index.ts` 기반 Barrel Pattern을 활용해 내부 구현을 은닉하고 명확한 인터페이스만 외부에 노출하도록 구성했습니다. + +**팀 합의** +백엔드 API 명세 변경 시 수정 범위를 최소화하고 유지보수성을 높이기 위해 해당 구조를 **프로젝트 기본 아키텍처로 채택**했습니다. + +
---- +
+📦 캡슐화된 쿼리 훅 (Encapsulated Query Hooks) +
-## ⚒️ 기술 스택 +**선정 이유** +`useQuery`를 컴포넌트에서 직접 사용할 경우 쿼리 키 중복, 캐싱 전략 불일치, 정책 분산 문제가 발생할 수 있습니다. +이를 방지하기 위해 도메인별 커스텀 훅으로 감싸 쿼리 키 및 캐싱 전략(staleTime 등)을 일원화했습니다. -| 구분 | 기술 | 버전 | -| --------------- | -------------- | ------- | -| Package Manager | pnpm | 10.26.2 | -| UI | React | 19.2.3 | -| Styling | Tailwind CSS | 4.1.18 | -| HTTP | Axios | 1.13.2 | -| Routing | React Router | 7.11.0 | -| Server State | TanStack Query | 5.90.12 | -| Global State | Zustand | 5.0.9 | +**팀 합의** +UI는 데이터 표현에 집중하고, 데이터 정책은 훅 계층에서 관리하는 방향으로 **관심사 분리 원칙을 팀 규칙으로 합의**했습니다. ---- +
-## 📐 네이밍 컨벤션 +
+🧬 제네릭 기반 API 응답 타입 설계 (ApiResponse<T>) +
-- **변수 / 함수**: camelCase - `fetchData()`, `newUser` -- **컴포넌트 / 클래스 / 타입**: PascalCase - `UserCard`, `UserData` -- **상수**: UPPER_SNAKE_CASE - `MAX_LIMIT` -- **파일명** - - 컴포넌트: `PascalCase.tsx` - - 훅: `camelCase.ts` - - 폴더: 소문자, 필요 시 `_` +**선정 이유** +모든 API 응답 구조를 `ApiResponse` 형태로 일반화하여 타입 추론을 강화하고, 런타임 에러 가능성을 최소화했습니다. ---- +**팀 합의** +타입 안정성과 개발 생산성 향상을 위해 **공통 응답 타입을 전 API에 일괄 적용**하기로 합의했습니다. + +
+ +
+🗄️ 스토리지 파사드 패턴 (Storage Facade Pattern) +
+ +**선정 이유** +토큰 접근 로직이 여러 파일에 분산될 경우 중복 코드 및 보안 취약점이 발생할 수 있습니다. +이를 방지하기 위해 `tokenStorage` 객체로 접근 로직을 단일화했습니다. + +**팀 합의** +추후 쿠키 기반 인증 등 저장소 정책 변경에 유연하게 대응할 수 있도록 **파사드 패턴 도입에 합의**했습니다. + +
+ +## 2️⃣ 개발 환경 및 인프라 + +
+⚡ Vite + PNPM +
+ +**선정 이유** +Vite는 빠른 HMR과 빌드 속도를 제공하며, PNPM은 디스크 공간 효율성과 의존성 충돌 방지 측면에서 장점이 있습니다. +개발 생산성과 프로젝트 확장성을 동시에 고려해 해당 조합을 선택했습니다. + +**팀 합의** +프로젝트 초기부터 최적화된 개발 환경을 팀 표준으로 확정했습니다. + +
+ +
+🤖 GitHub Actions + Husky +
+ +**선정 이유** +PR 단계에서 자동 린트 및 빌드 테스트를 수행하고, 커밋 단계에서 포맷 및 린트 오류를 사전에 차단하여 코드 품질을 유지하기 위함입니다. + +**팀 합의** +“항상 배포 가능한 상태”를 유지하기 위해 **자동화된 품질 게이트 운영에 합의**했습니다. + +
+ +
+🧭 Path Aliasing (@/) +
+ +**선정 이유** +상대 경로 중첩을 제거하여 가독성을 높이고, 파일 이동 시 import 경로가 깨지는 문제를 방지하기 위해 루트 기준 절대 경로를 도입했습니다. + +**팀 합의** +프로젝트 전반에서 절대 경로 사용을 **공식 규칙으로 표준화**했습니다. + +
+ +## 3️⃣ UI/UX 특화 라이브러리 + +
+🎨 SVGR +
+ +**선정 이유** +SVG를 React 컴포넌트로 변환하여 Props 및 Tailwind 기반 동적 스타일링이 가능하도록 구성했습니다. + +**팀 합의** +아이콘 자산의 재사용성과 유지보수성을 높이기 위해 도입에 합의했습니다. + +
+ +
+🧱 React Masonry CSS +
+ +**선정 이유** +사진 커뮤니티 피드에서 다양한 비율의 이미지를 빈틈 없이 배치하기 위해 Masonry 레이아웃을 적용했습니다. + +**팀 합의** +커뮤니티 UX 완성도 향상을 위해 전담 레이아웃 라이브러리 사용에 합의했습니다. + +
+ +
+🏠 React Daum Postcode +
+ +**선정 이유** +국내 사용자에게 익숙한 주소 검색 UX를 제공하고, 도로명 주소 입력 정확도를 높이기 위해 도입했습니다. + +**팀 합의** +실사용자 경험 개선을 위해 도입에 합의했습니다. + +
+ +
## 🌿 브랜치 전략 @@ -93,7 +299,7 @@ AI 사진 복원, 커뮤니티 피드, 필름 카메라 가이드 등 - `develop` : 개발 - 모든 작업 브랜치는 **develop 기준 생성** ---- +
## 💬 Commit 메시지 @@ -103,22 +309,16 @@ type: 메시지 (#이슈번호) - feat / fix / refactor / style / chore / docs ---- +
## 🔀 Pull Request -- Base: `develop` -- 제목: `[FEAT] 회원가입 API 연동` -- Issue 연결: - -```text -Closes #10 -``` - +- Base: develop +- 제목: [FEAT] 회원가입 API 연동 +- Issue 연결: Closes #10 - 24시간 내 리뷰 필수 +
---- - -

- UMC 9th · Finders · Web +

+UMC 9th · Finders · Web

diff --git a/eslint.config.js b/eslint.config.js index d32031c6..1a1b97e2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,12 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, - rules: {}, + rules: { + "react-hooks/set-state-in-effect": "off", + + "react-hooks/refs": "off", + + "react-hooks/exhaustive-deps": "off", + }, }, ]); diff --git a/src/assets/filmNews/film-news-section-1.png b/src/assets/filmNews/film-news-section-1.png deleted file mode 100644 index be51d66a..00000000 Binary files a/src/assets/filmNews/film-news-section-1.png and /dev/null differ diff --git a/src/assets/filmNews/film-news-section-2.png b/src/assets/filmNews/film-news-section-2.png deleted file mode 100644 index 1f8de6fa..00000000 Binary files a/src/assets/filmNews/film-news-section-2.png and /dev/null differ diff --git a/src/assets/filmNews/film-news-section-3.png b/src/assets/filmNews/film-news-section-3.png deleted file mode 100644 index 35b2d265..00000000 Binary files a/src/assets/filmNews/film-news-section-3.png and /dev/null differ diff --git a/src/assets/icon/Apple.svg b/src/assets/icon/apple.svg similarity index 100% rename from src/assets/icon/Apple.svg rename to src/assets/icon/apple.svg diff --git a/src/assets/icon/bell-fill.svg b/src/assets/icon/bell-fill.svg deleted file mode 100644 index a58bda49..00000000 --- a/src/assets/icon/bell-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/bigLogo.svg b/src/assets/icon/big-logo.svg similarity index 100% rename from src/assets/icon/bigLogo.svg rename to src/assets/icon/big-logo.svg diff --git a/src/assets/icon/bookmark-empty.svg b/src/assets/icon/bookmark-empty.svg deleted file mode 100644 index 595d4fcc..00000000 --- a/src/assets/icon/bookmark-empty.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/icon/bookmark.svg b/src/assets/icon/bookmark.svg deleted file mode 100644 index e5df8582..00000000 --- a/src/assets/icon/bookmark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/bruned.svg b/src/assets/icon/bruned.svg deleted file mode 100644 index b0d8bd28..00000000 --- a/src/assets/icon/bruned.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/camera-fill.svg b/src/assets/icon/camera-fill.svg deleted file mode 100644 index 6798f01b..00000000 --- a/src/assets/icon/camera-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/caret-down-fill.svg b/src/assets/icon/caret-down-fill.svg deleted file mode 100644 index d44d5f54..00000000 --- a/src/assets/icon/caret-down-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/chat-bubble-double-ellipsis.svg b/src/assets/icon/chat-bubble-double-ellipsis.svg deleted file mode 100644 index c7e3ccd8..00000000 --- a/src/assets/icon/chat-bubble-double-ellipsis.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/icon/chat-bubble-double-fill.svg b/src/assets/icon/chat-bubble-double-fill.svg deleted file mode 100644 index 3a52f78a..00000000 --- a/src/assets/icon/chat-bubble-double-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/chat-square.svg b/src/assets/icon/chat-square.svg deleted file mode 100644 index ade94e58..00000000 --- a/src/assets/icon/chat-square.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/check.svg b/src/assets/icon/check.svg deleted file mode 100644 index 309339b9..00000000 --- a/src/assets/icon/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/confirmation.svg b/src/assets/icon/confirmation.svg deleted file mode 100644 index fe534b8f..00000000 --- a/src/assets/icon/confirmation.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/icon/defaultProfile.svg b/src/assets/icon/default-profile.svg similarity index 100% rename from src/assets/icon/defaultProfile.svg rename to src/assets/icon/default-profile.svg diff --git a/src/assets/icon/delivery.svg b/src/assets/icon/delivery.svg deleted file mode 100644 index d02dc255..00000000 --- a/src/assets/icon/delivery.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/icon/develop.svg b/src/assets/icon/develop.svg deleted file mode 100644 index f966a3d6..00000000 --- a/src/assets/icon/develop.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/icon/flim-image.svg b/src/assets/icon/flim-image.svg deleted file mode 100644 index d3485f38..00000000 --- a/src/assets/icon/flim-image.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/headset-fill.svg b/src/assets/icon/headset-fill.svg deleted file mode 100644 index 8af143eb..00000000 --- a/src/assets/icon/headset-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/headset.svg b/src/assets/icon/headset.svg deleted file mode 100644 index f8dc6abb..00000000 --- a/src/assets/icon/headset.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/icon/index.ts b/src/assets/icon/index.ts index eb064755..d063b13f 100644 --- a/src/assets/icon/index.ts +++ b/src/assets/icon/index.ts @@ -4,28 +4,23 @@ export { default as ArrowLeftIcon } from "./arrow-left.svg?react"; export { default as ArrowTurnUpLeftIcon } from "./arrow-turn-up-left.svg?react"; export { default as ArrowTurnUpRightIcon } from "./arrow-turn-up-right.svg?react"; export { default as BellIcon } from "./bell.svg?react"; -export { default as BookMarkEmptyIcon } from "./bookmark-empty.svg?react"; export { default as BriefcaseIcon } from "./briefcase.svg?react"; export { default as CalendarIcon } from "./calendar.svg?react"; export { default as ChatBubbleEmptyIcon } from "./chat-bubble-empty.svg?react"; export { default as ChatBubbleIcon } from "./chat-bubble.svg?react"; export { default as ChatIcon } from "./tab-chat-bubble.svg?react"; -export { default as ChatSquareIcon } from "./chat-square.svg?react"; -export { default as CheckIcon } from "./check.svg?react"; export { default as CheckEmptyIcon } from "./check-empty.svg?react"; export { default as ChevronLeftIcon } from "./chevron-left.svg?react"; export { default as ClockIcon } from "./clock.svg?react"; export { default as CloseIcon } from "./close.svg?react"; export { default as CopyIcon } from "./copy.svg?react"; export { default as CopyFillIcon } from "./copy-filled.svg?react"; -export { default as CustomPinIcon } from "./custom-pin.svg?react"; -export { default as DefaultProfileIcon } from "./defaultProfile.svg?react"; +export { default as DefaultProfileIcon } from "./default-profile.svg?react"; export { default as DownloadIcon } from "./download.svg?react"; export { default as EmptyBoxIcon } from "./empty-box.svg?react"; export { default as EmptyCheckCircleIcon } from "./empty-check-circle.svg?react"; export { default as EmptyCircleIcon } from "./empty-circle.svg?react"; export { default as ExclamationCircleIcon } from "./exclamation-circle.svg?react"; -export { default as HeadsetIcon } from "./headset.svg?react"; export { default as HeartIcon } from "./heart.svg?react"; export { default as HomeIcon } from "./home.svg?react"; export { default as LogoIcon } from "./logo.svg?react"; @@ -45,7 +40,6 @@ export { default as SecurityIcon } from "./security.svg?react"; export { default as StarIcon } from "./star.svg?react"; export { default as TabHomeIcon } from "./tab-home.svg?react"; export { default as TicketIcon } from "./ticket.svg?react"; -export { default as ToDoListIcon } from "./to-do-list.svg?react"; export { default as UploadIcon } from "./upload.svg?react"; export { default as XMarkIcon } from "./x-mark.svg?react"; @@ -58,51 +52,31 @@ export { default as RestoraionSparkleIcon } from "./restoration-sparkle.svg?reac export { default as TooltipXIcon } from "./tooltip-x.svg?react"; // 소셜 / 브랜드 -export { default as AppleIcon } from "./Apple.svg?react"; -export { default as KakaoIcon } from "./Kakao.svg?react"; - -// 배너 사진 -export { default as DevelopPicIcon } from "./develop.svg?react"; -export { default as ScanPicIcon } from "./scanPic.svg?react"; -export { default as PrintPicIcon } from "./print.svg?react"; -export { default as DeliveryPicIcon } from "./delivery.svg?react"; -export { default as PromotionBannerIcon1 } from "./promotion-banner-1.svg?react"; -export { default as PromotionBannerIcon2 } from "./promotion-banner-2.svg?react"; -export { default as PromotionBannerIcon3 } from "./promotion-banner-3.svg?react"; +export { default as AppleIcon } from "./apple.svg?react"; +export { default as KakaoIcon } from "./kakao.svg?react"; // Fill 아이콘 -export { default as BellFillIcon } from "./bell-fill.svg?react"; -export { default as BookmarkIcon } from "./bookmark.svg?react"; export { default as BriefcaseFillIcon } from "./briefcase-fill.svg?react"; -export { default as BrunedIcon } from "./bruned.svg?react"; export { default as CalendarFillIcon } from "./calendar-fill.svg?react"; -export { default as CameraFillIcon } from "./camera-fill.svg?react"; -export { default as CaretDownFillIcon } from "./caret-down-fill.svg?react"; -export { default as ChatBubbleDoubleEllipsisIcon } from "./chat-bubble-double-ellipsis.svg?react"; -export { default as ChatBubbleDoubleFillIcon } from "./chat-bubble-double-fill.svg?react"; export { default as ChatFillIcon } from "./tab-chat-bubble-fill.svg?react"; export { default as CheckCircleIcon } from "./check-circle.svg?react"; export { default as CheckBoxIcon } from "./check-box-fill.svg?react"; export { default as CircleIcon } from "./circle.svg?react"; export { default as CircleFillIcon } from "./circle-fill.svg?react"; export { default as CircleGlareFillIcon } from "./circle-glare-fill.svg?react"; -export { default as BigLogoIcon } from "./bigLogo.svg?react"; +export { default as BigLogoIcon } from "./big-logo.svg?react"; export { default as DeleteIcon } from "./delete.svg?react"; export { default as EllipsisVerticalIcon } from "./ellipsis-vertical.svg?react"; export { default as FlimIcon } from "./flim.svg?react"; -export { default as FlimImageIcon } from "./flim-image.svg?react"; export { default as FloatingIcon } from "./floating.svg?react"; -export { default as HeadsetFillIcon } from "./headset-fill.svg?react"; export { default as HeartFillIcon } from "./heart-fill.svg?react"; export { default as KakaoFillIcon } from "./kakao-fill.svg?react"; export { default as ManageFillIcon } from "./tab-camera-fill.svg?react"; -export { default as MapPinFillIcon } from "./map-pin-fill.svg?react"; export { default as MyPageFillIcon } from "./mypage-fill.svg?react"; export { default as PackageIcon } from "./package.svg?react"; export { default as PaperAirplaneFillIcon } from "./paper-airplane-fill.svg?react"; export { default as PencilLineFillIcon } from "./pencil-line-fill.svg?react"; export { default as PhotoFillIcon } from "./photo-fill.svg?react"; -export { default as PhotoIcon } from "./photo.svg?react"; export { default as PhotoLabFillIcon } from "./photolab-fill.svg?react"; export { default as ShareIcon } from "./share.svg?react"; export { default as ShoeIcon } from "./shoe.svg?react"; @@ -110,12 +84,9 @@ export { default as SparklesFillIcon } from "./sparkles-fill.svg?react"; export { default as SplashIcon } from "./splash.svg?react"; export { default as StarFillIcon } from "./star-fill.svg?react"; export { default as TabHomeFillIcon } from "./tab-home-fill.svg?react"; -export { default as TicketFillIcon } from "./ticket-fill.svg?react"; -export { default as TruckFillIcon } from "./truck-fill.svg?react"; export { default as TruckIcon } from "./truck.svg?react"; export { default as MainCamera } from "./main-camera.svg?react"; export { default as MainFilm } from "./main-film.svg?react"; -export { default as MainSparkle } from "./main-sparkle.svg?react"; export { default as PaintBrushIcon } from "./paint-brush.svg?react"; export { default as NoticeLocationIcon } from "./notice-location.svg?react"; export { default as NoticeTimeIcon } from "./notice-time.svg?react"; diff --git a/src/assets/icon/Kakao.svg b/src/assets/icon/kakao.svg similarity index 100% rename from src/assets/icon/Kakao.svg rename to src/assets/icon/kakao.svg diff --git a/src/assets/icon/main-sparkle.svg b/src/assets/icon/main-sparkle.svg deleted file mode 100644 index 22fde5ee..00000000 --- a/src/assets/icon/main-sparkle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/icon/map-pin-fill.svg b/src/assets/icon/map-pin-fill.svg deleted file mode 100644 index 6d42579a..00000000 --- a/src/assets/icon/map-pin-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/photo.svg b/src/assets/icon/photo.svg deleted file mode 100644 index e8ac5acd..00000000 --- a/src/assets/icon/photo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/icon/print.svg b/src/assets/icon/print.svg deleted file mode 100644 index 4ebfb7df..00000000 --- a/src/assets/icon/print.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/icon/promotion-banner-1.svg b/src/assets/icon/promotion-banner-1.svg deleted file mode 100644 index c1d7211b..00000000 --- a/src/assets/icon/promotion-banner-1.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/assets/icon/promotion-banner-2.svg b/src/assets/icon/promotion-banner-2.svg deleted file mode 100644 index 64399182..00000000 --- a/src/assets/icon/promotion-banner-2.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/assets/icon/promotion-banner-3.svg b/src/assets/icon/promotion-banner-3.svg deleted file mode 100644 index 7eaa1850..00000000 --- a/src/assets/icon/promotion-banner-3.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/assets/icon/scanPic.svg b/src/assets/icon/scanPic.svg deleted file mode 100644 index da61205e..00000000 --- a/src/assets/icon/scanPic.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/icon/ticket-fill.svg b/src/assets/icon/ticket-fill.svg deleted file mode 100644 index 0472551a..00000000 --- a/src/assets/icon/ticket-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/to-do-list.svg b/src/assets/icon/to-do-list.svg deleted file mode 100644 index cb4f5b21..00000000 --- a/src/assets/icon/to-do-list.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icon/truck-fill.svg b/src/assets/icon/truck-fill.svg deleted file mode 100644 index bf1b9dae..00000000 --- a/src/assets/icon/truck-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/images/delivery.webp b/src/assets/images/delivery.webp new file mode 100644 index 00000000..a9d263cc Binary files /dev/null and b/src/assets/images/delivery.webp differ diff --git a/src/assets/images/develop.webp b/src/assets/images/develop.webp new file mode 100644 index 00000000..2f1dced8 Binary files /dev/null and b/src/assets/images/develop.webp differ diff --git a/src/assets/images/film-news-section-1.webp b/src/assets/images/film-news-section-1.webp new file mode 100644 index 00000000..975b8944 Binary files /dev/null and b/src/assets/images/film-news-section-1.webp differ diff --git a/src/assets/images/film-news-section-2.webp b/src/assets/images/film-news-section-2.webp new file mode 100644 index 00000000..cddb0144 Binary files /dev/null and b/src/assets/images/film-news-section-2.webp differ diff --git a/src/assets/images/film-news-section-3.webp b/src/assets/images/film-news-section-3.webp new file mode 100644 index 00000000..61af07f7 Binary files /dev/null and b/src/assets/images/film-news-section-3.webp differ diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts new file mode 100644 index 00000000..287fc135 --- /dev/null +++ b/src/assets/images/index.ts @@ -0,0 +1,10 @@ +export { default as developPic } from "./develop.webp"; +export { default as scanPic } from "./scan-pic.webp"; +export { default as printPic } from "./print.webp"; +export { default as deliveryPic } from "./delivery.webp"; +export { default as promotionBanner1 } from "./promotion-banner-1.webp"; +export { default as promotionBanner2 } from "./promotion-banner-2.webp"; +export { default as promotionBanner3 } from "./promotion-banner-3.webp"; +export { default as filmNewsThumb1 } from "./film-news-section-1.webp"; +export { default as filmNewsThumb2 } from "./film-news-section-2.webp"; +export { default as filmNewsThumb3 } from "./film-news-section-3.webp"; diff --git a/src/assets/images/print.webp b/src/assets/images/print.webp new file mode 100644 index 00000000..539bf5f8 Binary files /dev/null and b/src/assets/images/print.webp differ diff --git a/src/assets/images/promotion-banner-1.webp b/src/assets/images/promotion-banner-1.webp new file mode 100644 index 00000000..73235de0 Binary files /dev/null and b/src/assets/images/promotion-banner-1.webp differ diff --git a/src/assets/images/promotion-banner-2.webp b/src/assets/images/promotion-banner-2.webp new file mode 100644 index 00000000..f9fba9c9 Binary files /dev/null and b/src/assets/images/promotion-banner-2.webp differ diff --git a/src/assets/images/promotion-banner-3.webp b/src/assets/images/promotion-banner-3.webp new file mode 100644 index 00000000..add89b84 Binary files /dev/null and b/src/assets/images/promotion-banner-3.webp differ diff --git a/src/assets/images/scan-pic.webp b/src/assets/images/scan-pic.webp new file mode 100644 index 00000000..2d8e38e5 Binary files /dev/null and b/src/assets/images/scan-pic.webp differ diff --git a/src/assets/mocks/PLmock.png b/src/assets/mocks/PLmock.png deleted file mode 100644 index 6551813f..00000000 Binary files a/src/assets/mocks/PLmock.png and /dev/null differ diff --git a/src/assets/mocks/index.ts b/src/assets/mocks/index.ts deleted file mode 100644 index 3272e9b6..00000000 --- a/src/assets/mocks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as MainBannerAiIcon } from "./mock-main-banner-ai.svg?react"; diff --git a/src/assets/mocks/mock-main-banner-ai.svg b/src/assets/mocks/mock-main-banner-ai.svg deleted file mode 100644 index 8780684f..00000000 --- a/src/assets/mocks/mock-main-banner-ai.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/mocks/mock1.jpg b/src/assets/mocks/mock1.jpg deleted file mode 100644 index aa08448a..00000000 Binary files a/src/assets/mocks/mock1.jpg and /dev/null differ diff --git a/src/assets/mocks/mock2.jpg b/src/assets/mocks/mock2.jpg deleted file mode 100644 index 91084db0..00000000 Binary files a/src/assets/mocks/mock2.jpg and /dev/null differ diff --git a/src/assets/mocks/mock3.jpg b/src/assets/mocks/mock3.jpg deleted file mode 100644 index fcfe6e29..00000000 Binary files a/src/assets/mocks/mock3.jpg and /dev/null differ diff --git a/src/assets/mocks/mock4.jpg b/src/assets/mocks/mock4.jpg deleted file mode 100644 index 7bab2fd1..00000000 Binary files a/src/assets/mocks/mock4.jpg and /dev/null differ diff --git a/src/assets/mocks/mock5.jpg b/src/assets/mocks/mock5.jpg deleted file mode 100644 index 17af8aa6..00000000 Binary files a/src/assets/mocks/mock5.jpg and /dev/null differ diff --git a/src/assets/mocks/mock6.jpg b/src/assets/mocks/mock6.jpg deleted file mode 100644 index 02fa5519..00000000 Binary files a/src/assets/mocks/mock6.jpg and /dev/null differ diff --git a/src/assets/mocks/mock7.jpg b/src/assets/mocks/mock7.jpg deleted file mode 100644 index 2be411ae..00000000 Binary files a/src/assets/mocks/mock7.jpg and /dev/null differ diff --git a/src/assets/mocks/mock8.jpg b/src/assets/mocks/mock8.jpg deleted file mode 100644 index d0ef98ed..00000000 Binary files a/src/assets/mocks/mock8.jpg and /dev/null differ diff --git a/src/components/auth/AppleButton.tsx b/src/components/auth/AppleButton.tsx index 012a81cf..4cce9849 100644 --- a/src/components/auth/AppleButton.tsx +++ b/src/components/auth/AppleButton.tsx @@ -1,4 +1,4 @@ -import AppleLogo from "../../assets/icon/Apple.svg"; +import { AppleIcon } from "@/assets/icon"; interface AppleButtonProps { onClick?: () => void; @@ -10,7 +10,7 @@ export const AppleButton = ({ onClick }: AppleButtonProps) => { className="bg-neutral-875 inline-flex h-[3.125rem] w-full items-center justify-center gap-2 rounded-2xl font-semibold text-white shadow-sm active:scale-[0.99]" onClick={onClick} > - Apple Logo + 애플로 로그인 ); diff --git a/src/components/common/BottomSheet.tsx b/src/components/common/BottomSheet.tsx index 7c42ba21..62ff2421 100644 --- a/src/components/common/BottomSheet.tsx +++ b/src/components/common/BottomSheet.tsx @@ -18,6 +18,7 @@ type BottomSheetProps = { isBackDrop?: boolean; /** true면 TabBar 위에 표시 (z-60/70), false면 TabBar 아래 (z-40) */ overlay?: boolean; + fixedMin?: boolean; // true면 처음 높이가 최소 높이(더 내려가지 않음) }; // clamp 유틸 @@ -37,6 +38,7 @@ export default function BottomSheet({ sheetClassName, isBackDrop = true, overlay = false, + fixedMin = false, }: BottomSheetProps) { const sheetRef = useRef(null); @@ -53,10 +55,11 @@ export default function BottomSheet({ return { expandedH: exp, collapsedH: col }; }, [vh, collapsedRatio, expandedVh]); - // 현재 시트 높이(px) (드래그로 변함) - const [sheetH, setSheetH] = useState(() => - initialSnap === "expanded" ? expandedH : collapsedH, - ); + const initialH = useMemo(() => { + return initialSnap === "expanded" ? expandedH : collapsedH; + }, [initialSnap, expandedH, collapsedH]); + + const [sheetH, setSheetH] = useState(() => initialH); // open이 false → true로 전환될 때 높이를 초기값으로 리셋 const [prevOpen, setPrevOpen] = useState(open); @@ -64,7 +67,7 @@ export default function BottomSheet({ setPrevOpen(open); if (open) { setSnap(initialSnap); - setSheetH(initialSnap === "expanded" ? expandedH : collapsedH); + setSheetH(initialH); } } @@ -73,7 +76,8 @@ export default function BottomSheet({ const [prevSnapTargetH, setPrevSnapTargetH] = useState(snapTargetH); if (open && !dragging && snapTargetH !== prevSnapTargetH) { setPrevSnapTargetH(snapTargetH); - setSheetH(snapTargetH); + const minH = fixedMin ? initialH : 0; + setSheetH(clamp(snapTargetH, minH, expandedH)); } // open 상태에서 뷰포트/키보드 등으로 높이가 바뀌면 현재 snap 기준으로 높이 보정 @@ -86,7 +90,14 @@ export default function BottomSheet({ const nextExpandedH = (nextVh * expandedVh) / 100; const nextCollapsedH = nextVh * collapsedRatio; - setSheetH(snap === "expanded" ? nextExpandedH : nextCollapsedH); + const nextInitialH = + initialSnap === "expanded" ? nextExpandedH : nextCollapsedH; + + const target = snap === "expanded" ? nextExpandedH : nextCollapsedH; + + // fixedMin이면 최소 높이를 nextInitialH로 유지 + const minH = fixedMin ? nextInitialH : 0; + setSheetH(clamp(target, minH, nextExpandedH)); const isTyping = document.activeElement instanceof HTMLInputElement || @@ -107,7 +118,7 @@ export default function BottomSheet({ window.visualViewport?.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize); }; - }, [open, snap, collapsedRatio, expandedVh]); + }, [open, snap, collapsedRatio, expandedVh, initialSnap, fixedMin]); // body 스크롤 잠금(인스타 느낌) useEffect(() => { @@ -139,7 +150,8 @@ export default function BottomSheet({ const { startClientY, startH } = pointerState.current; const delta = e.clientY - startClientY; // 아래로 +, 위로 - // 아래로 끌면 높이 줄고, 위로 끌면 높이 늘어남 - const nextH = clamp(startH - delta, 0, expandedH); + const minH = fixedMin ? initialH : 0; + const nextH = clamp(startH - delta, minH, expandedH); setSheetH(nextH); }; @@ -150,6 +162,14 @@ export default function BottomSheet({ setDragging(false); // 스냅 판정 + // 0) fixedMin이면 닫기 판정 자체를 하지 않음(내려갈 수 없으니까) + if (!fixedMin) { + if (sheetH <= vh * CLOSE_THRESHOLD_RATIO) { + onClose(); + return; + } + } + // 1) 너무 아래로 끌면 닫기 if (sheetH <= vh * CLOSE_THRESHOLD_RATIO) { onClose(); @@ -157,10 +177,12 @@ export default function BottomSheet({ } // 2) expanded/collapsed 중 가까운 쪽으로 - const midH = (expandedH + collapsedH) / 2; + const baseCollapsedH = fixedMin ? initialH : collapsedH; + + const midH = (expandedH + baseCollapsedH) / 2; const nextSnap: Snap = sheetH >= midH ? "expanded" : "collapsed"; setSnap(nextSnap); - setSheetH(nextSnap === "expanded" ? expandedH : collapsedH); + setSheetH(nextSnap === "expanded" ? expandedH : baseCollapsedH); }; if (!open) return null; diff --git a/src/components/common/CTA_Button.tsx b/src/components/common/CTA_Button.tsx index 4d5194c0..ac09107a 100644 --- a/src/components/common/CTA_Button.tsx +++ b/src/components/common/CTA_Button.tsx @@ -36,7 +36,7 @@ export const CTA_Button = ({ }: CTA_ButtonProps) => { const router = useNavigate(); const baseClass = - "inline-flex items-center justify-center rounded-2xl border shadow-sm active:scale-[0.99]"; + "inline-flex items-center justify-center rounded-2xl border shadow-sm active:scale-[0.99] gap-2"; const sizeClass: Record = { xsmall: "h-[2.875rem] w-[7.5625rem] text-[0.875rem]", small: "h-[3.5rem] w-[7.5625rem]", diff --git a/src/components/common/TextArea.tsx b/src/components/common/TextArea.tsx index 666e5f50..b0a168c1 100644 --- a/src/components/common/TextArea.tsx +++ b/src/components/common/TextArea.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, forwardRef } from "react"; type TextAreaType = "title" | "content"; @@ -27,75 +27,99 @@ type TextAreaProps = { className?: string; textareaClassName?: string; disabled?: boolean; + + isError?: boolean; }; -export function TextArea({ - type, - value, - onChange, - placeholder = "", - maxLength, - minLength, - emptyHint = "min", - className = "", - textareaClassName = "", - disabled = false, -}: TextAreaProps) { - const textareaRef = useRef(null); +export const TextArea = forwardRef( + ( + { + type, + value, + onChange, + placeholder = "", + maxLength, + minLength, + emptyHint = "min", + className = "", + textareaClassName = "", + disabled = false, + isError = false, + }, + forwardedRef, + ) => { + const innerRef = useRef(null); + + // forwardedRef + innerRef 같이 연결 + const setRefs = (el: HTMLTextAreaElement | null) => { + innerRef.current = el; + if (!forwardedRef) return; + if (typeof forwardedRef === "function") forwardedRef(el); + else forwardedRef.current = el; + }; - const length = value.length; - const hasTyped = length > 0; // 한 글자라도 입력했는가 - const isOverMax = typeof maxLength === "number" && length > maxLength; + const length = value.length; + const hasTyped = length > 0; // 한 글자라도 입력했는가 + const isOverMax = typeof maxLength === "number" && length > maxLength; - const heightStyle = - type && HEIGHT_BY_TYPE[type] - ? { - minHeight: HEIGHT_BY_TYPE[type].min, - maxHeight: HEIGHT_BY_TYPE[type].max, - } - : { - minHeight: "5.5rem", - maxHeight: "11.4375rem", - }; + const heightStyle = + type && HEIGHT_BY_TYPE[type] + ? { + minHeight: HEIGHT_BY_TYPE[type].min, + maxHeight: HEIGHT_BY_TYPE[type].max, + } + : { + minHeight: "5.5rem", + maxHeight: "11.4375rem", + }; - // 내용에 따라 높이 자동 조절 - useEffect(() => { - const el = textareaRef.current; - if (!el) return; + // 내용에 따라 높이 자동 조절 + useEffect(() => { + const el = innerRef.current; + if (!el) return; - el.style.height = "auto"; // 초기화 - el.style.height = `${el.scrollHeight}px`; // 내용만큼 증가 - }, [value]); + el.style.height = "auto"; // 초기화 + el.style.height = `${el.scrollHeight}px`; // 내용만큼 증가 + }, [value]); - return ( -
-