YouTube 섹션 스타일 개선 및 문서 업데이트
- PC/모바일 YouTube 상세 페이지 그림자 감소, 배경색 조정 - 모바일 YouTube 섹션을 카드에서 배경 스타일로 변경 - 문서 업데이트: 완료된 마이그레이션 작업 반영, 누락된 API 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bdd5b90870
commit
9eb49929c7
6 changed files with 169 additions and 61 deletions
47
docs/api.md
47
docs/api.md
|
|
@ -7,8 +7,8 @@ Base URL: `/api`
|
||||||
### POST /auth/login
|
### POST /auth/login
|
||||||
로그인 (JWT 토큰 발급)
|
로그인 (JWT 토큰 발급)
|
||||||
|
|
||||||
### GET /auth/me
|
### GET /auth/verify
|
||||||
현재 사용자 정보 (인증 필요)
|
토큰 검증 및 사용자 정보 (인증 필요)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -149,6 +149,49 @@ Meilisearch 전체 동기화 (인증 필요)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### GET /schedules/suggestions/popular
|
||||||
|
인기 검색어 조회
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `limit` - 결과 개수 (기본 10)
|
||||||
|
|
||||||
|
**응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"queries": ["프로미스나인", "송하영", "이서연"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /schedules/suggestions/save
|
||||||
|
검색어 저장 (검색 실행 시 호출)
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "검색어"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /schedules/suggestions/dict
|
||||||
|
사용자 사전 조회 (인증 필요)
|
||||||
|
|
||||||
|
**응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "프로미스나인\t프로미스나인\tNNP\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PUT /schedules/suggestions/dict
|
||||||
|
사용자 사전 저장 (인증 필요)
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "프로미스나인\t프로미스나인\tNNP\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 관리자 - 봇 관리 (인증 필요)
|
## 관리자 - 봇 관리 (인증 필요)
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,22 @@ fromis_9/
|
||||||
│ │ │ ├── meilisearch.js # 검색 엔진
|
│ │ │ ├── meilisearch.js # 검색 엔진
|
||||||
│ │ │ └── scheduler.js # 봇 스케줄러
|
│ │ │ └── scheduler.js # 봇 스케줄러
|
||||||
│ │ ├── routes/ # API 라우트
|
│ │ ├── routes/ # API 라우트
|
||||||
│ │ │ ├── auth/
|
│ │ │ ├── admin/ # 관리자 API
|
||||||
│ │ │ ├── members/
|
│ │ │ │ ├── bots.js # 봇 관리
|
||||||
|
│ │ │ │ ├── youtube.js # YouTube 일정 관리
|
||||||
|
│ │ │ │ └── x.js # X 일정 관리
|
||||||
│ │ │ ├── albums/
|
│ │ │ ├── albums/
|
||||||
|
│ │ │ │ ├── index.js # 앨범 CRUD
|
||||||
|
│ │ │ │ ├── photos.js # 앨범 사진 관리
|
||||||
|
│ │ │ │ └── teasers.js # 앨범 티저 관리
|
||||||
|
│ │ │ ├── auth.js # 인증 (로그인, 토큰 검증)
|
||||||
|
│ │ │ ├── members/
|
||||||
|
│ │ │ │ └── index.js # 멤버 조회/수정
|
||||||
│ │ │ ├── schedules/
|
│ │ │ ├── schedules/
|
||||||
│ │ │ │ ├── index.js # 일정 조회/검색
|
│ │ │ │ ├── index.js # 일정 조회/검색/삭제
|
||||||
│ │ │ │ └── suggestions.js
|
│ │ │ │ └── suggestions.js # 추천 검색어
|
||||||
|
│ │ │ ├── stats/
|
||||||
|
│ │ │ │ └── index.js # 통계 조회
|
||||||
│ │ │ └── index.js # 라우트 등록
|
│ │ │ └── index.js # 라우트 등록
|
||||||
│ │ ├── services/ # 비즈니스 로직
|
│ │ ├── services/ # 비즈니스 로직
|
||||||
│ │ │ ├── youtube/ # YouTube 봇
|
│ │ │ ├── youtube/ # YouTube 봇
|
||||||
|
|
@ -40,7 +50,18 @@ fromis_9/
|
||||||
│ │ ├── api/ # API 클라이언트
|
│ │ ├── api/ # API 클라이언트
|
||||||
│ │ │ ├── index.js # fetchApi 유틸
|
│ │ │ ├── index.js # fetchApi 유틸
|
||||||
│ │ │ ├── public/ # 공개 API
|
│ │ │ ├── public/ # 공개 API
|
||||||
|
│ │ │ │ ├── albums.js
|
||||||
|
│ │ │ │ ├── members.js
|
||||||
|
│ │ │ │ └── schedules.js
|
||||||
│ │ │ └── admin/ # 어드민 API
|
│ │ │ └── admin/ # 어드민 API
|
||||||
|
│ │ │ ├── albums.js
|
||||||
|
│ │ │ ├── auth.js
|
||||||
|
│ │ │ ├── bots.js
|
||||||
|
│ │ │ ├── categories.js
|
||||||
|
│ │ │ ├── members.js
|
||||||
|
│ │ │ ├── schedules.js
|
||||||
|
│ │ │ ├── stats.js
|
||||||
|
│ │ │ └── suggestions.js
|
||||||
│ │ ├── components/ # 공통 컴포넌트
|
│ │ ├── components/ # 공통 컴포넌트
|
||||||
│ │ │ └── common/
|
│ │ │ └── common/
|
||||||
│ │ │ ├── Lightbox.jsx # 이미지 라이트박스 (PC)
|
│ │ │ ├── Lightbox.jsx # 이미지 라이트박스 (PC)
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,35 @@ docker exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
## 프론트엔드 개발 가이드
|
## 프론트엔드 개발 가이드
|
||||||
|
|
||||||
|
### API 클라이언트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
src/api/
|
||||||
|
├── index.js # fetchApi 유틸 (에러 처리, 토큰 주입)
|
||||||
|
├── public/ # 공개 API (인증 불필요)
|
||||||
|
│ ├── albums.js # getAlbums, getAlbumByName, getTrack
|
||||||
|
│ ├── members.js # getMembers
|
||||||
|
│ └── schedules.js # getSchedules, getSchedule, getCategories
|
||||||
|
└── admin/ # 관리자 API (인증 필요)
|
||||||
|
├── auth.js # login, verifyToken
|
||||||
|
├── albums.js # createAlbum, updateAlbum, deleteAlbum, ...
|
||||||
|
├── bots.js # getBots, startBot, stopBot, syncBot
|
||||||
|
├── categories.js # getCategories
|
||||||
|
├── members.js # updateMember
|
||||||
|
├── schedules.js # getYoutubeInfo, saveYoutube, getXInfo, saveX, ...
|
||||||
|
├── stats.js # getStats
|
||||||
|
└── suggestions.js # getDict, saveDict
|
||||||
|
```
|
||||||
|
|
||||||
|
**사용 예시:**
|
||||||
|
```jsx
|
||||||
|
// 공개 API
|
||||||
|
import { getSchedules, getSchedule } from '@/api/public/schedules';
|
||||||
|
|
||||||
|
// 관리자 API
|
||||||
|
import { getBots, startBot } from '@/api/admin/bots';
|
||||||
|
```
|
||||||
|
|
||||||
### React Query 사용 (데이터 페칭)
|
### React Query 사용 (데이터 페칭)
|
||||||
|
|
||||||
데이터 페칭 시 `useEffect` 대신 `useQuery`를 사용합니다.
|
데이터 페칭 시 `useEffect` 대신 `useQuery`를 사용합니다.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## 개요
|
## 개요
|
||||||
|
|
||||||
`backend-backup/` (Express) → `backend/` (Fastify)로 마이그레이션 진행 중
|
`backend-backup/` (Express) → `backend/` (Fastify)로 마이그레이션 완료
|
||||||
|
|
||||||
## 완료된 작업
|
## 완료된 작업
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
### API 라우트 (`src/routes/`)
|
### API 라우트 (`src/routes/`)
|
||||||
- [x] 인증 (`/api/auth`)
|
- [x] 인증 (`/api/auth`)
|
||||||
- POST /login - 로그인
|
- POST /login - 로그인
|
||||||
- GET /me - 현재 사용자 정보
|
- GET /verify - 토큰 검증
|
||||||
- [x] 멤버 (`/api/members`)
|
- [x] 멤버 (`/api/members`)
|
||||||
- GET / - 목록 조회
|
- GET / - 목록 조회
|
||||||
- GET /:name - 상세 조회
|
- GET /:name - 상세 조회
|
||||||
|
|
@ -40,19 +40,37 @@
|
||||||
- GET / - 목록
|
- GET / - 목록
|
||||||
- POST / - 업로드
|
- POST / - 업로드
|
||||||
- DELETE /:teaserId - 삭제
|
- DELETE /:teaserId - 삭제
|
||||||
- [x] 일정 (`/api/schedules`) - 조회만
|
- [x] 일정 (`/api/schedules`)
|
||||||
- GET / - 월별 조회 (생일 포함)
|
- GET / - 월별 조회 (생일 포함)
|
||||||
- GET /?search= - Meilisearch 검색
|
- GET /?search= - Meilisearch 검색
|
||||||
- GET /:id - 상세 조회
|
- GET /:id - 상세 조회
|
||||||
|
- DELETE /:id - 삭제
|
||||||
- POST /sync-search - Meilisearch 동기화
|
- POST /sync-search - Meilisearch 동기화
|
||||||
- [x] 추천 검색어 (`/api/schedules/suggestions`)
|
- [x] 추천 검색어 (`/api/schedules/suggestions`)
|
||||||
- GET / - 추천 검색어 조회
|
- GET / - 추천 검색어 조회
|
||||||
- kiwi-nlp 형태소 분석
|
- GET /popular - 인기 검색어 조회
|
||||||
- bi-gram 자동완성
|
- POST /save - 검색어 저장
|
||||||
- 초성 검색
|
- GET /dict - 사용자 사전 조회 (관리자)
|
||||||
|
- PUT /dict - 사용자 사전 저장 (관리자)
|
||||||
- [x] 통계 (`/api/stats`)
|
- [x] 통계 (`/api/stats`)
|
||||||
- GET / - 대시보드 통계
|
- GET / - 대시보드 통계
|
||||||
|
|
||||||
|
### 관리자 API (`src/routes/admin/`)
|
||||||
|
- [x] 봇 관리 (`/api/admin/bots`)
|
||||||
|
- GET / - 봇 목록
|
||||||
|
- POST /:id/start - 봇 시작
|
||||||
|
- POST /:id/stop - 봇 정지
|
||||||
|
- POST /:id/sync-all - 전체 동기화
|
||||||
|
- GET /quota-warning - 할당량 경고 조회
|
||||||
|
- DELETE /quota-warning - 할당량 경고 해제
|
||||||
|
- [x] YouTube 관리 (`/api/admin/youtube`)
|
||||||
|
- GET /video-info - 영상 정보 조회
|
||||||
|
- POST /schedule - 일정 저장
|
||||||
|
- PUT /schedule/:id - 일정 수정
|
||||||
|
- [x] X 관리 (`/api/admin/x`)
|
||||||
|
- GET /post-info - 게시글 정보 조회
|
||||||
|
- POST /schedule - 일정 저장
|
||||||
|
|
||||||
### 서비스 (`src/services/`)
|
### 서비스 (`src/services/`)
|
||||||
- [x] YouTube 봇 (`services/youtube/`)
|
- [x] YouTube 봇 (`services/youtube/`)
|
||||||
- 영상 자동 수집
|
- 영상 자동 수집
|
||||||
|
|
@ -64,41 +82,33 @@
|
||||||
- 일정 검색
|
- 일정 검색
|
||||||
- 전체 동기화
|
- 전체 동기화
|
||||||
- [x] 추천 검색어 (`services/suggestions/`)
|
- [x] 추천 검색어 (`services/suggestions/`)
|
||||||
- 형태소 분석
|
- 형태소 분석 (kiwi-nlp)
|
||||||
- bi-gram 빈도
|
- bi-gram 빈도
|
||||||
|
- 초성 검색
|
||||||
|
- 사용자 사전 관리
|
||||||
- [x] 이미지 업로드 (`services/image.js`)
|
- [x] 이미지 업로드 (`services/image.js`)
|
||||||
- 앨범 커버
|
- 앨범 커버
|
||||||
- 멤버 이미지
|
- 멤버 이미지
|
||||||
- 앨범 사진/티저
|
- 앨범 사진/티저
|
||||||
|
|
||||||
## 남은 작업
|
## 남은 작업 (미구현)
|
||||||
|
|
||||||
### 관리자 API (admin.js에서 마이그레이션 필요)
|
### 일반 일정 CRUD
|
||||||
- [ ] 일정 CRUD
|
- [ ] POST /api/schedules - 일정 생성 (일반)
|
||||||
- POST /api/schedules - 생성
|
- [ ] PUT /api/schedules/:id - 일정 수정 (일반)
|
||||||
- PUT /api/schedules/:id - 수정
|
|
||||||
- DELETE /api/schedules/:id - 삭제
|
|
||||||
- [ ] 일정 카테고리 CRUD
|
|
||||||
- GET /api/schedule-categories - 목록
|
|
||||||
- POST /api/schedule-categories - 생성
|
|
||||||
- PUT /api/schedule-categories/:id - 수정
|
|
||||||
- DELETE /api/schedule-categories/:id - 삭제
|
|
||||||
- PUT /api/schedule-categories-order - 순서 변경
|
|
||||||
- [ ] 봇 관리 API
|
|
||||||
- GET /api/bots - 봇 목록
|
|
||||||
- POST /api/bots/:id/start - 봇 시작
|
|
||||||
- POST /api/bots/:id/stop - 봇 정지
|
|
||||||
- POST /api/bots/:id/sync-all - 전체 동기화
|
|
||||||
- [ ] 카카오 장소 검색 프록시
|
|
||||||
- GET /api/kakao/places - 장소 검색
|
|
||||||
- [ ] YouTube 할당량 관리
|
|
||||||
- POST /api/quota-alert - Webhook 수신
|
|
||||||
- GET /api/quota-warning - 경고 상태 조회
|
|
||||||
- DELETE /api/quota-warning - 경고 해제
|
|
||||||
|
|
||||||
### 기타 기능
|
※ 현재는 YouTube/X 전용 일정 생성 API만 구현됨
|
||||||
- [ ] X 프로필 조회 (`/api/schedules/x-profile/:username`)
|
|
||||||
- [ ] 어드민 사전 관리 (형태소 분석용 사전)
|
### 카테고리 관리
|
||||||
|
- [ ] POST /api/schedule-categories - 생성
|
||||||
|
- [ ] PUT /api/schedule-categories/:id - 수정
|
||||||
|
- [ ] DELETE /api/schedule-categories/:id - 삭제
|
||||||
|
- [ ] PUT /api/schedule-categories-order - 순서 변경
|
||||||
|
|
||||||
|
※ GET은 구현됨 (목록 조회)
|
||||||
|
|
||||||
|
### 기타
|
||||||
|
- [ ] GET /api/kakao/places - 카카오 장소 검색 프록시
|
||||||
|
|
||||||
## 파일 비교표
|
## 파일 비교표
|
||||||
|
|
||||||
|
|
@ -108,14 +118,17 @@
|
||||||
| routes/admin.js (앨범 CRUD) | routes/albums/index.js | 완료 |
|
| routes/admin.js (앨범 CRUD) | routes/albums/index.js | 완료 |
|
||||||
| routes/admin.js (사진/티저) | routes/albums/photos.js, teasers.js | 완료 |
|
| routes/admin.js (사진/티저) | routes/albums/photos.js, teasers.js | 완료 |
|
||||||
| routes/admin.js (멤버 수정) | routes/members/index.js | 완료 |
|
| routes/admin.js (멤버 수정) | routes/members/index.js | 완료 |
|
||||||
| routes/admin.js (일정 CRUD) | - | 미완료 |
|
| routes/admin.js (일정 삭제) | routes/schedules/index.js | 완료 |
|
||||||
| routes/admin.js (카테고리) | - | 미완료 |
|
| routes/admin.js (일정 생성/수정) | - | 미완료 |
|
||||||
| routes/admin.js (봇 관리) | - | 미완료 |
|
| routes/admin.js (카테고리 CUD) | - | 미완료 |
|
||||||
|
| routes/admin.js (봇 관리) | routes/admin/bots.js | 완료 |
|
||||||
|
| routes/admin.js (할당량) | routes/admin/bots.js | 완료 |
|
||||||
| routes/admin.js (카카오) | - | 미완료 |
|
| routes/admin.js (카카오) | - | 미완료 |
|
||||||
| routes/admin.js (할당량) | - | 미완료 |
|
| - | routes/admin/youtube.js | 신규 |
|
||||||
|
| - | routes/admin/x.js | 신규 |
|
||||||
| routes/albums.js | routes/albums/index.js | 완료 |
|
| routes/albums.js | routes/albums/index.js | 완료 |
|
||||||
| routes/members.js | routes/members/index.js | 완료 |
|
| routes/members.js | routes/members/index.js | 완료 |
|
||||||
| routes/schedules.js | routes/schedules/index.js | 부분 완료 |
|
| routes/schedules.js | routes/schedules/index.js | 완료 |
|
||||||
| routes/stats.js | routes/stats/index.js | 완료 |
|
| routes/stats.js | routes/stats/index.js | 완료 |
|
||||||
| services/youtube-bot.js | services/youtube/ | 완료 |
|
| services/youtube-bot.js | services/youtube/ | 완료 |
|
||||||
| services/youtube-scheduler.js | plugins/scheduler.js | 완료 |
|
| services/youtube-scheduler.js | plugins/scheduler.js | 완료 |
|
||||||
|
|
|
||||||
|
|
@ -200,12 +200,12 @@ function YoutubeSection({ schedule }) {
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 영상 정보 카드 */}
|
{/* 영상 정보 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="bg-white rounded-xl p-4 shadow-sm"
|
className="bg-gradient-to-br from-gray-100 to-gray-200/80 rounded-xl p-4"
|
||||||
>
|
>
|
||||||
{/* 제목 */}
|
{/* 제목 */}
|
||||||
<h1 className="font-bold text-gray-900 text-base leading-relaxed mb-3">
|
<h1 className="font-bold text-gray-900 text-base leading-relaxed mb-3">
|
||||||
|
|
@ -247,6 +247,7 @@ function YoutubeSection({ schedule }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 유튜브에서 보기 버튼 */}
|
{/* 유튜브에서 보기 버튼 */}
|
||||||
|
<div className="pt-4 border-t border-gray-300/50">
|
||||||
<a
|
<a
|
||||||
href={schedule.videoUrl}
|
href={schedule.videoUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -258,6 +259,7 @@ function YoutubeSection({ schedule }) {
|
||||||
</svg>
|
</svg>
|
||||||
YouTube에서 보기
|
YouTube에서 보기
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ function VideoInfo({ schedule, isShorts }) {
|
||||||
const isFullGroup = members.length === 5;
|
const isFullGroup = members.length === 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`bg-gradient-to-br from-gray-50 to-gray-100/50 rounded-2xl p-6 ${isShorts ? 'flex-1' : ''}`}>
|
<div className={`bg-gradient-to-br from-gray-100 to-gray-200/80 rounded-2xl p-6 ${isShorts ? 'flex-1' : ''}`}>
|
||||||
{/* 제목 */}
|
{/* 제목 */}
|
||||||
<h1 className={`font-bold text-gray-900 mb-4 leading-relaxed ${isShorts ? 'text-lg' : 'text-xl'}`}>
|
<h1 className={`font-bold text-gray-900 mb-4 leading-relaxed ${isShorts ? 'text-lg' : 'text-xl'}`}>
|
||||||
{decodeHtmlEntities(schedule.title)}
|
{decodeHtmlEntities(schedule.title)}
|
||||||
|
|
@ -56,7 +56,7 @@ function VideoInfo({ schedule, isShorts }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 유튜브에서 보기 버튼 */}
|
{/* 유튜브에서 보기 버튼 */}
|
||||||
<div className="mt-6 pt-5 border-t border-gray-200">
|
<div className="mt-6 pt-5 border-t border-gray-300/60">
|
||||||
<a
|
<a
|
||||||
href={schedule.videoUrl}
|
href={schedule.videoUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -91,7 +91,7 @@ function YoutubeSection({ schedule }) {
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="w-[420px] flex-shrink-0"
|
className="w-[420px] flex-shrink-0"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-[9/16] bg-gray-900 rounded-2xl overflow-hidden shadow-2xl shadow-black/20">
|
<div className="relative aspect-[9/16] bg-gray-900 rounded-2xl overflow-hidden shadow-lg shadow-black/10">
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://www.youtube.com/embed/${videoId}?rel=0`}
|
src={`https://www.youtube.com/embed/${videoId}?rel=0`}
|
||||||
title={schedule.title}
|
title={schedule.title}
|
||||||
|
|
@ -125,7 +125,7 @@ function YoutubeSection({ schedule }) {
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-video bg-gray-900 rounded-2xl overflow-hidden shadow-2xl shadow-black/20">
|
<div className="relative aspect-video bg-gray-900 rounded-2xl overflow-hidden shadow-lg shadow-black/10">
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://www.youtube.com/embed/${videoId}?rel=0`}
|
src={`https://www.youtube.com/embed/${videoId}?rel=0`}
|
||||||
title={schedule.title}
|
title={schedule.title}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue