diff --git a/.github/ISSUE_TEMPLATE/bug-issue-template.md b/.github/ISSUE_TEMPLATE/bug-issue-template.md new file mode 100644 index 0000000..770353a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-issue-template.md @@ -0,0 +1,20 @@ +--- +name: Bug Issue Template +about: 버그 관련 이슈 템플릿 +title: '' +labels: '' +assignees: '' + +--- + +## 에러 설명🚦 + + +## 환경⚙️ + + +## 재현 방법🧿 + + +## 에러 화면📸 + diff --git a/.github/ISSUE_TEMPLATE/feature-issue-template.md b/.github/ISSUE_TEMPLATE/feature-issue-template.md new file mode 100644 index 0000000..8800082 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-issue-template.md @@ -0,0 +1,17 @@ +--- +name: Feature Issue Template +about: 기능 개발 시에 사용하는 이슈 템플릿 +title: 'feat:' +labels: '' +assignees: '' + +--- + +## 이슈 설명☀️ + + +## TO-DO📒 +- [ ] 할 일 1 + +## 기타🍀 + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..79b7af5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## 1️⃣ 작업 내용 Summary📱💎 + +- resolved #(issue_num) + +### 기존 코드에 영향을 미치지 않는 변경사항 + +### 기존 코드에 영향을 미치는 변경사항 + +### ✚ 작업 내용 스크린 샷📸 + +## 2️⃣ 리뷰어에게 공유할 내용👥 + +## 3️⃣ 추후 작업할 내용👋 + +- [ ] `main` 브랜치의 최신 코드를 `pull` 받았나요? diff --git a/.github/readme-assets/ai-flow.png b/.github/readme-assets/ai-flow.png new file mode 100644 index 0000000..4a7cc6e Binary files /dev/null and b/.github/readme-assets/ai-flow.png differ diff --git a/.github/readme-assets/architecture.png b/.github/readme-assets/architecture.png new file mode 100644 index 0000000..1da7077 Binary files /dev/null and b/.github/readme-assets/architecture.png differ diff --git a/.github/readme-assets/cicd.png b/.github/readme-assets/cicd.png new file mode 100644 index 0000000..6d3ed26 Binary files /dev/null and b/.github/readme-assets/cicd.png differ diff --git a/.github/readme-assets/erd.png b/.github/readme-assets/erd.png new file mode 100644 index 0000000..f8e626c Binary files /dev/null and b/.github/readme-assets/erd.png differ diff --git a/.github/readme-assets/server-test.png b/.github/readme-assets/server-test.png new file mode 100644 index 0000000..e6934c5 Binary files /dev/null and b/.github/readme-assets/server-test.png differ diff --git a/.github/readme-assets/test-coverage.png b/.github/readme-assets/test-coverage.png new file mode 100644 index 0000000..0e5218f Binary files /dev/null and b/.github/readme-assets/test-coverage.png differ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3081a05 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,30 @@ +name: git push into another repo to deploy to vercel + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + container: pandoc/latex + steps: + - uses: actions/checkout@v2 + - name: Install mustache (to update the date) + run: apk add ruby && gem install mustache + - name: creates output + run: sh ./build.sh + - name: Pushes to another repository + id: push_directory + uses: cpina/github-action-push-to-another-repository@main + env: + API_TOKEN_GITHUB: ${{ secrets.ACTIONS_TOKEN }} + with: + source-directory: 'output' + destination-github-username: dvp-tae + destination-repository-name: claco-client + user-email: ${{ secrets.EMAIL }} + commit-message: ${{ github.event.commits[0].message }} + target-branch: main + - name: Test get variable exported by push-to-another-repository + run: echo $DESTINATION_CLONED_DIRECTORY diff --git a/README.md b/README.md index 74872fd..575e634 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,211 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) -``` - -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from 'eslint-plugin-react' - -export default tseslint.config({ - // Set the react version - settings: { react: { version: '18.3' } }, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - }, -}) -``` +# 사용자 맞춤 클래식 공연 큐레이션 서비스 Claco + +## 📱 서비스 소개 +- 클래식 공연 감상의 길잡이가 되는 서비스를 제공함으로써 클래식 공연 문화를 더 즐겁게 향유할 수 있도록 합니다. +- 사용자의 취향에 맞는 공연을 추천해주고, 티켓을 만들어 추억을 간직하고, 공유합니다. + +### 📆 개발 기간 및 인원 +- ***2024.10.05 ~ 2024.11.24*** +- ```Frontend```: 2명 +- ```Backend```: 2명 + +## 🧑‍💻 R&R +| Profile | Name | Role | +|:---------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------:|:------------------------------------------------------------------------------------------------:| +| | 김진희
**kimzini**
FE | 소셜 로그인, 온보딩, 공연 상세, 마이 페이지,
티켓 등록 서비스 GUI 개발 및 API 연동 +| | 성태현
**dvp-tae**
FE | 서비스 메인, 리뷰, 둘러보기,
클라코북 만들기 서비스 GUI 개발 및 API 연동 +| | \<개발 리드>
이건
**devkeon**
BE | 아키텍처 설계, ERD 설계, 메인 서버 인프라 및 CI/CD 구축,
인증/인가, 모니터링 시스템 구축, 티켓/리뷰 기능,
클라코북 기능, 회원 관련 기능 | +| | 정희찬
**anselmo**
BE | ERD 설계, AI 및 배치 서버 인프라 및 CI/CD 구축,
추천 AI 모델 구현, 배치 기능(데이터 로드) 구축,
공연 기능, 공연 및 티켓 추천 기능 | + + +## 🏛️ 아키텍처 +![architecture.png](./.github/readme-assets/architecture.png) + +### PWA +- 개발 및 유지보수 비용 절감 +- 사용자가 홈 화면에 추가하여 앱처럼 활용 가능 +- 네이티브 앱과 유사한 사용성과 빠른 로딩 속도 제공 +- 플랫폼 제약 없이 다양한 환경에서 동일한 사용자 경험 제공 +- 추후 웹 푸시 알림, 백그라운드 동작 등 기능 확장이 용이 + +### 보안 고려 사항 +- JWT를 활용한 인증/인가 + - SSL 보안 계층을 활용한 토큰 암호화 (HTTPS, ALB 설치) + - CSRF / XSS 공격에 대비한 토큰 저장 분리 (Local storage, HTTP-only Cookie) +- Nginx를 활용한 actuator와 같은 민감 정보 deny +- Spring Security를 활용한 철저한 Auth 검사 및 uri 접근 조정 +- Kakao OAuth2.0을 활용한 인증/인가 기능 간편화 +- docker 네트워크를 활용하여 spring 서버나, prometheus같은 인스턴스 포트 매핑x (Endpoint 단일화) + +### 추천 시스템 로직 + +- Collaborative Filtering & Cosine Similarity 기반 추천시스템 + 1. 각 Concert는 AI가 추출해준 키워드 값에 대해 0 ~ 1 사이의 값을 가짐 + 2. 유저도 마찬가지로 온보딩에서 등록한 취향 정보로 부터 모든 키워드 값에 대해 0 ~ 1사이 값을 가짐 + 3. Concerts, Users CSV파일을 통해서 Cosine Similarity와 Collaborative Filtering을 통한 유사도 계산 후 추천 진행 + +### 메인 서버 +- 서비스의 주요 로직을 처리하는 서버 +- Grafana와 Prometheus에 기반한 모니터링 시스템 구축 +- Nginx를 통한 리버스 프록시 설정 + +### AI 서버 + +- 공연 성격 분석이나, 유저 성격 분석, OCR을 처리하는 서버 +- OCR 및 공연 성격 정보 추출은 NCP의 AI 서비스를 활용 +- 추천 시스템의 경우 직접 Collaborative Filtering Model 구현 + +### 배치 서버 + +- KOPIS 시스템으로부터 공연 정보를 주기적으로 업데이트하는 서버(한달에 1번) +- KOPIS에서 데이터를 받아올때마다 AI서버에 학습 요청 + +# ⭐️ Frontend + +#### 배포 URL https://claco-client.vercel.app/ +* * * + +## 💻 Technology +* ![Static Badge](https://img.shields.io/badge/react-%252320232a.svg?logo=React&color=%231C1C1C) ![Static Badge](https://img.shields.io/badge/Zustand-%252320232a.svg?color=%231C1C1C) +* ![Static Badge](https://img.shields.io/badge/typescript-%253178C6.svg?logo=typescript&logoColor=%23FFFFFF&color=%233178C6) ![Static Badge](https://img.shields.io/badge/yarn-%253178C6.svg?logo=yarn&logoColor=%23FFFFFF&color=%232C8EBB) +* ![Static Badge](https://img.shields.io/badge/tailwindCSS-%253178C6.svg?logo=tailwindCSS&logoColor=%23FFFFFF&color=%2306B6D4) ![Static Badge](https://img.shields.io/badge/shadcn%2Fui-%253178C6.svg?logo=shadcn%2Fui&logoColor=%23FFFFFF&color=%23000000) +* ![Static Badge](https://img.shields.io/badge/TanStack%20Query-%253178C6.svg?logo=React%20Query&logoColor=%23FFFFFF&color=%23FF4154) ![Static Badge](https://img.shields.io/badge/Vercel-%253178C6.svg?logo=Vercel&logoColor=%23FFFFFF&color=%23000000) ![Static Badge](https://img.shields.io/badge/PWA-%253178C6.svg?logo=PWA&logoColor=%23FFFFFF&color=%235A0FC8) + + +## 🧸 기술 스택 선정 이유 + +| 기술 스택 | 설명 | +|-----------|------| +| React | React는 가장 핵심 요소인 Virtual Dom을 이용하여 불필요한 화면 갱신을 최소화합니다. 이를 통해, 성능 향상을 시킬 수 있으며 빠른 렌더링을 지원합니다. React는 컴포넌트 기반 아키텍처를 채택하고 있으며, UI 요소들을 컴포넌트로 분리하여 개발하고 조합하는 방식으로 구성할 수 있습니다. 따라서, 컴포넌트의 재사용성을 용이하게 하며, 코드 수정 및 유지·보수에 효율적이기에 React를 사용하게 되었습니다. | +| TypeScript | 정적 타입 언어로서 코드의 안정성을 높이고 협업을 용이하게 하며, 생산성 접근 파일 단계에서 오류를 사전에 발견하여 런타임 오류를 방지할 수 있습니다. 또한, 코드 힌트와 자동 완성을 제공해 개발 생산성을 향상시킬 수 있어 TypeScript를 선정했습니다. | +| Zustand | 간결하고 직관적인 상태 관리 방식과 최소한의 보일러플레이트로 유연성을 제공하며, 상태 변경 시에만 컴포넌트를 렌더링하여 불필요한 렌더링을 최소화하고 성능 향상에 도움이 됩니다. | +| Yarn | 빠른 속도와 높은 신뢰성을 바탕으로 안정적인 JavaScript 패키지 관리를 지원하며, 보안성 강화 기능을 제공하여 효율적인 프로젝트 관리가 가능합니다. | +| TailwindCSS | 개발의 편의성 HTML과 CSS 파일을 별도로 개발 및 관리할 필요가 없기 때문에 개발하기에 편리하고, 팀핑하는 각 태그의 클래스명을 고민할 시간을 절약할 수 있어 빠른 개발이 가능합니다. | +| shadcn/ui | TailwindCSS와의 긴밀한 통합으로 빠르고 일관된 스타일링이 가능하며, 높은 확장성과 유연한 커스터마이징 기능을 통해 프로젝트 요구사항에 맞는 UI를 효율적으로 구현할 수 있습니다. | +| Tanstack-Query | 효율적인 데이터 페칭과 관리를 제공하며, 데이터 캐싱 기능을 통해 불필요한 요청을 줄여 성능 최적화와 네트워크 비용 절감이 가능합니다. | +| Vercel | 깃 저장소와 통합되어 코드 변경 사항을 자동으로 감지하고, 푸시할 때마다 자동 배포를 지원하여 효율적이고 간편하게 웹사이트를 배포할 수 있습니다. | +| PWA | 웹 기술 기반으로 다양한 플랫폼에서 동작하고, 앱 스토어 없이 홈 화면에 추가할 수 있어 접근성과 편의성이 뛰어나며, 개발 비용 절감과 빠른 배포가 가능합니다. | + +## 🗂️ Naming Rules +* 폴더명 - `PascalCase` +* 파일명 - `PascalCase` +* 타입, 유틸함수 등 - `camelCase` +* 상수 - `UpperCase` + +## 📄 Commit Convension +커밋 메시지는 `태그: 커밋 메시지` 형식으로 작성 (ex. git commit -m "feat: 카카오 로그인 기능 구현") + +📌Type + +| 태그명 | commit 규칙 | +|----------|--------------| +| 🔗 feat | 새로운 기능 개발 | +| 🛠 fix | UI,UX 및 코드 수정 | +| 🎨 style | CSS 스타일링 및 퍼블리싱 작업 | +| 📄 docs | 문서 작업(REANME.md 등) | +| 📘 test | 배포 테스트, QA 테스팅 관련 | +| 🧰 refactor | 코드 리팩토링 | +| 🔧 rename | 폴더 혹은 파일명 변경 | +| ✂️ remove | 파일 삭제 | + +## 🍀 Issue Template +* `기능 개발 관련 Issue Template` + ### 이슈 설명☀️ + 이슈에 관한 설명 + + ### TO-DO📒 + - [ ] 할 일 1 + + ### 기타🍀 + +* `버그 수정 관련 Issue Template` + ### 에러 설명🚦 + 무슨 에러인지 설명! + + ### 환경⚙️ + 특정 기기에서만 발생하는 에러라면 디바이스 종류, 브라우저 종류 등! + + ### 재현 방법🧿 + 어떻게 재현하는지 설명! + + ### 에러 화면📸 + 스크린샷 or GIF 등.. + +## 🍀 Pull Request Template +* 관련 이슈 +* 기존 코드에 영향을 미치는 작업 사항 +* 기존 코드에 영향을 미치지 않는 작업 사항 +* 작업 내용 스크린 샷 +* 리뷰어에게 공유할 내용 +* 추후 작업할 내용 +* main (develop) 브랜치 pull 여부 확인 + +## 🌊 Git Flow +| 브랜치 명 | 역할 | +|----------|--------------| +| main | 최종 배포될 서비스의 브랜치 | +| develop | 개발 브랜치, 해당 브랜치에서 분기를 파 작업 후 merge | +| feature | 기능 개발 브랜치 | +| hotfix | main 브랜치 배포 후 긴급 수정 사항 발생 시 사용하는 브랜치 | + + +# 🔥 Backend + +## 💻 개발 환경 +> Language: ```Java 17```
+> Framework: ```Spring Boot 3.3.4```
+> Database: ```MySQL 8.x```
+> ORM: ```JPA(Hibernate)```
+> CI/CD: ```Github Actions```
+> Cloud Platform: ```AWS(EC2, ALB, ACM), GCP(SQL)```
+> Test DB: ```testcontainer``` + +## ⚙️ 개발 프로세스 +- ```TDD (테스트 주도 개발)``` : 구문 커버지리 (Statement coverage) 기준 80%를 목표로 수행 +- ```Agile (애자일 프로세스)``` : 1주 단위 스프린트 수행 +- ```Github Flow 전략``` : 초기 개발 과정에서 불필요한 브랜치 관리를 피하고, 빠른 배포를 위한 전략 선택 +- ```CI/CD 파이프라인을 통한 배포 자동화``` : 서비스 개발이 50% 완료된 시점에서 구축하여 배포 자동화 + +## 💫 TDD 결과 +- Service는 단위 테스트, Repository는 통합 테스트 진행 +- ```testcontainer```를 활용하여 데이터베이스 멱등성 보장 +- 테스트 코드 커버리지 측정 툴: ```IntelliJ```
+ +![img.png](./.github/readme-assets/test-coverage.png) +- summary + - statement coverage 기준: 88% + - branch coverage: 54.8% + - class coverage: 100% + - method coverage: 96.7% + +## 💫 부하 테스트 결과 +- 사용 인스턴스 유형: ```t2.large (ram 8GB)``` +- 부하 테스트 측정 툴: ```Jmeter``` + +![server-test.png](./.github/readme-assets/server-test.png) +- summary + - 도메인별 주요 api 평균 50.3 Throughput + + + + + +## 🔄 FlowChart of AI & Batch Server +![flow-chart.png](./.github/readme-assets/ai-flow.png) + +## 📁 ERD +![erd.png](./.github/readme-assets/erd.png) + +- 카테고리에서 연관 관계 설정을 통해 관계형 데이터베이스 활용 +- AI 서비스 학습을 위한 soft delete 활용 + +## 🛠️ CI/CD pipeline +![ci-cd.png](./.github/readme-assets/cicd.png) +1. PR 이벤트 발생 시 CI 실행 (테스트 포함) +2. approve 및 CI 성공 시 merge 가능 +3. merge 이벤트 발생 시 CI 스크립트 수행 +4. CI 스크립트 성공 시 CD 스크립트 수행 +5. Docker 이미지 docker hub에 push +6. SSH로 AWS EC2 연결 +7. docker hub에서 이미지 pull +8. dokcer-compose를 활용해 서비스 실행 및 도커 네트워크 구축 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..36891d7 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +cd ../ +mkdir output +cp -R ./claco-client/* ./output +cp -R ./output ./claco-client/ \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..6d7f3f9 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/index.html b/index.html index aa695ff..1c6407a 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,133 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Claco +
diff --git a/package.json b/package.json index 6b4ca0b..6efb722 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,51 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "pwa": "pwa-assets-generator --preset minimal public/icons/logo.svg" }, "dependencies": { + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.1", + "@svgr/rollup": "^8.1.0", "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query-devtools": "^5.61.0", + "@types/html2canvas": "^1.0.0", "@types/node": "^22.7.4", "autoprefixer": "^10.4.20", "axios": "^1.7.7", + "chart.js": "^4.4.5", + "chartjs-plugin-datalabels": "^2.2.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "file-saver": "^2.0.5", + "html2canvas": "^1.4.1", + "lucide-react": "^0.452.0", "postcss": "^8.4.47", "react": "^18.3.1", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", + "react-lottie-player": "^2.1.0", "react-router-dom": "^6.26.2", - "recoil": "^0.7.7", - "tailwindcss": "^3.4.13" + "swiper": "^11.1.14", + "tailwind-merge": "^2.5.4", + "tailwind-scrollbar-hide": "^1.1.7", + "tailwindcss": "^3.4.13", + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.12.0", + "@types/file-saver": "^2.0.7", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@typescript-eslint/parser": "^8.8.0", + "@vite-pwa/assets-generator": "^0.2.6", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", @@ -37,10 +60,9 @@ "eslint-plugin-react-refresh": "^0.4.12", "globals": "^15.10.0", "prettier": "^3.3.3", - "typescript": "^5.5.3", + "typescript": "~5.4.2", "typescript-eslint": "^8.8.0", "vite": "^5.4.8", - "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^5.0.1" } } diff --git a/public/assets/MagnifierBackground.png b/public/assets/MagnifierBackground.png new file mode 100644 index 0000000..21017eb Binary files /dev/null and b/public/assets/MagnifierBackground.png differ diff --git a/public/icons/apple-touch-icon-114x114.png b/public/icons/apple-touch-icon-114x114.png new file mode 100644 index 0000000..85c3960 Binary files /dev/null and b/public/icons/apple-touch-icon-114x114.png differ diff --git a/public/icons/apple-touch-icon-152x152.png b/public/icons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..3256498 Binary files /dev/null and b/public/icons/apple-touch-icon-152x152.png differ diff --git a/public/icons/apple-touch-icon-180x180.png b/public/icons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..9dee08d Binary files /dev/null and b/public/icons/apple-touch-icon-180x180.png differ diff --git a/public/icons/apple-touch-icon-60x60.png b/public/icons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..a57ff67 Binary files /dev/null and b/public/icons/apple-touch-icon-60x60.png differ diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png new file mode 100644 index 0000000..e2ae6e7 Binary files /dev/null and b/public/icons/favicon-16x16.png differ diff --git a/public/icons/favicon-196x196.png b/public/icons/favicon-196x196.png new file mode 100644 index 0000000..4595616 Binary files /dev/null and b/public/icons/favicon-196x196.png differ diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png new file mode 100644 index 0000000..cc1205e Binary files /dev/null and b/public/icons/favicon-32x32.png differ diff --git a/public/icons/favicon-96x96.png b/public/icons/favicon-96x96.png new file mode 100644 index 0000000..5d29d68 Binary files /dev/null and b/public/icons/favicon-96x96.png differ diff --git a/public/icons/favicon.ico b/public/icons/favicon.ico new file mode 100644 index 0000000..9e5549b Binary files /dev/null and b/public/icons/favicon.ico differ diff --git a/public/icons/logo.svg b/public/icons/logo.svg new file mode 100644 index 0000000..3acaae7 --- /dev/null +++ b/public/icons/logo.svg @@ -0,0 +1,60 @@ + + + + diff --git a/public/icons/maskable-icon-512x512.png b/public/icons/maskable-icon-512x512.png new file mode 100644 index 0000000..a8d5b11 Binary files /dev/null and b/public/icons/maskable-icon-512x512.png differ diff --git a/public/icons/mstile-150x150.png b/public/icons/mstile-150x150.png new file mode 100644 index 0000000..66e45b6 Binary files /dev/null and b/public/icons/mstile-150x150.png differ diff --git a/public/icons/mstile-310x310.png b/public/icons/mstile-310x310.png new file mode 100644 index 0000000..9e5d09c Binary files /dev/null and b/public/icons/mstile-310x310.png differ diff --git a/public/icons/mstile-70x70.png b/public/icons/mstile-70x70.png new file mode 100644 index 0000000..6debde0 Binary files /dev/null and b/public/icons/mstile-70x70.png differ diff --git a/public/icons/pwa-192x192.png b/public/icons/pwa-192x192.png new file mode 100644 index 0000000..e619df1 Binary files /dev/null and b/public/icons/pwa-192x192.png differ diff --git a/public/icons/pwa-512x512.png b/public/icons/pwa-512x512.png new file mode 100644 index 0000000..c416993 Binary files /dev/null and b/public/icons/pwa-512x512.png differ diff --git a/public/icons/pwa-64x64.png b/public/icons/pwa-64x64.png new file mode 100644 index 0000000..725185c Binary files /dev/null and b/public/icons/pwa-64x64.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..c2f2903 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,31 @@ +{ + "name": "Claco", + "short_name": "Claco", + "start_url": "/", + "display": "standalone", + "background_color": "#1C1C1C", + "theme_color": "#1C1C1C", + "icons": [ + { + "src": "icons/pwa-64x64.png", + "sizes": "64x64", + "type": "image/png" + }, + { + "src": "icons/pwa-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/pwa-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/maskable-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png b/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png new file mode 100644 index 0000000..fcfd9db Binary files /dev/null and b/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png differ diff --git a/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png b/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png new file mode 100644 index 0000000..44fd001 Binary files /dev/null and b/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_11__iPhone_XR_portrait.png b/public/splash_screens/iPhone_11__iPhone_XR_portrait.png new file mode 100644 index 0000000..56ac6a8 Binary files /dev/null and b/public/splash_screens/iPhone_11__iPhone_XR_portrait.png differ diff --git a/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png b/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png new file mode 100644 index 0000000..e07d487 Binary files /dev/null and b/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png differ diff --git a/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png b/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png new file mode 100644 index 0000000..95fc44e Binary files /dev/null and b/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png b/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png new file mode 100644 index 0000000..48cb7db Binary files /dev/null and b/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png differ diff --git a/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png b/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png new file mode 100644 index 0000000..50ebea2 Binary files /dev/null and b/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_16_Pro_Max_portrait.png b/public/splash_screens/iPhone_16_Pro_Max_portrait.png new file mode 100644 index 0000000..d6f6350 Binary files /dev/null and b/public/splash_screens/iPhone_16_Pro_Max_portrait.png differ diff --git a/public/splash_screens/iPhone_16_Pro_portrait.png b/public/splash_screens/iPhone_16_Pro_portrait.png new file mode 100644 index 0000000..9e80caa Binary files /dev/null and b/public/splash_screens/iPhone_16_Pro_portrait.png differ diff --git a/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png b/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png new file mode 100644 index 0000000..fd8d275 Binary files /dev/null and b/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png differ diff --git a/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png b/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png new file mode 100644 index 0000000..c9d9e9c Binary files /dev/null and b/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png differ diff --git a/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png b/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png new file mode 100644 index 0000000..72a4f53 Binary files /dev/null and b/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 0d63c3e..4cb8a29 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ -import { RouterProvider } from 'react-router-dom'; -import AppRegister from '@/libraries/index'; -import Router from '@/routes/router'; +import { RouterProvider } from "react-router-dom"; +import Router from "@/routes/router"; +import AppContainer from "@/libraries/index"; export default function App() { return ( - + - + ); } diff --git a/src/apis/client.ts b/src/apis/client.ts new file mode 100644 index 0000000..8e37085 --- /dev/null +++ b/src/apis/client.ts @@ -0,0 +1,53 @@ +import axios, { InternalAxiosRequestConfig } from "axios"; + +const client = axios.create({ + baseURL: import.meta.env.VITE_SERVER_URL, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, +}); + +client.interceptors.response.use( + (res) => { + if (res.data.refreshed) { + const new_accessToken = res.headers["authorization"]; + localStorage.setItem("accessToken", new_accessToken); + window.location.replace("/main"); + } + + // /* 해당 에러 발생 시 재로그인 하도록 로그인 화면으로 리다이렉트 */ + if ( + res.data.code === "ACT-001" || + res.data.code === "RFT-001" || + res.data.code === "MSE-001" || + res.data.code === "ATH-001" + ) { + localStorage.clear(); + window.location.replace("/"); + } + return res; + }, + async (error) => { + console.error(error); + return Promise.reject(error); + }, +); + +client.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + config.withCredentials = true; + const accessToken = localStorage.getItem("accessToken"); + if (accessToken) { + config.headers = config.headers || {}; + config.headers.Authorization = `${accessToken}`; + } else { + return config; + } + return config; + }, + (error) => { + return Promise.reject(error); + }, +); +export default client; diff --git a/src/apis/index.ts b/src/apis/index.ts new file mode 100644 index 0000000..6742b42 --- /dev/null +++ b/src/apis/index.ts @@ -0,0 +1,5 @@ +export { default as client } from "./client"; +export { default as nickNameCheck } from "./nickNameCheck"; +export { default as submitUserInformation } from "./submitUserInformation"; +export { default as getPlaceCategories} from "./useGetPlaceCategories"; +export { default as getTagCategories } from "./useGetTagCategories"; \ No newline at end of file diff --git a/src/apis/nickNameCheck.ts b/src/apis/nickNameCheck.ts new file mode 100644 index 0000000..8578ccd --- /dev/null +++ b/src/apis/nickNameCheck.ts @@ -0,0 +1,15 @@ +import client from "@/apis/client"; + +export type NickNameCheckResponse = { + code: string; + message: string; +}; + +const nickNameCheck = async (nickName: string) => { + const response = await client.get( + `/members/check-nickname?nickname=${nickName}` + ); + return response.data; +}; + +export default nickNameCheck; diff --git a/src/apis/submitUserInformation.ts b/src/apis/submitUserInformation.ts new file mode 100644 index 0000000..0107863 --- /dev/null +++ b/src/apis/submitUserInformation.ts @@ -0,0 +1,36 @@ +import client from "./client"; + +export type UserInformationResponse = { + code: string; + message: string; +}; + +export type UserInformationRequest = { + nickname: string; + gender: string; + age: number; + minPrice: number; + maxPrice: number; + regionPreferences: { preferenceRegion: string }[]; + typePreferences: { preferenceType: string }[]; + categoryPreferences: { preferenceCategory: string }[]; +}; + +const submitUserInformation = (userInfo: UserInformationRequest) => { + return client.post(`/members`, userInfo); +}; + +// const useUserInformationSubmit = async (userInfo: UserInformationRequest) => { +// try { +// const response = await client.post( +// `/members`, +// userInfo +// ); +// return response.data; +// } catch (error) { +// console.error(error); +// throw error; +// } +// }; + +export default submitUserInformation; diff --git a/src/apis/useGetPlaceCategories.ts b/src/apis/useGetPlaceCategories.ts new file mode 100644 index 0000000..771aa2a --- /dev/null +++ b/src/apis/useGetPlaceCategories.ts @@ -0,0 +1,10 @@ +import { client } from "@/apis"; +import { PlaceCategoriesResponse } from "@/types"; + +const getPlaceCategories = async (): Promise => { + const response = + await client.get("/place-categories"); + return response.data; +}; + +export default getPlaceCategories; diff --git a/src/apis/useGetShowPosterUrl.ts b/src/apis/useGetShowPosterUrl.ts new file mode 100644 index 0000000..90bbb02 --- /dev/null +++ b/src/apis/useGetShowPosterUrl.ts @@ -0,0 +1,24 @@ +import client from "./client"; + +export type ShowPosterUrlResponse = { + code: string; + message: string; + result: string; + refreshed: boolean; +}; + +const useGetShowPoster = async ( + KopisURL: string, +): Promise => { + try { + const response = await client.get( + `/concerts/posters?KopisURL=${encodeURIComponent(KopisURL)}`, + ); + return response.data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export default useGetShowPoster; diff --git a/src/apis/useGetTagCategories.ts b/src/apis/useGetTagCategories.ts new file mode 100644 index 0000000..7a132ef --- /dev/null +++ b/src/apis/useGetTagCategories.ts @@ -0,0 +1,9 @@ +import { client } from "@/apis"; +import { TagCategoriesResponse } from "@/types"; + +const getTagCategories = async (): Promise => { + const response = await client.get("/tag-categories"); + return response.data; +}; + +export default getTagCategories; diff --git a/src/assets/fonts/Nonchalance-Medium.woff b/src/assets/fonts/Nonchalance-Medium.woff new file mode 100644 index 0000000..63351aa Binary files /dev/null and b/src/assets/fonts/Nonchalance-Medium.woff differ diff --git a/src/assets/fonts/Nonchalance-Medium.woff2 b/src/assets/fonts/Nonchalance-Medium.woff2 new file mode 100644 index 0000000..8faa648 Binary files /dev/null and b/src/assets/fonts/Nonchalance-Medium.woff2 differ diff --git a/src/assets/fonts/Pretendard-Bold.subset.woff b/src/assets/fonts/Pretendard-Bold.subset.woff new file mode 100644 index 0000000..06ba102 Binary files /dev/null and b/src/assets/fonts/Pretendard-Bold.subset.woff differ diff --git a/src/assets/fonts/Pretendard-Bold.subset.woff2 b/src/assets/fonts/Pretendard-Bold.subset.woff2 new file mode 100644 index 0000000..f80bdb7 Binary files /dev/null and b/src/assets/fonts/Pretendard-Bold.subset.woff2 differ diff --git a/src/assets/fonts/Pretendard-Medium.subset.woff b/src/assets/fonts/Pretendard-Medium.subset.woff new file mode 100644 index 0000000..f97a78f Binary files /dev/null and b/src/assets/fonts/Pretendard-Medium.subset.woff differ diff --git a/src/assets/fonts/Pretendard-Medium.subset.woff2 b/src/assets/fonts/Pretendard-Medium.subset.woff2 new file mode 100644 index 0000000..9419980 Binary files /dev/null and b/src/assets/fonts/Pretendard-Medium.subset.woff2 differ diff --git a/src/assets/fonts/Pretendard-Regular.subset.woff b/src/assets/fonts/Pretendard-Regular.subset.woff new file mode 100644 index 0000000..174736a Binary files /dev/null and b/src/assets/fonts/Pretendard-Regular.subset.woff differ diff --git a/src/assets/fonts/Pretendard-Regular.subset.woff2 b/src/assets/fonts/Pretendard-Regular.subset.woff2 new file mode 100644 index 0000000..6fc8ec4 Binary files /dev/null and b/src/assets/fonts/Pretendard-Regular.subset.woff2 differ diff --git a/src/assets/fonts/Pretendard-SemiBold.subset.woff b/src/assets/fonts/Pretendard-SemiBold.subset.woff new file mode 100644 index 0000000..ee2fa3d Binary files /dev/null and b/src/assets/fonts/Pretendard-SemiBold.subset.woff differ diff --git a/src/assets/fonts/Pretendard-SemiBold.subset.woff2 b/src/assets/fonts/Pretendard-SemiBold.subset.woff2 new file mode 100644 index 0000000..38175ff Binary files /dev/null and b/src/assets/fonts/Pretendard-SemiBold.subset.woff2 differ diff --git a/src/assets/images/ClacoBook/ClcaoBook_Gray.png b/src/assets/images/ClacoBook/ClcaoBook_Gray.png new file mode 100644 index 0000000..5208a31 Binary files /dev/null and b/src/assets/images/ClacoBook/ClcaoBook_Gray.png differ diff --git a/src/assets/images/ClacoBook/ClcaoBook_Orange.png b/src/assets/images/ClacoBook/ClcaoBook_Orange.png new file mode 100644 index 0000000..667c193 Binary files /dev/null and b/src/assets/images/ClacoBook/ClcaoBook_Orange.png differ diff --git a/src/assets/images/ClacoBook/ClcaoBook_Pink.png b/src/assets/images/ClacoBook/ClcaoBook_Pink.png new file mode 100644 index 0000000..e4b9d26 Binary files /dev/null and b/src/assets/images/ClacoBook/ClcaoBook_Pink.png differ diff --git a/src/assets/images/Genre/classical.png b/src/assets/images/Genre/classical.png new file mode 100644 index 0000000..a891d3a Binary files /dev/null and b/src/assets/images/Genre/classical.png differ diff --git a/src/assets/images/Genre/delicate.png b/src/assets/images/Genre/delicate.png new file mode 100644 index 0000000..26cf149 Binary files /dev/null and b/src/assets/images/Genre/delicate.png differ diff --git a/src/assets/images/Genre/dynamic.png b/src/assets/images/Genre/dynamic.png new file mode 100644 index 0000000..ef3e9ff Binary files /dev/null and b/src/assets/images/Genre/dynamic.png differ diff --git a/src/assets/images/Genre/familiar.png b/src/assets/images/Genre/familiar.png new file mode 100644 index 0000000..e8a1893 Binary files /dev/null and b/src/assets/images/Genre/familiar.png differ diff --git a/src/assets/images/Genre/grand.png b/src/assets/images/Genre/grand.png new file mode 100644 index 0000000..4680c64 Binary files /dev/null and b/src/assets/images/Genre/grand.png differ diff --git a/src/assets/images/Genre/lyrical.png b/src/assets/images/Genre/lyrical.png new file mode 100644 index 0000000..a9fa9bb Binary files /dev/null and b/src/assets/images/Genre/lyrical.png differ diff --git a/src/assets/images/Genre/modern.png b/src/assets/images/Genre/modern.png new file mode 100644 index 0000000..e714efe Binary files /dev/null and b/src/assets/images/Genre/modern.png differ diff --git a/src/assets/images/Genre/novel.png b/src/assets/images/Genre/novel.png new file mode 100644 index 0000000..c0cc723 Binary files /dev/null and b/src/assets/images/Genre/novel.png differ diff --git a/src/assets/images/Genre/romantic.png b/src/assets/images/Genre/romantic.png new file mode 100644 index 0000000..3a930cd Binary files /dev/null and b/src/assets/images/Genre/romantic.png differ diff --git a/src/assets/images/Genre/tragic.png b/src/assets/images/Genre/tragic.png new file mode 100644 index 0000000..e6036f8 Binary files /dev/null and b/src/assets/images/Genre/tragic.png differ diff --git a/src/assets/images/kakao.png b/src/assets/images/kakao.png new file mode 100644 index 0000000..5afd83a Binary files /dev/null and b/src/assets/images/kakao.png differ diff --git a/src/assets/images/loading.gif b/src/assets/images/loading.gif new file mode 100644 index 0000000..c9d73bb Binary files /dev/null and b/src/assets/images/loading.gif differ diff --git a/src/assets/images/loginbackground.png b/src/assets/images/loginbackground.png new file mode 100644 index 0000000..c1971d6 Binary files /dev/null and b/src/assets/images/loginbackground.png differ diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..9342216 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/assets/images/showReview.png b/src/assets/images/showReview.png new file mode 100644 index 0000000..9fd059a Binary files /dev/null and b/src/assets/images/showReview.png differ diff --git a/src/assets/lotties/onboarding-loading.json b/src/assets/lotties/onboarding-loading.json new file mode 100644 index 0000000..660a90d --- /dev/null +++ b/src/assets/lotties/onboarding-loading.json @@ -0,0 +1,2104 @@ +{ + "assets": [ + { "h": 339, "id": "0", "p": "MagnifierBackground.png", "u": "https://claco-client.vercel.app/assets/", "w": 344, "e": 0 }, + { "h": 170, "id": "1", "p": "delicate-COgzgCmJ.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "2", "p": "dynamic-DyHv12pT.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "3", "p": "familiar-Dg6qKd33.png", "u": "https://claco-client.vercel.app/assets/", "w": 168, "e": 0 }, + { "h": 170, "id": "4", "p": "grand-BYiR4a-o.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "5", "p": "lyrical-BgxdJF4r.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "6", "p": "modern-CpuftXSH.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "7", "p": "novel-DIk8ha5_.png", "u": "https://claco-client.vercel.app/assets/", "w": 168, "e": 0 }, + { "h": 170, "id": "8", "p": "romantic-o0K20Lyg.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "9", "p": "tragic-5xq7Nqci.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { "h": 170, "id": "10", "p": "classical-BXnO5iTb.png", "u": "https://claco-client.vercel.app/assets/", "w": 170, "e": 0 }, + { + "id": "17", + "layers": [ + { + "ind": 16, + "ty": 4, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "shapes": [ + { + "ty": "rc", + "p": { "a": 0, "k": [33, 31.5] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [66, 63] } + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0, 0] }, + "o": { "a": 0, "k": 0 } + } + ] + }, + { + "ind": 0, + "ty": 4, + "ks": { "s": { "a": 0, "k": [133.33, 133.33] } }, + "ip": 0, + "op": 481, + "st": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [60.73, 58], + [5.47, 5.9] + ] + } + } + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.98, 0.82, 0.76, 1] }, + "lc": 2, + "lj": 2, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 10 } + }, + { + "ty": "tr", + "o": { "a": 0, "k": 100 }, + "s": { "a": 0, "k": [75, 75] } + } + ] + }, + { "ty": "tr", "o": { "a": 0, "k": 100 } } + ] + } + ] + } + ] + }, + { + "id": "23", + "layers": [ + { + "ind": 22, + "ty": 4, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "shapes": [ + { + "ty": "rc", + "p": { "a": 0, "k": [80.5, 78.5] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [161, 157] } + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0, 0] }, + "o": { "a": 0, "k": 0 } + } + ] + }, + { + "ind": 0, + "ty": 4, + "ks": { "s": { "a": 0, "k": [133.33, 133.33] } }, + "ip": 0, + "op": 481, + "st": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [0, 0], + [0, 40.55], + [41.42, 0], + [0, -40.55], + [-41.42, 0] + ], + "o": [ + [41.42, 0], + [0, -40.55], + [-41.42, 0], + [0, 40.55], + [0, 0] + ], + "v": [ + [80.58, 151.84], + [155.58, 78.42], + [80.58, 5], + [5.58, 78.42], + [80.58, 151.84] + ] + } + } + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.98, 0.82, 0.76, 1] }, + "lc": 2, + "lj": 2, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 10 } + }, + { + "ty": "tr", + "o": { "a": 0, "k": 100 }, + "s": { "a": 0, "k": [75, 75] } + } + ] + }, + { "ty": "tr", "o": { "a": 0, "k": 100 } } + ] + } + ] + } + ] + }, + { + "id": "34", + "layers": [ + { + "ind": 33, + "ty": 2, + "parent": 32, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "1" + }, + { + "ind": 32, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "40", + "layers": [ + { + "ind": 39, + "ty": 2, + "parent": 38, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "2" + }, + { + "ind": 38, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "46", + "layers": [ + { + "ind": 45, + "ty": 2, + "parent": 44, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "3" + }, + { + "ind": 44, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "52", + "layers": [ + { + "ind": 51, + "ty": 2, + "parent": 50, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "4" + }, + { + "ind": 50, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "58", + "layers": [ + { + "ind": 57, + "ty": 2, + "parent": 56, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "5" + }, + { + "ind": 56, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "64", + "layers": [ + { + "ind": 63, + "ty": 2, + "parent": 62, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "6" + }, + { + "ind": 62, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "70", + "layers": [ + { + "ind": 69, + "ty": 2, + "parent": 68, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "7" + }, + { + "ind": 68, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "76", + "layers": [ + { + "ind": 75, + "ty": 2, + "parent": 74, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "8" + }, + { + "ind": 74, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "82", + "layers": [ + { + "ind": 81, + "ty": 2, + "parent": 80, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "9" + }, + { + "ind": 80, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "88", + "layers": [ + { + "ind": 87, + "ty": 2, + "parent": 86, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "10" + }, + { + "ind": 86, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "95", + "layers": [ + { + "ind": 94, + "ty": 2, + "parent": 93, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "1" + }, + { + "ind": 93, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "101", + "layers": [ + { + "ind": 100, + "ty": 2, + "parent": 99, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "2" + }, + { + "ind": 99, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "107", + "layers": [ + { + "ind": 106, + "ty": 2, + "parent": 105, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "3" + }, + { + "ind": 105, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "113", + "layers": [ + { + "ind": 112, + "ty": 2, + "parent": 111, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "4" + }, + { + "ind": 111, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "119", + "layers": [ + { + "ind": 118, + "ty": 2, + "parent": 117, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "5" + }, + { + "ind": 117, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "125", + "layers": [ + { + "ind": 124, + "ty": 2, + "parent": 123, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "6" + }, + { + "ind": 123, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "131", + "layers": [ + { + "ind": 130, + "ty": 2, + "parent": 129, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "7" + }, + { + "ind": 129, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "137", + "layers": [ + { + "ind": 136, + "ty": 2, + "parent": 135, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "8" + }, + { + "ind": 135, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "143", + "layers": [ + { + "ind": 142, + "ty": 2, + "parent": 141, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "9" + }, + { + "ind": 141, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "149", + "layers": [ + { + "ind": 148, + "ty": 2, + "parent": 147, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "10" + }, + { + "ind": 147, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "157", + "layers": [ + { + "ind": 156, + "ty": 2, + "parent": 155, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "1" + }, + { + "ind": 155, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "163", + "layers": [ + { + "ind": 162, + "ty": 2, + "parent": 161, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "2" + }, + { + "ind": 161, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "169", + "layers": [ + { + "ind": 168, + "ty": 2, + "parent": 167, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "3" + }, + { + "ind": 167, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "175", + "layers": [ + { + "ind": 174, + "ty": 2, + "parent": 173, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "4" + }, + { + "ind": 173, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "181", + "layers": [ + { + "ind": 180, + "ty": 2, + "parent": 179, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "5" + }, + { + "ind": 179, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "187", + "layers": [ + { + "ind": 186, + "ty": 2, + "parent": 185, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "6" + }, + { + "ind": 185, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "193", + "layers": [ + { + "ind": 192, + "ty": 2, + "parent": 191, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "7" + }, + { + "ind": 191, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "199", + "layers": [ + { + "ind": 198, + "ty": 2, + "parent": 197, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "8" + }, + { + "ind": 197, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "205", + "layers": [ + { + "ind": 204, + "ty": 2, + "parent": 203, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "9" + }, + { + "ind": 203, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "211", + "layers": [ + { + "ind": 210, + "ty": 2, + "parent": 209, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "10" + }, + { + "ind": 209, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "218", + "layers": [ + { + "ind": 217, + "ty": 2, + "parent": 216, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "1" + }, + { + "ind": 216, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "224", + "layers": [ + { + "ind": 223, + "ty": 2, + "parent": 222, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "2" + }, + { + "ind": 222, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "230", + "layers": [ + { + "ind": 229, + "ty": 2, + "parent": 228, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "3" + }, + { + "ind": 228, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "236", + "layers": [ + { + "ind": 235, + "ty": 2, + "parent": 234, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "4" + }, + { + "ind": 234, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "242", + "layers": [ + { + "ind": 241, + "ty": 2, + "parent": 240, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "5" + }, + { + "ind": 240, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "248", + "layers": [ + { + "ind": 247, + "ty": 2, + "parent": 246, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "6" + }, + { + "ind": 246, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "254", + "layers": [ + { + "ind": 253, + "ty": 2, + "parent": 252, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "7" + }, + { + "ind": 252, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "260", + "layers": [ + { + "ind": 259, + "ty": 2, + "parent": 258, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "8" + }, + { + "ind": 258, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "266", + "layers": [ + { + "ind": 265, + "ty": 2, + "parent": 264, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "9" + }, + { + "ind": 264, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + }, + { + "id": "272", + "layers": [ + { + "ind": 271, + "ty": 2, + "parent": 270, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0, + "refId": "10" + }, + { + "ind": 270, + "ty": 3, + "ks": { "s": { "a": 0, "k": [50, 50] } }, + "ip": 0, + "op": 481, + "st": 0 + } + ] + } + ], + "fr": 60, + "h": 844, + "ip": 0, + "layers": [ + { + "ind": 19, + "ty": 0, + "parent": 15, + "ks": {}, + "w": 66, + "h": 63, + "ip": 0, + "op": 481, + "st": 0, + "refId": "17" + }, + { + "ind": 15, + "ty": 3, + "parent": 14, + "ks": { "s": { "a": 0, "k": [76.32, 76.67] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 14, + "ty": 3, + "parent": 13, + "ks": { "p": { "a": 0, "k": [94.643, 93.533] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 25, + "ty": 0, + "parent": 21, + "ks": {}, + "w": 161, + "h": 157, + "ip": 0, + "op": 481, + "st": 0, + "refId": "23" + }, + { + "ind": 21, + "ty": 3, + "parent": 20, + "ks": { "s": { "a": 0, "k": [76.32, 76.67] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 20, + "ty": 3, + "parent": 13, + "ks": { "p": { "a": 0, "k": [-3.053, -3.833] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 28, + "ty": 2, + "parent": 27, + "ks": { "s": { "a": 0, "k": [43.6, 43.36] } }, + "ip": 0, + "op": 481, + "st": 0, + "refId": "0" + }, + { + "ind": 27, + "ty": 3, + "parent": 26, + "ks": { "s": { "a": 0, "k": [76.32, 76.67] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { "ind": 26, "ty": 3, "parent": 13, "ks": {}, "ip": 0, "op": 481, "st": 0 }, + { + "ind": 13, + "ty": 3, + "parent": 12, + "ks": { "p": { "a": 0, "k": [125, 387] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 36, + "ty": 0, + "parent": 31, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "34" + }, + { "ind": 31, "ty": 3, "parent": 30, "ks": {}, "ip": 0, "op": 481, "st": 0 }, + { + "ind": 42, + "ty": 0, + "parent": 37, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "40" + }, + { + "ind": 37, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [105, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 48, + "ty": 0, + "parent": 43, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "46" + }, + { + "ind": 43, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [944, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 54, + "ty": 0, + "parent": 49, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "52" + }, + { + "ind": 49, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [210, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 60, + "ty": 0, + "parent": 55, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "58" + }, + { + "ind": 55, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [315, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 66, + "ty": 0, + "parent": 61, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "64" + }, + { + "ind": 61, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [420, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 72, + "ty": 0, + "parent": 67, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "70" + }, + { + "ind": 67, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [840, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 78, + "ty": 0, + "parent": 73, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "76" + }, + { + "ind": 73, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [735, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 84, + "ty": 0, + "parent": 79, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "82" + }, + { + "ind": 79, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [630, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 90, + "ty": 0, + "parent": 85, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "88" + }, + { + "ind": 85, + "ty": 3, + "parent": 30, + "ks": { "p": { "a": 0, "k": [525, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 30, + "ty": 3, + "parent": 29, + "ks": { "p": { "a": 0, "k": [1048, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 97, + "ty": 0, + "parent": 92, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "95" + }, + { "ind": 92, "ty": 3, "parent": 91, "ks": {}, "ip": 0, "op": 481, "st": 0 }, + { + "ind": 103, + "ty": 0, + "parent": 98, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "101" + }, + { + "ind": 98, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [105, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 109, + "ty": 0, + "parent": 104, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "107" + }, + { + "ind": 104, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [944, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 115, + "ty": 0, + "parent": 110, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "113" + }, + { + "ind": 110, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [210, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 121, + "ty": 0, + "parent": 116, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "119" + }, + { + "ind": 116, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [315, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 127, + "ty": 0, + "parent": 122, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "125" + }, + { + "ind": 122, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [420, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 133, + "ty": 0, + "parent": 128, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "131" + }, + { + "ind": 128, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [840, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 139, + "ty": 0, + "parent": 134, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "137" + }, + { + "ind": 134, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [735, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 145, + "ty": 0, + "parent": 140, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "143" + }, + { + "ind": 140, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [630, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 151, + "ty": 0, + "parent": 146, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "149" + }, + { + "ind": 146, + "ty": 3, + "parent": 91, + "ks": { "p": { "a": 0, "k": [525, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { "ind": 91, "ty": 3, "parent": 29, "ks": {}, "ip": 0, "op": 481, "st": 0 }, + { + "ind": 29, + "ty": 3, + "parent": 12, + "ks": { + "p": { + "a": 1, + "k": [ + { + "t": 0, + "s": [-1723, 456], + "i": { "x": [0, 1], "y": [1, 1] }, + "o": { "x": [0.5, 0], "y": [0, 0] } + }, + { "t": 480, "s": [20, 456], "h": 1 } + ] + } + }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 159, + "ty": 0, + "parent": 154, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "157" + }, + { + "ind": 154, + "ty": 3, + "parent": 153, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 165, + "ty": 0, + "parent": 160, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "163" + }, + { + "ind": 160, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [105, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 171, + "ty": 0, + "parent": 166, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "169" + }, + { + "ind": 166, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [944, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 177, + "ty": 0, + "parent": 172, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "175" + }, + { + "ind": 172, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [210, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 183, + "ty": 0, + "parent": 178, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "181" + }, + { + "ind": 178, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [315, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 189, + "ty": 0, + "parent": 184, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "187" + }, + { + "ind": 184, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [420, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 195, + "ty": 0, + "parent": 190, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "193" + }, + { + "ind": 190, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [840, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 201, + "ty": 0, + "parent": 196, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "199" + }, + { + "ind": 196, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [735, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 207, + "ty": 0, + "parent": 202, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "205" + }, + { + "ind": 202, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [630, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 213, + "ty": 0, + "parent": 208, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "211" + }, + { + "ind": 208, + "ty": 3, + "parent": 153, + "ks": { "p": { "a": 0, "k": [525, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 153, + "ty": 3, + "parent": 152, + "ks": { "p": { "a": 0, "k": [1048, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 220, + "ty": 0, + "parent": 215, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "218" + }, + { + "ind": 215, + "ty": 3, + "parent": 214, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 226, + "ty": 0, + "parent": 221, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "224" + }, + { + "ind": 221, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [105, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 232, + "ty": 0, + "parent": 227, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "230" + }, + { + "ind": 227, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [944, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 238, + "ty": 0, + "parent": 233, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "236" + }, + { + "ind": 233, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [210, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 244, + "ty": 0, + "parent": 239, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "242" + }, + { + "ind": 239, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [315, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 250, + "ty": 0, + "parent": 245, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "248" + }, + { + "ind": 245, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [420, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 256, + "ty": 0, + "parent": 251, + "ks": {}, + "w": 84, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "254" + }, + { + "ind": 251, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [840, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 262, + "ty": 0, + "parent": 257, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "260" + }, + { + "ind": 257, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [735, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 268, + "ty": 0, + "parent": 263, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "266" + }, + { + "ind": 263, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [630, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 274, + "ty": 0, + "parent": 269, + "ks": {}, + "w": 85, + "h": 85, + "ip": 0, + "op": 481, + "st": 0, + "refId": "272" + }, + { + "ind": 269, + "ty": 3, + "parent": 214, + "ks": { "p": { "a": 0, "k": [525, 0] } }, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 214, + "ty": 3, + "parent": 152, + "ks": {}, + "ip": 0, + "op": 481, + "st": 0 + }, + { + "ind": 152, + "ty": 3, + "parent": 12, + "ks": { + "p": { + "a": 1, + "k": [ + { + "t": 0, + "s": [27, 350], + "i": { "x": [0, 1], "y": [1, 1] }, + "o": { "x": [0.5, 0], "y": [0, 0] } + }, + { "t": 480, "s": [-1688, 350], "h": 1 } + ] + } + }, + "ip": 0, + "op": 481, + "st": 0 + }, + { "ind": 12, "ty": 3, "parent": 11, "ks": {}, "ip": 0, "op": 481, "st": 0 }, + { "ind": 11, "ty": 3, "ks": {}, "ip": 0, "op": 481, "st": 0 } + ], + "meta": { "g": "https://jitter.video" }, + "op": 480, + "v": "5.7.4", + "w": 390 +} diff --git a/src/assets/svgs/Arrow 2.svg b/src/assets/svgs/Arrow 2.svg new file mode 100644 index 0000000..f4b3e42 --- /dev/null +++ b/src/assets/svgs/Arrow 2.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/BackArrow.svg b/src/assets/svgs/BackArrow.svg new file mode 100644 index 0000000..5d942f2 --- /dev/null +++ b/src/assets/svgs/BackArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/Book.svg b/src/assets/svgs/Book.svg new file mode 100644 index 0000000..16761a8 --- /dev/null +++ b/src/assets/svgs/Book.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/svgs/BookMark.svg b/src/assets/svgs/BookMark.svg new file mode 100644 index 0000000..779ded3 --- /dev/null +++ b/src/assets/svgs/BookMark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/Calendar.svg b/src/assets/svgs/Calendar.svg new file mode 100644 index 0000000..9f71a82 --- /dev/null +++ b/src/assets/svgs/Calendar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/Claco_Main.svg b/src/assets/svgs/Claco_Main.svg new file mode 100644 index 0000000..e6642da --- /dev/null +++ b/src/assets/svgs/Claco_Main.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/Claco_Ticket.svg b/src/assets/svgs/Claco_Ticket.svg new file mode 100644 index 0000000..f2fc16c --- /dev/null +++ b/src/assets/svgs/Claco_Ticket.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/CompletePreference.svg b/src/assets/svgs/CompletePreference.svg new file mode 100644 index 0000000..36dffca --- /dev/null +++ b/src/assets/svgs/CompletePreference.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/DownLoadBox.svg b/src/assets/svgs/DownLoadBox.svg new file mode 100644 index 0000000..629a20d --- /dev/null +++ b/src/assets/svgs/DownLoadBox.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/svgs/Edit.svg b/src/assets/svgs/Edit.svg new file mode 100644 index 0000000..42aebd0 --- /dev/null +++ b/src/assets/svgs/Edit.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svgs/Female.svg b/src/assets/svgs/Female.svg new file mode 100644 index 0000000..6a24bc4 --- /dev/null +++ b/src/assets/svgs/Female.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/Heart.svg b/src/assets/svgs/Heart.svg new file mode 100644 index 0000000..761f404 --- /dev/null +++ b/src/assets/svgs/Heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/Light.svg b/src/assets/svgs/Light.svg new file mode 100644 index 0000000..971dd5c --- /dev/null +++ b/src/assets/svgs/Light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/Like.svg b/src/assets/svgs/Like.svg new file mode 100644 index 0000000..ce8d153 --- /dev/null +++ b/src/assets/svgs/Like.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svgs/Location.svg b/src/assets/svgs/Location.svg new file mode 100644 index 0000000..2a90c2e --- /dev/null +++ b/src/assets/svgs/Location.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/Location_gray.svg b/src/assets/svgs/Location_gray.svg new file mode 100644 index 0000000..de6a792 --- /dev/null +++ b/src/assets/svgs/Location_gray.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/Male.svg b/src/assets/svgs/Male.svg new file mode 100644 index 0000000..6bc3c8a --- /dev/null +++ b/src/assets/svgs/Male.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/Megaphone.svg b/src/assets/svgs/Megaphone.svg new file mode 100644 index 0000000..8a8606d --- /dev/null +++ b/src/assets/svgs/Megaphone.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/RightArrow.svg b/src/assets/svgs/RightArrow.svg new file mode 100644 index 0000000..2434584 --- /dev/null +++ b/src/assets/svgs/RightArrow.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/Romantic.svg b/src/assets/svgs/Romantic.svg new file mode 100644 index 0000000..c05e347 --- /dev/null +++ b/src/assets/svgs/Romantic.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/StarRating.svg b/src/assets/svgs/StarRating.svg new file mode 100644 index 0000000..f8c316c --- /dev/null +++ b/src/assets/svgs/StarRating.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/UserProfile.svg b/src/assets/svgs/UserProfile.svg new file mode 100644 index 0000000..7f374d8 --- /dev/null +++ b/src/assets/svgs/UserProfile.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/svgs/X-icon.svg b/src/assets/svgs/X-icon.svg new file mode 100644 index 0000000..0b76aee --- /dev/null +++ b/src/assets/svgs/X-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svgs/agreeIcon.svg b/src/assets/svgs/agreeIcon.svg new file mode 100644 index 0000000..4f9e47c --- /dev/null +++ b/src/assets/svgs/agreeIcon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svgs/chat.svg b/src/assets/svgs/chat.svg new file mode 100644 index 0000000..582485b --- /dev/null +++ b/src/assets/svgs/chat.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/svgs/check.svg b/src/assets/svgs/check.svg new file mode 100644 index 0000000..5bc9292 --- /dev/null +++ b/src/assets/svgs/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/checked.svg b/src/assets/svgs/checked.svg new file mode 100644 index 0000000..586986f --- /dev/null +++ b/src/assets/svgs/checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/dotsthree.svg b/src/assets/svgs/dotsthree.svg new file mode 100644 index 0000000..3aec909 --- /dev/null +++ b/src/assets/svgs/dotsthree.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svgs/download.svg b/src/assets/svgs/download.svg new file mode 100644 index 0000000..fd70fd0 --- /dev/null +++ b/src/assets/svgs/download.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/errorIcon.svg b/src/assets/svgs/errorIcon.svg new file mode 100644 index 0000000..fc20439 --- /dev/null +++ b/src/assets/svgs/errorIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/svgs/filter.svg b/src/assets/svgs/filter.svg new file mode 100644 index 0000000..7c0c37f --- /dev/null +++ b/src/assets/svgs/filter.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/halfStarRating.svg b/src/assets/svgs/halfStarRating.svg new file mode 100644 index 0000000..7d34f38 --- /dev/null +++ b/src/assets/svgs/halfStarRating.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/home.svg b/src/assets/svgs/home.svg new file mode 100644 index 0000000..f545be8 --- /dev/null +++ b/src/assets/svgs/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svgs/listen.svg b/src/assets/svgs/listen.svg new file mode 100644 index 0000000..08be7ac --- /dev/null +++ b/src/assets/svgs/listen.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/plus.svg b/src/assets/svgs/plus.svg new file mode 100644 index 0000000..d58bd09 --- /dev/null +++ b/src/assets/svgs/plus.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/refresh.svg b/src/assets/svgs/refresh.svg new file mode 100644 index 0000000..ead17a3 --- /dev/null +++ b/src/assets/svgs/refresh.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/remove.svg b/src/assets/svgs/remove.svg new file mode 100644 index 0000000..5fba10a --- /dev/null +++ b/src/assets/svgs/remove.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/required.svg b/src/assets/svgs/required.svg new file mode 100644 index 0000000..ad3ca57 --- /dev/null +++ b/src/assets/svgs/required.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/search.svg b/src/assets/svgs/search.svg new file mode 100644 index 0000000..1e3ff24 --- /dev/null +++ b/src/assets/svgs/search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/svgs/settings.svg b/src/assets/svgs/settings.svg new file mode 100644 index 0000000..3264547 --- /dev/null +++ b/src/assets/svgs/settings.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/ticket.svg b/src/assets/svgs/ticket.svg new file mode 100644 index 0000000..71aed3e --- /dev/null +++ b/src/assets/svgs/ticket.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svgs/trash.svg b/src/assets/svgs/trash.svg new file mode 100644 index 0000000..56bdba0 --- /dev/null +++ b/src/assets/svgs/trash.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/svgs/user.svg b/src/assets/svgs/user.svg new file mode 100644 index 0000000..19304c7 --- /dev/null +++ b/src/assets/svgs/user.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/Browse/ClacoPick/ClacoPickShow/index.tsx b/src/components/Browse/ClacoPick/ClacoPickShow/index.tsx new file mode 100644 index 0000000..c73ed43 --- /dev/null +++ b/src/components/Browse/ClacoPick/ClacoPickShow/index.tsx @@ -0,0 +1,16 @@ +import { ClacoPickShowProps } from "@/types"; + +export const ClacoPickShow = ({ imageSrc, title }: ClacoPickShowProps) => { + return ( +
+ {title} + + {title} + +
+ ); +}; diff --git a/src/components/Browse/ClacoPick/index.tsx b/src/components/Browse/ClacoPick/index.tsx new file mode 100644 index 0000000..7089899 --- /dev/null +++ b/src/components/Browse/ClacoPick/index.tsx @@ -0,0 +1,47 @@ +import { ClacoPickShow } from "./ClacoPickShow"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { useUserStore } from "@/libraries/store/user"; +import { ClacoPickProps } from "@/types"; +import { useNavigate } from "react-router-dom"; + +export const ClacoPick = ({ pickData }: ClacoPickProps) => { + const { nickname } = useUserStore(); + const navigate = useNavigate(); + + const gotoConcertDetail = (id: number) => { + navigate(`/show/${id}`); + }; + + return ( +
+
+ + 이런 공연은 어떠세요? + + + {nickname}님이 좋아할 만한 공연이에요 + +
+ +
+ + {pickData.map((pick, index) => ( + gotoConcertDetail(pick.id)} + > + + + ))} + +
+
+ ); +}; diff --git a/src/components/Browse/RecentConcertResult/index.tsx b/src/components/Browse/RecentConcertResult/index.tsx new file mode 100644 index 0000000..4f30dbc --- /dev/null +++ b/src/components/Browse/RecentConcertResult/index.tsx @@ -0,0 +1,44 @@ +import { ShowSummaryCard } from "@/components/common/ShowSummaryCard"; +import { GetConcertInfiniteResponse } from "@/types"; + +export type RecentConcertResultProps = { + concertData: GetConcertInfiniteResponse; + isFetchingNextPage: boolean; +}; + +export const RecentConcertResult = ({ + concertData, + isFetchingNextPage, +}: RecentConcertResultProps) => { + return ( + <> + + 총 {concertData?.pages[0].result.totalCount}개 + +
+ {concertData && + concertData.pages[0].result.listPageResponse.length === 0 ? ( +
검색결과없음
+ ) : ( + <> + {concertData && + concertData.pages.flatMap((page, pageIndex) => + page.result.listPageResponse.map((show, index) => ( + + )) + )} + {/* 추가 데이터 로드 */} + {isFetchingNextPage && ( +
+ 로딩 중... +
+ )} + + )} +
+ + ); +}; diff --git a/src/components/Browse/SearchResult/index.tsx b/src/components/Browse/SearchResult/index.tsx new file mode 100644 index 0000000..dac2062 --- /dev/null +++ b/src/components/Browse/SearchResult/index.tsx @@ -0,0 +1,60 @@ +import { GetConcertInfiniteResponse } from "@/types"; +import { ClacoPick } from "../ClacoPick"; +import { ShowSummaryCard } from "@/components/common/ShowSummaryCard"; + +export type SearchReultProps = { + searchData: GetConcertInfiniteResponse; + isFetchingNextPage: boolean; +}; + +export const SearchResult = ({ + searchData, + isFetchingNextPage, +}: SearchReultProps) => { + return ( + // 검색 이후 로직 + <> +
+ {searchData?.pages[0].result.totalCount === 0 ? ( +
+
+ + 찾으시는 공연 정보가 없어요 + + + 입력하신 단어가 정확한지 확인해주세요 + +
+ +
+ +
+
+ ) : ( + <> + + 총 {searchData?.pages[0].result.totalCount}개의 공연 + + {searchData && + searchData.pages.flatMap((page) => + page.result.listPageResponse.map((show) => ( + + )) + )} + {/* 추가 데이터 로드 */} + {isFetchingNextPage && ( +
+ 로딩 중... +
+ )} + + )} +
+ + ); +}; diff --git a/src/components/Browse/ShowFilter/ShowFeatureFilter/index.tsx b/src/components/Browse/ShowFilter/ShowFeatureFilter/index.tsx new file mode 100644 index 0000000..ade2baf --- /dev/null +++ b/src/components/Browse/ShowFilter/ShowFeatureFilter/index.tsx @@ -0,0 +1,22 @@ +interface ButtonProps extends React.ButtonHTMLAttributes { + isChecked: boolean; +} + +export const ShowFeatureFilter = ({ + isChecked, + children = "", + ...props +}: ButtonProps) => { + return ( + + ); +}; diff --git a/src/components/Browse/ShowFilter/index.tsx b/src/components/Browse/ShowFilter/index.tsx new file mode 100644 index 0000000..638597f --- /dev/null +++ b/src/components/Browse/ShowFilter/index.tsx @@ -0,0 +1,225 @@ +import { ReactComponent as Refresh } from "@/assets/svgs/refresh.svg"; +import { ReactComponent as X } from "@/assets/svgs/X-icon.svg"; +import { ConfirmButton } from "@/components/common/Button"; +import { Location } from "@/components/common/Location"; +import { Price } from "@/components/common/Price"; +import { useEffect, useState } from "react"; +import { ShowFeatureFilter } from "./ShowFeatureFilter"; +import { ShowFilterProps } from "@/types"; +import { Calendar } from "@/components/common/Calendar"; + +const features = [ + "웅장한", + "섬세한", + "친숙한", + "새로운", + "고전적인", + "현대적인", + "낭만적인", + "비극적인", + "서정적인", + "역동적인", +]; + +export const ShowFilter = ({ onClose, onApply }: ShowFilterProps) => { + const [minPrice, setMinPrice] = useState(0); + const [maxPrice, setMaxPrice] = useState(1000000); + const [selectedLocation, setSelectedLocation] = useState([]); + const [selectedFeatures, setSelectedFeatures] = useState([]); + const [rangeStart, setRangeStart] = useState(null); + const [rangeEnd, setRangeEnd] = useState(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const savedFilter = localStorage.getItem("filterObj"); + if (savedFilter) { + const parsedFilter = JSON.parse(savedFilter); + setMinPrice(parsedFilter.minPrice || 0); + setMaxPrice(parsedFilter.maxPrice || 1000000); + setSelectedLocation(parsedFilter.selectedLocation || []); + setSelectedFeatures(parsedFilter.categories || []); + setRangeStart( + parsedFilter.startDate ? new Date(parsedFilter.startDate) : null + ); + setRangeEnd(parsedFilter.endDate ? new Date(parsedFilter.endDate) : null); + } + }, []); + + useEffect(() => { + const timer = setTimeout(() => setIsVisible(true), 50); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + document.body.style.overflow = "hidden"; + + return () => { + document.body.style.overflow = "unset"; + }; + }, []); + + const handleClose = () => { + setIsVisible(false); + setTimeout(onClose, 1000); + }; + + const handleApply = () => { + const priceRange = `${minPrice}~${maxPrice}원`; + const location = selectedLocation.join(", "); + let dateRange = ""; + if (rangeStart && rangeEnd) { + const startYear = rangeStart.getFullYear(); + const startMonth = String(rangeStart.getMonth() + 1).padStart(2, "0"); + const startDate = String(rangeStart.getDate()).padStart(2, "0"); + + const endYear = rangeEnd.getFullYear(); + const endMonth = String(rangeEnd.getMonth() + 1).padStart(2, "0"); + const endDate = String(rangeEnd.getDate()).padStart(2, "0"); + + dateRange = `${startYear}.${startMonth}.${startDate}~${endYear}.${endMonth}.${endDate}`; + } + + const feature = selectedFeatures.join(", "); + setIsVisible(false); + setTimeout(() => onApply(priceRange, location, dateRange, feature), 300); + + const [startDate, endDate] = dateRange.split("~"); + const filterObj = { + minPrice: minPrice, + maxPrice: maxPrice, + selectedLocation: selectedLocation, + startDate: startDate, + endDate: endDate, + categories: selectedFeatures, + }; + // console.log(filterObj); + + localStorage.setItem("filterObj", JSON.stringify(filterObj)); + }; + + const handleLocationClick = (values: string[], label: string) => { + console.log(label); + if (values.every((value) => selectedLocation.includes(value))) { + setSelectedLocation( + selectedLocation.filter((loc) => !values.includes(loc)) + ); + } else { + setSelectedLocation([...selectedLocation, ...values]); + } + }; + + const handleFeatureClick = (feature: string) => { + if (selectedFeatures.includes(feature)) { + setSelectedFeatures(selectedFeatures.filter((item) => item !== feature)); + } else if (selectedFeatures.length < 5) { + setSelectedFeatures([...selectedFeatures, feature]); + } + }; + + const handleRefreshClick = () => { + setMinPrice(0); + setMaxPrice(1000000); + setSelectedLocation([]); + setSelectedFeatures([]); + setRangeStart(null); + setRangeEnd(null); + }; + + return ( +
+
+
+
+ + 필터 설정 + +
+
+ 가격 + +
+
+ + 공연장 위치 + + +
+
+ 공연 날짜 + { + setRangeStart(start); + setRangeEnd(end); + }} + /> +
+
+
+ 공연 특성 + + *최대 5개까지 선택 가능해요 + +
+
+ {features.map((feature) => ( + handleFeatureClick(feature)} + > + {feature} + + ))} +
+
+ + 적용하기 + +
+
+ ); +}; diff --git a/src/components/Layout/Footer/index.tsx b/src/components/Layout/Footer/index.tsx new file mode 100644 index 0000000..b21ab06 --- /dev/null +++ b/src/components/Layout/Footer/index.tsx @@ -0,0 +1,76 @@ +import { ReactComponent as Home } from "@/assets/svgs/home.svg"; +import { ReactComponent as Search } from "@/assets/svgs/search.svg"; +import { ReactComponent as Ticket } from "@/assets/svgs/ticket.svg"; +import { ReactComponent as User } from "@/assets/svgs/user.svg"; +import { useLocation, useNavigate } from "react-router-dom"; + +const CATEGORIES = [ + { + text: "홈", + image: , + path: "/main", + }, + { + text: "둘러보기", + image: , + path: "/browse", + }, + { + text: "티켓북", + image: , + path: "/ticketbook", + }, + { + text: "마이페이지", + image: , + path: "/mypage", + }, +]; + +const Footer = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const getActiveCategory = () => { + const currentCategory = CATEGORIES.find( + (category) => category.path === location.pathname + ); + return currentCategory?.text || CATEGORIES[0].text; + }; + + const handleFooter = (category: string) => { + const targetCategory = CATEGORIES.find((item) => item.text === category); + if (targetCategory) { + navigate(targetCategory.path); + } + }; + + return ( +
+
+ {CATEGORIES.map((category, index) => ( +
handleFooter(category.text)} + > + {category.image} + + {category.text} + +
+ ))} +
+
+ ); +}; + +export default Footer; diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx new file mode 100644 index 0000000..8ccd1b7 --- /dev/null +++ b/src/components/Layout/Header/index.tsx @@ -0,0 +1,5 @@ +const Header = () => { + return
; +}; + +export default Header; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx new file mode 100644 index 0000000..ccad798 --- /dev/null +++ b/src/components/Layout/index.tsx @@ -0,0 +1,15 @@ +import { Outlet } from "react-router-dom"; +import Header from "./Header"; +import Footer from "./Footer"; + +const Layout = () => { + return ( +
+
+ +
+
+ ); +}; + +export default Layout; diff --git a/src/components/Main/Analysis/AIRecommend/index.tsx b/src/components/Main/Analysis/AIRecommend/index.tsx new file mode 100644 index 0000000..e59fb05 --- /dev/null +++ b/src/components/Main/Analysis/AIRecommend/index.tsx @@ -0,0 +1,62 @@ +import { MainPosterCard } from "@/components/Main/MainPosterCard"; +import { Pagination } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/pagination"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { useUserStore } from "@/libraries/store/user"; +import { useEffect, useState } from "react"; +import { UserBased } from "@/types"; +import { useGetUserBased } from "@/hooks/queries"; +import { useDeferredLoading } from "@/hooks/utils"; +import { Skeleton } from "@/components/ui/skeleton"; + +export const AIRecommend = () => { + const [userBased, setUserBased] = useState([]); + const nickname = useUserStore((state) => state.nickname); + const { data, isLoading } = useGetUserBased(); + + useEffect(() => { + if (!isLoading && data?.result) { + setUserBased(data.result); + } + }, [isLoading, data]); + + const { shouldShowSkeleton } = useDeferredLoading(isLoading); + + if (shouldShowSkeleton) { + //skeleton UI 적용될 부분 + return ( +
+
+ + +
+ +
+ ); + } + + return ( +
+
+ {nickname}님만의 취향을 담은 +
+ 공연을 준비했어요 +
+
+ + {userBased.map((data) => ( + + + + ))} + +
+
+ ); +}; diff --git a/src/components/Main/Analysis/SimilarKeyWordRecommend/index.tsx b/src/components/Main/Analysis/SimilarKeyWordRecommend/index.tsx new file mode 100644 index 0000000..344cc6b --- /dev/null +++ b/src/components/Main/Analysis/SimilarKeyWordRecommend/index.tsx @@ -0,0 +1,110 @@ +import { HorizontalInfoCard } from "@/components/Main/InformationCard"; +import { Genre } from "@/components/common/Genre"; +import { useEffect, useState } from "react"; +import dynamic from "@/assets/images/Genre/dynamic.png"; +import romantic from "@/assets/images/Genre/romantic.png"; +import tragic from "@/assets/images/Genre/tragic.png"; +import { useUserStore } from "@/libraries/store/user"; +import { useGetItemBased } from "@/hooks/queries"; +import { UserItemBased } from "@/types"; + +const USER_GENRE = [ + { imgUrl: dynamic, keyWord: "역동적인" }, + { imgUrl: romantic, keyWord: "낭만적인" }, + { imgUrl: tragic, keyWord: "비극적인" }, +]; + +export const SimilarKeyWordRecommend = () => { + const [itemBased, setItemBased] = useState(); + const [currentIndex, setCurrentIndex] = useState(0); + const nickname = useUserStore((state) => state.nickname); + const { data, isLoading } = useGetItemBased(); + + useEffect(() => { + if (!isLoading && data?.result) { + // console.log(data.result); + setItemBased(data.result); + } + }, [isLoading, data]); + + useEffect(() => { + const rotateGenre = () => { + setCurrentIndex((prev) => (prev + 1) % USER_GENRE.length); + }; + + const interval = setInterval(rotateGenre, 3500); + return () => clearInterval(interval); + }, []); + + const getItemStyle = (index: number) => { + const baseStyle = + "absolute top-1/2 -translate-y-1/2 transition-all duration-500 ease-in-out"; + + const position = index - currentIndex; + + const normalizedPosition = + position < -1 ? position + 3 : position > 1 ? position - 3 : position; + + if (normalizedPosition === 0) { + return `${baseStyle} left-1/2 -translate-x-1/2 z-30 scale-100 opacity-100`; + } else if (normalizedPosition === 1) { + return `${baseStyle} left-[85%] -translate-x-1/2 z-20 scale-75 opacity-60`; + } else { + return `${baseStyle} left-[15%] -translate-x-1/2 z-10 scale-75 opacity-60`; + } + }; + + if (isLoading) { + //skeleton UI 적용될 부분 + return
로딩 중..
; + } + + return ( +
+
+
+
+ {itemBased?.likedHistory ? ( + <> + {nickname}님이 좋아요한 공연과
+ 비슷한 느낌의 공연도 확인해보세요 + + ) : ( + <> + 많은 사람들이 주목한
+ 클래식 공연도 확인해보세요 + + )} +
+
+
+ {itemBased?.keywords.map((item, index) => ( +
+ +
+ ))} +
+
+
+
+ {itemBased?.recommendationConcertsResponseV1s.map((concert) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/components/Main/Analysis/TicketRecommend/Reviews/index.tsx b/src/components/Main/Analysis/TicketRecommend/Reviews/index.tsx new file mode 100644 index 0000000..4c572c9 --- /dev/null +++ b/src/components/Main/Analysis/TicketRecommend/Reviews/index.tsx @@ -0,0 +1,28 @@ +import { useTruncateText } from "@/hooks/utils"; +import { ReactComponent as Chat } from "@/assets/svgs/chat.svg"; +import { TicketReviewSummary } from "@/types"; + +export type ReviewsProps = { + data: TicketReviewSummary; +}; + +export const Reviews = ({ data }: ReviewsProps) => { + return ( +
+
+ {data.concertName} +
+
+
+ +
+ {data.nickName}님의 리뷰 +
+
+
+ {useTruncateText(data.content, 64)} +
+
+
+ ); +}; diff --git a/src/components/Main/Analysis/TicketRecommend/index.tsx b/src/components/Main/Analysis/TicketRecommend/index.tsx new file mode 100644 index 0000000..53ee1b7 --- /dev/null +++ b/src/components/Main/Analysis/TicketRecommend/index.tsx @@ -0,0 +1,169 @@ +import { ReactComponent as Arrow } from "@/assets/svgs/Arrow 2.svg"; +import { TouchEvent, useEffect, useState } from "react"; +import { Reviews } from "./Reviews"; +import { TicketReviewSummary } from "@/types"; +import { useGetRecommendClacoTicket } from "@/hooks/queries"; +import { useNavigate } from "react-router-dom"; +export const TicketRecommend = () => { + const navigate = useNavigate(); + + const { data, isLoading, isError } = useGetRecommendClacoTicket(); + const [error, setError] = useState(false); + const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>( + null + ); + const [touchEnd, setTouchEnd] = useState<{ x: number; y: number } | null>( + null + ); + const [currentIndex, setCurrentIndex] = useState(0); + const [reviewContent, setReviewContent] = useState({ + nickName: "", + concertId: 0, + concertName: "", + content: "", + createdAt: "", + }); + const [isReviewVisible, setIsReviewVisible] = useState(true); + + useEffect(() => { + if (data) { + if (data.code === "COM-000" && data.result.length > 0) { + setError(false); + setReviewContent(data.result[0].ticketReviewSummary); + } else if (data.code === "CLB-001") { + // 클라코북을 찾을 수 없는 오류 + setError(true); + // window.confirm("알 수 없는 오류가 발생했습니다."); + // navigate("/"); + } else { + navigate("/"); + localStorage.clear(); + } + } + }, [data, navigate]); + + const handleTouchStart = (e: TouchEvent) => { + setTouchEnd(null); + setTouchStart({ + x: e.targetTouches[0].clientX, + y: e.targetTouches[0].clientY, + }); + }; + const handleTouchMove = (e: TouchEvent) => { + setTouchEnd({ + x: e.targetTouches[0].clientX, + y: e.targetTouches[0].clientY, + }); + }; + const handleTouchEnd = () => { + if (!touchStart || !touchEnd || !data) return; + const xDistance = touchStart.x - touchEnd.x; + const yDistance = Math.abs(touchStart.y - touchEnd.y); + if (yDistance > Math.abs(xDistance)) { + return; + } + const minSwipeDistance = 50; + const isLeftSwipe = xDistance > minSwipeDistance; + const isRightSwipe = xDistance < -minSwipeDistance; + if (!isLeftSwipe && !isRightSwipe) return; + setIsReviewVisible(false); + let nextIndex; + if (isLeftSwipe) { + nextIndex = (currentIndex + 1) % data.result.length; + } else if (isRightSwipe) { + nextIndex = (currentIndex - 1 + data.result.length) % data.result.length; + } else { + nextIndex = currentIndex; + } + setCurrentIndex(nextIndex); + setTimeout(() => { + setReviewContent(data.result[nextIndex].ticketReviewSummary); + setIsReviewVisible(true); + }, 400); + }; + const getItemStyle = (index: number) => { + const baseStyle = + "w-[232px] absolute top-1/2 -translate-y-1/2 transition-all duration-500 ease-in-out"; + const position = index - currentIndex; + const normalizedPosition = + position < -1 ? position + 3 : position > 1 ? position - 3 : position; + if (normalizedPosition === 0) { + return `${baseStyle} left-1/2 -translate-x-1/2 z-40 scale-100 opacity-100`; + } else if (normalizedPosition === 1) { + return `${baseStyle} left-[115%] -translate-x-1/2 z-20 scale-100 opacity-30`; + } else { + return `${baseStyle} left-[-15%] -translate-x-1/2 z-10 scale-100 opacity-30`; + } + }; + const handleGoToTicket = () => { + const currentTicketId = data?.result[currentIndex].ticketInfoResponse.id; + navigate(`/ticket/${currentTicketId}`); + }; + + // 로딩, 에러, 데이터 없음 상태 처리 + if ( + isLoading || + isError || + !data || + !data.result || + data.result.length === 0 + ) { + return ( +
+ 비슷한 취향을 가진 사람들의 정보를
+ 분석해주는 서비스를 준비중이에요.. +
+ ); + } + + // 클라코북을 찾을 수 없는 에러 발생 시 + if (error) { + return ( +
+ 비슷한 취향을 가진 사람들의 정보를
+ 분석해주는 서비스를 준비중이에요.. +
+ ); + } + + return ( +
+
+ 비슷한 취향을 가진 사람들의 공연 후기로
+ 클래식 경험의 폭을 넓혀보세요 +
+
+ {data?.result.map((ticket, index) => ( +
+ 클라코 티켓 이미지 +
+ ))} +
+
+ {/* 리뷰 영역 */} +
+ +
+
+ +
보러가기
+
+
+ ); +}; diff --git a/src/components/Main/Analysis/index.tsx b/src/components/Main/Analysis/index.tsx new file mode 100644 index 0000000..0cb8c7c --- /dev/null +++ b/src/components/Main/Analysis/index.tsx @@ -0,0 +1,13 @@ +import { AIRecommend } from "./AIRecommend"; +import { SimilarKeyWordRecommend } from "./SimilarKeyWordRecommend"; +import { TicketRecommend } from "./TicketRecommend"; + +export const Analysis = () => { + return ( +
+ + + +
+ ); +}; diff --git a/src/components/Main/ClassicalPalette/index.tsx b/src/components/Main/ClassicalPalette/index.tsx new file mode 100644 index 0000000..5d87061 --- /dev/null +++ b/src/components/Main/ClassicalPalette/index.tsx @@ -0,0 +1,17 @@ +import { ReactComponent as ClacoMain } from "@/assets/svgs/Claco_Main.svg"; +import { ReactComponent as Light } from "@/assets/svgs/Light.svg"; +import { ClacoAnalysisCard } from "@/components/common/ClacoAnalysisCard"; + +export const ClassicalPalette = () => { + return ( +
+
+ +
+ +
+ +
+
+ ); +}; diff --git a/src/components/Main/InformationCard/index.tsx b/src/components/Main/InformationCard/index.tsx new file mode 100644 index 0000000..b02cf36 --- /dev/null +++ b/src/components/Main/InformationCard/index.tsx @@ -0,0 +1,111 @@ +import { ReactComponent as Location_Gray } from "@/assets/svgs/Location_gray.svg"; +import { ReactComponent as Calendar } from "@/assets/svgs/Calendar.svg"; +import { InfoCardProps } from "@/types/poster"; +import { formatDateYYYYMMDD } from "@/hooks/utils"; +import { CategoryTag } from "@/components/common/CategoryTag"; +import { useNavigate } from "react-router-dom"; + +export const VerticalInfoCard = ({ + id, + image, + title, + location, + dateFrom, + dateTo, + genrenm, +}: InfoCardProps) => { + const _dateFrom = formatDateYYYYMMDD(dateFrom); + const _dateTo = formatDateYYYYMMDD(dateTo); + const navigate = useNavigate(); + const gotoShowDetail = () => { + navigate(`/show/${id}`); + }; + + return ( +
+
+ + +

+ {title} +

+
+
+ + {location} +
+
+ + + {_dateFrom === _dateTo ? ( + <>{_dateFrom} + ) : ( + <> + {_dateFrom}~{_dateTo} + + )} + +
+
+
+
+ ); +}; + +export const HorizontalInfoCard = ({ + id, + image, + title, + location, + dateFrom, + dateTo, + genrenm, +}: InfoCardProps) => { + const _dateFrom = formatDateYYYYMMDD(dateFrom); + const _dateTo = formatDateYYYYMMDD(dateTo); + + const navigate = useNavigate(); + const gotoShowDetail = () => { + navigate(`/show/${id}`); + }; + return ( +
+
+ +
+ +

+ {title} +

+
+
+ + {location} +
+
+ + + {_dateFrom === _dateTo ? ( + <>{_dateFrom} + ) : ( + <> + {_dateFrom}~{_dateTo} + + )} + +
+
+
+
+
+ ); +}; diff --git a/src/components/Main/MainPosterCard/index.tsx b/src/components/Main/MainPosterCard/index.tsx new file mode 100644 index 0000000..da1062a --- /dev/null +++ b/src/components/Main/MainPosterCard/index.tsx @@ -0,0 +1,112 @@ +import { ReactComponent as Heart } from "@/assets/svgs/Heart.svg"; +import { CategoryTag } from "@/components/common/CategoryTag"; +import { UserBased } from "@/types"; +import { useNavigate } from "react-router-dom"; +import { useState, useEffect, useRef } from "react"; +import { usePostLike } from "@/hooks/mutation"; + +export type MainPosterCardProps = { + data: UserBased; +}; + +export const MainPosterCard = ({ data }: MainPosterCardProps) => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [shouldLoad, setShouldLoad] = useState(false); + const posterRef = useRef(null); + + const { mutate: postLike } = usePostLike(); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !shouldLoad) { + setShouldLoad(true); + observer.unobserve(entry.target); + } + }, + { threshold: 0.1 } + ); + + const currentElement = posterRef.current; + if (currentElement) { + observer.observe(currentElement); + } + + return () => { + if (currentElement) { + observer.unobserve(currentElement); + } + }; + }, [shouldLoad]); + + const handleImageLoad = () => { + setIsLoading(false); + }; + + const gotoShowDetail = () => { + navigate(`/show/${data.id}`); + }; + + const handleLikeButton = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + postLike(data.id, { + onSuccess: () => {}, + onError: (error) => console.error(error), + }); + }; + + return ( +
+ + + {isLoading && ( +
+ )} + + poster + +
+
+
+ +
+ {data.prfnm} +
+
+
+
+ ); +}; diff --git a/src/components/Mypage/LikedShow/index.tsx b/src/components/Mypage/LikedShow/index.tsx new file mode 100644 index 0000000..a3a3d04 --- /dev/null +++ b/src/components/Mypage/LikedShow/index.tsx @@ -0,0 +1,79 @@ +import { SearchBar } from "@/components/common/Search/Bar"; +import { ShowFilterTab } from "@/components/common/ShowFilterTab"; +import { ShowSummaryCard } from "@/components/common/ShowSummaryCard"; +import { useEffect, useState } from "react"; +import { useDebouncedState, useDeferredLoading } from "@/hooks/utils"; +import { TabMenu } from "@/types"; +import { useGetConcertLikes } from "@/hooks/queries"; + +export const LikedShow = () => { + const [activeTab, setActiveTab] = useState(null); + const [query, setQuery] = useState(""); + const debouncedQuery = useDebouncedState(query, 1000); + + const { data, isLoading, refetch } = useGetConcertLikes({ + query: debouncedQuery, + genre: activeTab || "", + }); + + const { shouldShowSkeleton } = useDeferredLoading(isLoading); + + const handleSearchChange = (e: React.ChangeEvent) => { + setQuery(e.target.value.trim()); + }; + + const handleTabClick = (tab: TabMenu) => { + setActiveTab(tab); + refetch(); + }; + + useEffect(() => { + refetch(); + }, [debouncedQuery, activeTab, refetch]); + + if (shouldShowSkeleton) { + return ( +
+ +
+ +
+
+ {Array.from(Array(5).keys()).map((_, index) => ( + + ))} +
+
+ ); + } + + return ( +
+ +
+ +
+
+ {data?.result?.map((show) => ( + + ))} +
+
+ ); +}; diff --git a/src/components/Mypage/PreferenceAnalysis/index.tsx b/src/components/Mypage/PreferenceAnalysis/index.tsx new file mode 100644 index 0000000..04313bc --- /dev/null +++ b/src/components/Mypage/PreferenceAnalysis/index.tsx @@ -0,0 +1,24 @@ +import { ReactComponent as BackArrow } from "@/assets/svgs/BackArrow.svg"; +import { ClacoAnalysisCard } from "@/components/common/ClacoAnalysisCard"; +import { useNavigate } from "react-router-dom"; + +export const PreferenceAnalysis = () => { + const navigate = useNavigate(); + + const gotoPreference = () => { + navigate("/mypage/preference"); + }; + return ( +
+ +
+
+ 나의 취향 정보 수정 + +
+
+ ); +}; diff --git a/src/components/Onboarding/Registration/index.tsx b/src/components/Onboarding/Registration/index.tsx new file mode 100644 index 0000000..08e39d7 --- /dev/null +++ b/src/components/Onboarding/Registration/index.tsx @@ -0,0 +1,72 @@ +import { cn } from "@/lib/utils"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + isChecked: boolean; +} + +export const TypeButton = ({ + isChecked, + children, + className = "", + ...props +}: ButtonProps) => { + return ( + + ); +}; + +export const ConceptButton = ({ + isChecked, + children, + className = "", + ...props +}: ButtonProps) => { + return ( + + ); +}; + +export const FeatureButton = ({ + isChecked, + children, + className = "", + ...props +}: ButtonProps) => { + return ( + + ); +}; diff --git a/src/components/Review/ReviewCard/const/index.ts b/src/components/Review/ReviewCard/const/index.ts new file mode 100644 index 0000000..7a8d5df --- /dev/null +++ b/src/components/Review/ReviewCard/const/index.ts @@ -0,0 +1,146 @@ +import Profile from "@/assets/images/profile.png"; +import ReviewImage from "@/assets/images/review.png"; +import Review1 from "@/assets/images/poster1.gif"; +import Review2 from "@/assets/images/poster2.gif"; +import Review3 from "@/assets/images/poster3.webp"; +import Review4 from "@/assets/images/poster4.gif"; +import Review5 from "@/assets/images/poster5.gif"; +import Review6 from "@/assets/images/poster6.gif"; +import Review7 from "@/assets/images/poster7.gif"; +import { Review } from "@/types"; + +export const REVIEW_MOCK_DATA: Review[] = [ + { + ticketReviewId: 1, + userName: "울랄라", + profileImage: Profile, + createdDate: "2024.10.27", + watchSit: "9층 B열 14번", + starRate: 4.0, + content: + "크리스마스 밤, 클라라의 집에서 뭐가 벌어지는 것 같긴한데, 내용이 어떻게 될까요. 저는 모르겠습니다만 이렇게 두줄이 넘어가면 말줄임이 되어야 하지 않을까", + reviewImages: [{ imageUrl: Review6 }, { imageUrl: Review1 }], + placeReviews: [ + { placeCategoryId: 1, categoryName: "음향이 좋아요" }, + { placeCategoryId: 2, categoryName: "좌석 간격이 넓어요" }, + { placeCategoryId: 3, categoryName: "좌석이 편안해요" }, + { placeCategoryId: 4, categoryName: "시야가 탁 트여있어요" }, + { placeCategoryId: 5, categoryName: "주차가 편리해요" }, + ], + tagReviews: [ + { tagCategoryId: 1, tagName: "웅장한", iconUrl: "https://..." }, + { tagCategoryId: 2, tagName: "섬세한", iconUrl: "https://..." }, + ], + }, + { + ticketReviewId: 2, + userName: "발레러버", + profileImage: Profile, + createdDate: "2024.10.25", + watchSit: "1층 R열 23번", + starRate: 4.5, + content: + "발레단의 움직임이 정말 아름다웠어요. 특히 주인공의 독무 장면은 잊을 수 없을 것 같네요. 공연장의 분위기도 좋았고 객석에서 보이는 각도도 훌륭했습니다.", + reviewImages: [ + { imageUrl: ReviewImage }, + { imageUrl: Review4 }, + { imageUrl: Review5 }, + ], + placeReviews: [ + { placeCategoryId: 4, categoryName: "시야가 탁 트여있어요" }, + { placeCategoryId: 3, categoryName: "좌석이 편안해요" }, + { placeCategoryId: 1, categoryName: "음향이 좋아요" }, + { placeCategoryId: 5, categoryName: "주차가 편리해요" }, + ], + tagReviews: [ + { tagCategoryId: 2, tagName: "섬세한", iconUrl: "https://..." }, + { tagCategoryId: 3, tagName: "고전적인", iconUrl: "https://..." }, + ], + }, + { + ticketReviewId: 3, + userName: "공연매니아", + profileImage: Profile, + createdDate: "2024.10.23", + watchSit: "2층 C열 7번", + starRate: 3.5, + content: + "무용수들의 실력은 훌륭했지만, 좌석이 좀 불편했네요. 앞좌석과의 간격이 좁아서 무릎이 불편했어요. 하지만 공연 자체는 매우 인상적이었습니다.무용수들의 실력은 훌륭했지만, 좌석이 좀 불편했네요. 앞좌석과의 간격이 좁아서 무릎이 불편했어요. 하지만 공연 자체는 매우 인상적이었습니다.무용수들의 실력은 훌륭했지만, 좌석이 좀 불편했네요. 앞좌석과의 간격이 좁아서 무릎이 불편했어요. 하지만 공연 자체는 매우 인상적이었습니다.무용수들의 실력은 훌륭했지만, 좌석이 좀 불편했네요. 앞좌석과의 간격이 좁아서 무릎이 불편했어요. 하지만 공연 자체는 매우 인상적이었습니다.", + reviewImages: [], + placeReviews: [ + { placeCategoryId: 6, categoryName: "좌석 간격이 좁아요" }, + { placeCategoryId: 7, categoryName: "시야 확보가 어려워요" }, + { placeCategoryId: 1, categoryName: "음향이 좋아요" }, + { placeCategoryId: 8, categoryName: "주차 공간이 부족해요" }, + ], + tagReviews: [ + { tagCategoryId: 1, tagName: "웅장한", iconUrl: "https://..." }, + ], + }, + { + ticketReviewId: 4, + userName: "클래식팬", + profileImage: Profile, + createdDate: "2024.10.20", + watchSit: "VIP석 A열 12번", + starRate: 5.0, + content: + "오케스트라의 연주가 정말 완벽했어요! 음향 시설이 잘 되어있어서 섬세한 소리까지 잘 들렸고, VIP석이라 그런지 시야도 정말 좋았습니다. 다음에도 또 오고 싶네요.", + reviewImages: [ + { imageUrl: Review2 }, + { imageUrl: Review3 }, + { imageUrl: Review4 }, + ], + placeReviews: [ + { placeCategoryId: 1, categoryName: "음향이 좋아요" }, + { placeCategoryId: 4, categoryName: "시야가 탁 트여있어요" }, + { placeCategoryId: 3, categoryName: "좌석이 편안해요" }, + { placeCategoryId: 2, categoryName: "좌석 간격이 넓어요" }, + { placeCategoryId: 5, categoryName: "주차가 편리해요" }, + ], + tagReviews: [ + { tagCategoryId: 1, tagName: "웅장한", iconUrl: "https://..." }, + { tagCategoryId: 2, tagName: "섬세한", iconUrl: "https://..." }, + { tagCategoryId: 3, tagName: "고전적인", iconUrl: "https://..." }, + ], + }, + { + ticketReviewId: 5, + userName: "드라큘미", + profileImage: Profile, + createdDate: "2024.11.14", + watchSit: "VIP석 C열 8번", + starRate: 5.0, + content: + "크리스마스 밤, 클라라의 집에서 뭐가 벌어지는 것 같긴한데, 내용이 어떻게 될까요...(중략)...지금은 열시다", + reviewImages: [ + { imageUrl: Review4 }, + { imageUrl: Review2 }, + { imageUrl: Review7 }, + ], + placeReviews: [ + { placeCategoryId: 1, categoryName: "음향이 좋아요" }, + { placeCategoryId: 4, categoryName: "시야가 탁 트여있어요" }, + { placeCategoryId: 3, categoryName: "좌석이 편안해요" }, + { placeCategoryId: 2, categoryName: "좌석 간격이 넓어요" }, + { placeCategoryId: 5, categoryName: "주차가 편리해요" }, + ], + tagReviews: [ + { tagCategoryId: 1, tagName: "웅장한", iconUrl: "https://..." }, + { tagCategoryId: 5, tagName: "서정적인", iconUrl: "https://..." }, + ], + }, +]; + +/* +음향이 좋아요 +음향이 나빠요 +좌석 간격이 넓어요 +좌석 간격이 좁아요 +좌석이 편안해요 +좌석이 불편해요 +시야가 탁 트여있어요 +시야 확보가 어려워요 +주차가 편리해요 +주차 공간이 부족해요 +*/ diff --git a/src/components/Review/ReviewCard/index.tsx b/src/components/Review/ReviewCard/index.tsx new file mode 100644 index 0000000..ba4841a --- /dev/null +++ b/src/components/Review/ReviewCard/index.tsx @@ -0,0 +1,94 @@ +import { ReactComponent as BackArrow } from "@/assets/svgs/BackArrow.svg"; +import { ReactComponent as Star } from "@/assets/svgs/StarRating.svg"; +import { useTruncateText } from "@/hooks/utils"; +import { ReviewTag } from "@/components/common/ReviewTag"; +import { useNavigate } from "react-router-dom"; +import { ReviewCardProps } from "@/types"; +import { Skeleton } from "@/components/ui/skeleton"; + +export const ReviewCard = ({ review, onClick }: ReviewCardProps) => { + const SHOW_LENGTH = review.reviewImages.length === 0 ? 90 : 60; + + const navigate = useNavigate(); + const gotoReviewDetail = (id: number) => { + navigate(`/show/1/reviews/${id}`); + }; + + return ( +
+
+
+ 프로필 이미지 +
{review.userName}
+
+
+ +
+ {review.reviewImages.length === 0 ? null : ( +
+ 공연 이미지 +
+ {review.reviewImages.length} +
+
+ )} + +
+
+ +
+ {review.starRate} +
+
+
+ {useTruncateText(review.content, SHOW_LENGTH)} +
+
+
+ +
+
공연장
+
+ {review.watchSit} +
+
+ +
+ {review.placeReviews.map((lReview) => ( + + {lReview.categoryName} + + ))} +
+ +
+
+ {review.createdDate.replace(/-/g, ".")} +
+
+
gotoReviewDetail(review.ticketReviewId)}> + 더 보기 +
+ +
+
+
+ ); +}; + +ReviewCard.Skeleton = () => { + return ; +}; diff --git a/src/components/ShowDetail/AudienceReviews/ReviewSummaryCard/index.tsx b/src/components/ShowDetail/AudienceReviews/ReviewSummaryCard/index.tsx new file mode 100644 index 0000000..1d98076 --- /dev/null +++ b/src/components/ShowDetail/AudienceReviews/ReviewSummaryCard/index.tsx @@ -0,0 +1,41 @@ +import { ReactComponent as StarRating } from "@/assets/svgs/StarRating.svg"; +import { TicketSimpleReview } from "@/types"; +import { useNavigate, useParams } from "react-router-dom"; + +export const ReviewSummaryCard = ({ + ticketReviewId, + nickname, + starRate, + content, +}: TicketSimpleReview) => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const gotoShowReivewDetail = () => { + navigate(`/show/${id}/reviews/${ticketReviewId}`); + }; + return ( +
+
+
+ + {nickname} + + + + {starRate.toFixed(1)} + +
+
+ 더 보기 +
+
+ + + {content} + +
+ ); +}; diff --git a/src/components/ShowDetail/AudienceReviews/index.tsx b/src/components/ShowDetail/AudienceReviews/index.tsx new file mode 100644 index 0000000..2cd5e7c --- /dev/null +++ b/src/components/ShowDetail/AudienceReviews/index.tsx @@ -0,0 +1,65 @@ +import { Swiper, SwiperSlide } from "swiper/react"; +import "swiper/css"; +import { forwardRef } from "react"; +import { ReviewSummaryCard } from "./ReviewSummaryCard"; +import { useNavigate, useParams } from "react-router-dom"; +import { TicketSimpleReview } from "@/types"; + +export type AudienceReviewsProps = { + reviews: TicketSimpleReview[]; +}; + +const AudienceReviews = forwardRef( + ({ reviews }, ref) => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const gotoShowReview = () => { + navigate(`/show/${id}/reviews`); + }; + return ( +
+
+
+ 감상 리뷰 +
+ 전체 보기 +
+
+ + {reviews.length > 0 ? ( + + {reviews.map((review, index) => ( + + + + ))} + + ) : ( +
+

+ 아직 등록된 리뷰가 없어요 +

+
+ )} +
+
+ ); + }, +); + +export default AudienceReviews; diff --git a/src/components/ShowDetail/RelatedShowRecommend/index.tsx b/src/components/ShowDetail/RelatedShowRecommend/index.tsx new file mode 100644 index 0000000..5c02002 --- /dev/null +++ b/src/components/ShowDetail/RelatedShowRecommend/index.tsx @@ -0,0 +1,56 @@ +import { Swiper, SwiperSlide } from "swiper/react"; +import { VerticalInfoCard } from "@/components/Main/InformationCard"; +import { forwardRef, useEffect, useState } from "react"; +import { ConcertBased } from "@/types"; +import { useGetConcertBased } from "@/hooks/queries"; +import { useParams } from "react-router-dom"; + +const RelatedShowsRecommend = forwardRef((_, ref) => { + const [concertBased, setConcertBased] = useState([]); + const { id } = useParams<{ id: string }>(); + const { data, isLoading } = useGetConcertBased(Number(id)); + + useEffect(() => { + if (!isLoading && data?.result) { + setConcertBased(data.result); + } + }, [isLoading, data]); + + if (isLoading) { + return
로딩 중..
; + } + return ( +
+
+ + 이 공연도 마음에 들 거예요! + +
+ + {concertBased.map((concert, index) => ( + + + + ))} + +
+
+
+ ); +}); + +export default RelatedShowsRecommend; diff --git a/src/components/ShowDetail/ShowInformation/ShowEssentials/index.tsx b/src/components/ShowDetail/ShowInformation/ShowEssentials/index.tsx new file mode 100644 index 0000000..278900b --- /dev/null +++ b/src/components/ShowDetail/ShowInformation/ShowEssentials/index.tsx @@ -0,0 +1,87 @@ +import { forwardRef } from "react"; + +export type PrfGuidance = { + day: string; + times: string[]; +}; + +export type ShowEssentialsProps = { + fcltynm: string; + prfruntime: string; + prfdate: string; + dtguidance: PrfGuidance[]; + seats: string[]; + prices: string[]; +}; + +const ShowEssentials = forwardRef( + ({ fcltynm, prfruntime, prfdate, dtguidance, seats, prices }, ref) => { + return ( +
+
+ 공연 정보 + +
+
+ 기간 + {prfdate} +
+ +
+ 공연장소 + {fcltynm} +
+ +
+ 러닝타임 + + {prfruntime} + +
+ +
+ 공연시간 +
+ + *해당하는 요일과 시간에만 공연이 진행돼요 + + {dtguidance.map(({ day, times }: PrfGuidance) => ( +
+ + {day} + +
+
+ {times.map((time: string, index: number) => ( + {time} + ))} +
+
+ ))} +
+
+ +
+ 가격 +
+ {seats.map((seat, index) => ( +
+ + {seat} + +
+ + {prices[index]} + +
+ ))} +
+
+
+
+
+ ); + }, +); + +export default ShowEssentials; diff --git a/src/components/ShowDetail/ShowInformation/ShowOverview/index.tsx b/src/components/ShowDetail/ShowInformation/ShowOverview/index.tsx new file mode 100644 index 0000000..7e5744a --- /dev/null +++ b/src/components/ShowDetail/ShowInformation/ShowOverview/index.tsx @@ -0,0 +1,190 @@ +import { ReactComponent as ClacoMain } from "@/assets/svgs/Claco_Main.svg"; +import { ReactComponent as BackArrow } from "@/assets/svgs/BackArrow.svg"; +import { CategoryTag } from "@/components/common/CategoryTag"; +import { ReactComponent as Heart } from "@/assets/svgs/Heart.svg"; +import { ReactComponent as Megaphone } from "@/assets/svgs/Megaphone.svg"; +import { ReactComponent as Book } from "@/assets/svgs/Book.svg"; +import { Genre } from "@/components/common/Genre"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect, useRef, useState } from "react"; +import { ShowCategory } from "@/types"; +import { usePostLike } from "@/hooks/mutation"; +import { useConcertInfoStore } from "@/libraries/store/concertInfo"; + +export type ShowOverViewProps = { + prfstate: string; + prfprice: string; + genrenm: string; + prfnm: string; + poster: string; + area: string; + prfruntime: string; + prfage: string; + prfdate: string; + summary: string; + categories: ShowCategory[]; + liked: boolean; +}; + +const ShowOverview = ({ + prfstate, + prfage, + prfruntime, + genrenm, + prfnm, + poster, + area, + prfdate, + prfprice, + summary, + categories, + liked, +}: ShowOverViewProps) => { + const { setConcertInfo } = useConcertInfoStore(); + useEffect(() => { + setConcertInfo({ + genrenm, + prfstate, + prfnm, + }); + }, [genrenm, prfstate, prfnm, setConcertInfo]); + + const navigate = useNavigate(); + const [_liked, setLiked] = useState(liked); + const [isExpanded, setIsExpanded] = useState(false); + const [showButton, setShowButton] = useState(false); + const textRef = useRef(null); + const mutation = usePostLike(); + const { id } = useParams<{ id: string }>(); + + useEffect(() => { + if (textRef.current) { + const height = textRef.current.getBoundingClientRect().height; + + setShowButton(height > 40); + } + }, [summary]); + + const gotoBack = () => { + navigate(-1); + }; + + const handleLike = () => { + setLiked((prev) => !prev); + mutation.mutate(Number(id)); + }; + + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + return ( +
+ + + + +
+ + +
+ + {prfnm} + +
+
+
+
+ +
+ poster +
+
+
+
+ 장소 + 기간 + 시간 + 연령 + 가격 +
+
+ {area} + {prfdate} + {prfruntime} + {prfage} + {prfprice} +
+
+
+ 예매하러가기 +
+
+
+ +
+
+ + Claco 쉬운 공연 설명 +
+ +
+

+ {summary || ""} +

+ + {showButton && ( + + )} +
+
+ + 이런 느낌의 공연이에요 +
+
+ {categories?.map((item, index) => ( + + ))} +
+
+
+ ); +}; + +export default ShowOverview; diff --git a/src/components/ShowDetail/ShowInformation/ShowPoster/index.tsx b/src/components/ShowDetail/ShowInformation/ShowPoster/index.tsx new file mode 100644 index 0000000..fd78c38 --- /dev/null +++ b/src/components/ShowDetail/ShowInformation/ShowPoster/index.tsx @@ -0,0 +1,62 @@ +import { ReactComponent as BackArrow } from "@/assets/svgs/BackArrow.svg"; +import { forwardRef, useRef, useState } from "react"; + +export type DetailsInfoSectionProps = { + showFullImage: boolean; + setShowFullImage: (show: boolean) => void; + styurl: string; +}; + +const ShowPoster = forwardRef( + ({ showFullImage, setShowFullImage, styurl }, ref) => { + const [isImageOverflow, setIsImageOverflow] = useState(false); + const imageRef = useRef(null); + + const checkImageHeight = () => { + if (imageRef.current) { + const imageHeight = imageRef.current.naturalHeight; + setIsImageOverflow(imageHeight > 4000); + } + }; + + const toggleFullImageView = () => { + setShowFullImage(!showFullImage); + }; + + return ( +
+
+ 상세정보 +
+
+ poster details +
+ {isImageOverflow && ( + + )} +
+
+
+ ); + }, +); + +export default ShowPoster; diff --git a/src/components/Ticket/AudienceReview/KeywordTags/index.tsx b/src/components/Ticket/AudienceReview/KeywordTags/index.tsx new file mode 100644 index 0000000..bf2dc4c --- /dev/null +++ b/src/components/Ticket/AudienceReview/KeywordTags/index.tsx @@ -0,0 +1,40 @@ +import { ReviewTag } from "@/components/common/ReviewTag"; +import { KeywordTagProps } from "@/types"; + +export const KeywordTags = ({ + selectedTags, + onTagClick, + tagCategories, +}: KeywordTagProps) => { + const handleClick = (tag: string) => { + if (selectedTags.includes(tag)) { + onTagClick(tag); + } else if (selectedTags.length < 5) { + onTagClick(tag); + } + }; + + return ( +
+ + 공연 성격에 해당되는 키워드를 5개 선택해주세요 + +
+ {[0, 4, 8].map((startIndex) => ( +
+ {tagCategories?.slice(startIndex, startIndex + 4).map((tag) => ( + handleClick(tag.tagName)} + isSelected={selectedTags.includes(tag.tagName)} + isPlace={true} + > + {tag.tagName} + + ))} +
+ ))} +
+
+ ); +}; diff --git a/src/components/Ticket/AudienceReview/ReviewContents/ImageReview/index.tsx b/src/components/Ticket/AudienceReview/ReviewContents/ImageReview/index.tsx new file mode 100644 index 0000000..7f1570e --- /dev/null +++ b/src/components/Ticket/AudienceReview/ReviewContents/ImageReview/index.tsx @@ -0,0 +1,51 @@ +import { ReactComponent as Plus } from "@/assets/svgs/plus.svg"; +import { ImageReviewProps } from "@/types"; + +export const ImageReview = ({ files, onFileChange }: ImageReviewProps) => { + return ( +
+ + 사진 또는 비디오를 첨부해
+ 보다 생생한 리뷰를 공유해주세요. +
+
+ {files.length < 3 && ( + + )} + + {files.map((file, index) => ( +
+ {file.type.startsWith("image") ? ( + preview + ) : ( +
+ ))} +
+
+ ); +}; diff --git a/src/components/Ticket/AudienceReview/ReviewContents/TextReview/index.tsx b/src/components/Ticket/AudienceReview/ReviewContents/TextReview/index.tsx new file mode 100644 index 0000000..3fef519 --- /dev/null +++ b/src/components/Ticket/AudienceReview/ReviewContents/TextReview/index.tsx @@ -0,0 +1,32 @@ +import { ReactComponent as Required } from "@/assets/svgs/required.svg"; +import { TextReviewProps } from "@/types"; + +export const TextReview = ({ value, onChange }: TextReviewProps) => { + const handleChange = (e: React.ChangeEvent) => { + const text = e.target.value; + if (text.length <= 500) { + onChange(text); + } + }; + return ( +
+
+ + 감상평을 남겨주세요 + + +
+
+