v7.0.0-beta.1 release
This commit is contained in:
commit
6595fd0eb5
3967 files changed
+1875383
No files matched your search
@@ -0,0 +1,508 @@
|
||||
# Changelog
|
||||
|
||||
이 프로젝트의 모든 주요 변경사항을 기록합니다.
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.0-beta.1] - 2026-04-01
|
||||
|
||||
### Changed
|
||||
|
||||
- 오픈 베타 릴리즈
|
||||
|
||||
## [0.9.5] - 2026-03-31
|
||||
|
||||
### Added
|
||||
|
||||
- 이커머스 1:1 문의게시판 Extension Point에 `mode: "replace"` 적용 — 게시판 모듈 활성 시 default 폴백 메시지 교체
|
||||
|
||||
### Fixed
|
||||
|
||||
- 게시판 글 목록·신고 목록에서 다른 페이지로 이동 후 돌아올 때 이전 검색어가 Input에 유지되던 문제 수정 — `_global` 상태를 `_local`로 전환 (admin_board_posts_index.json, admin_board_reports_index.json)
|
||||
|
||||
## [0.9.4] - 2026-03-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- 활동 로그 enum 미번역 수정 — Post, Comment, Board, Report 모델의 `$activityLogFields` status/설정 필드 `type: 'text'` → `type: 'enum'` 변경 + 6개 Enum 클래스에 `labelKey()` 메서드 추가
|
||||
- 활동 로그 샘플 시더 enum 타입 동기화 — `ActivityLogSampleSeeder`의 status 관련 9건 `type: 'text'` → `type: 'enum'` 수정 및 `old_label_key`/`new_label_key` 추가
|
||||
|
||||
## [0.9.3] - 2026-03-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- 관리자 다크모드 레이아웃 전수 수정 — 19개 파일에서 누락된 `dark:` variant 약 70건 추가 (text-gray, bg-white/gray, focus:ring, text-red/blue, border-gray 등)
|
||||
|
||||
## [0.9.2] - 2026-03-30
|
||||
|
||||
### Changed
|
||||
|
||||
- `window.G7Config` 설정 노출 최소화 — frontend_schema 4개 카테고리(basic_defaults, report_policy, spam_security, display) 전체 `expose: false` 처리 (프론트엔드 미참조)
|
||||
|
||||
## [0.9.1] - 2026-03-30
|
||||
|
||||
### Changed
|
||||
|
||||
- ReportRepository의 직접 `MATCH...AGAINST` 호출을 `DatabaseFulltextEngine::whereFulltext()` 정적 헬퍼로 전환 — 다중 DBMS 호환
|
||||
- Post 모델에 `FulltextSearchable` 인터페이스 추가 — Scout 검색 파이프라인 지원
|
||||
- `searchIndexShouldBeUpdated()` 훅 기반 전환 — 검색 플러그인이 Post 인덱스 업데이트 제어 가능
|
||||
- 마이그레이션 FULLTEXT 인덱스 생성을 `DatabaseFulltextEngine::addFulltextIndex()` 정적 헬퍼로 전환
|
||||
|
||||
## [0.9.0] - 2026-03-30
|
||||
|
||||
### Added
|
||||
|
||||
- FULLTEXT 인덱스(ngram) 추가 — boards(name), boards_report_logs(snapshot) 총 2개
|
||||
|
||||
### Changed
|
||||
|
||||
- 신고 관리 검색 쿼리를 `LIKE '%keyword%'`에서 `MATCH...AGAINST IN BOOLEAN MODE`로 전환 — boards.name, boards_report_logs.snapshot 검색 성능 향상
|
||||
|
||||
## [0.8.1] - 2026-03-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- 신규 게시판 생성 시 조회수 표시가 기본 OFF로 설정되던 문제 수정 — 기본값을 ON으로 변경 (#228)
|
||||
- 신규 게시판 생성 시 최소 내용 길이 기본값이 10자로 설정되던 문제 수정 — 기본값을 2자로 완화 (#228)
|
||||
- 파일 크기 설정 단위를 bytes에서 MB로 통일 — 관리자 설정 입력, DB 저장, 검증, 안내 문구 모두 MB 기준으로 변경 (#228)
|
||||
- 게시글 삭제(소프트삭제) 시 댓글과 첨부파일이 함께 삭제되어 복원 시 미표시되던 문제 수정 — 게시글만 삭제하고 댓글/첨부파일은 유지 (#228)
|
||||
|
||||
## [0.8.0] - 2026-03-30
|
||||
|
||||
### Added
|
||||
|
||||
- 이커머스 1:1 문의 게시판 연동 지원
|
||||
- 이커머스 환경설정 > 문의게시판 설정 화면에 게시판 선택 드롭다운, 게시판 수정 이동, 문의내역 관리, 관리자 메뉴 추가 버튼 동적 주입
|
||||
- 문의/답변 생성·수정·삭제를 게시판 게시글로 처리하는 훅 리스너 추가
|
||||
- `PostRepository`에 관계 포함 조회 메서드 3개 추가 (`findWithBoard`, `findByIdsWithRelations`, `findFirstReplyWithBoard`)
|
||||
- 검색/필터/정렬 성능 향상을 위한 누락 인덱스 일괄 추가 — board_posts 2개([board_id+author_name] 비회원 작성자 검색, [board_id+status+created_at] 게시판별 상태+날짜 필터)
|
||||
|
||||
### Changed
|
||||
|
||||
- 문의 삭제/답변 삭제 시 게시판 알림이 발송되지 않도록 처리 방식 개선
|
||||
- 외부 모듈 위임 방식에서 `skip_notification` 옵션 직접 체크 방식으로 변경
|
||||
|
||||
### Fixed
|
||||
|
||||
- 게시글 수정 후 `after_update` 훅이 트랜잭션 내부에서 실행되어 롤백 시 부작용이 발생할 수 있던 문제 수정
|
||||
- `Board`, `Post` 모델 직접 조회를 Repository 경유로 교체
|
||||
|
||||
## [0.7.1] - 2026-03-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- 게시판 관리 부모 메뉴 URL을 null로 변경 — 대응 라우트 없는 `/admin/boards` URL이 404 발생 (module.php)
|
||||
|
||||
## [0.7.0] - 2026-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- BoardActivityLogListener: 게시판/게시판유형/게시물/댓글/첨부파일/신고 전체 로깅
|
||||
- 활동 로그 다국어 키 33개 정의 (resources/lang/ko/activity_log.php, en/activity_log.php)
|
||||
- $activityLogFields 메타데이터: Board, Post, Comment 모델
|
||||
- ActivityLog 샘플 시더 (database/seeders/ActivityLogSampleSeeder.php)
|
||||
- ActivityLogDescriptionResolver: 활동 로그 표시 시점에 게시판/게시물/댓글 엔티티명을 ID에서 해석하는 필터 훅 리스너
|
||||
- BoardActivityLogListener: 신고 일괄 상태변경 before 스냅샷 + after ChangeDetector 추가 (1훅 추가, 총 32훅)
|
||||
- BoardActivityLogListener: 신고 일괄 상태변경 per-Report 로깅 전환 (건별 loggable_id 기록)
|
||||
|
||||
### Fixed
|
||||
|
||||
- 활동 로그 description에 번역 키가 원문으로 노출되는 문제 수정 — src/lang 번역 파일 동기화, description_params 불일치 수정
|
||||
- UpdateBoardTypeRequest: `name.ko` 로케일 하드코딩 메시지 제거 — LocaleRequiredTranslatable이 자체 메시지 반환
|
||||
|
||||
## [0.6.10] - 2026-03-26
|
||||
|
||||
### Added
|
||||
|
||||
- 사용자 게시판 목록/상세에서 manager 권한 사용자가 삭제된 게시글/댓글을 선택적으로 볼 수 있는 토글 기능 추가
|
||||
|
||||
### Fixed
|
||||
|
||||
- 게시판 권한 탭 manager/step TagInput UUID 불일치 수정 — options 및 change 핸들러 표현식에서 `u.id` → `u.uuid`로 수정 (4곳)
|
||||
- `BoardController::getFormData()` 생성 모드에서 `board_manager_ids`에 uuid 반환하도록 수정
|
||||
- `BoardSampleSeeder` — `pluck('id')` → `pluck('uuid')` 수정
|
||||
- `StoreCommentRequest` — 서버 주입 필드(`user_id`, `ip_address`) 검증 규칙 제거 (규정 준수)
|
||||
- `CommentController` — `prepareForValidation()`으로 처리된 `post_id` 중복 대입 제거
|
||||
- 신고 시스템 API 응답 UUID 적용 — `ReportResource`, `ReportLogResource`, `ReportDetailResource`, `ReportService` `id` → `uuid` 전환
|
||||
- 마이페이지 내 게시글 목록에서 비활성 게시판(`is_active=false`)의 게시글이 노출되던 문제 수정
|
||||
- 공지글 쿼리에서 삭제 상태 공지 제외 누락 수정
|
||||
- 삭제된 게시글 상세 접근 시 명시적 404 처리 추가
|
||||
- 삭제된 게시글의 댓글을 권한 없는 사용자에게 반환하던 문제 수정
|
||||
- 게시판 폼 탭 순서 통일 (기본정보 → 설정 → 권한)
|
||||
- 대댓글 UI 들여쓰기 버그 수정
|
||||
|
||||
## [0.6.9] - 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- 게시판 설정 화면의 각 탭 이름을 한국어/영어로 표시할 수 있도록 다국어 키 추가
|
||||
|
||||
### Changed
|
||||
|
||||
- 게시판 설정 화면을 기능별 탭(기본, 권한, 목록, 게시글, 답변글, 댓글, 첨부파일, 알림, 일괄적용)으로 분리하여 레이아웃 파일 전면 재구성
|
||||
- 변경된 탭 구조에 맞게 레이아웃 렌더링 테스트 업데이트
|
||||
|
||||
## [0.6.8] - 2026-03-23
|
||||
|
||||
### Changed
|
||||
|
||||
- API Resource: author.id → author.uuid 전환 (PostResource, CommentResource, BoardResource)
|
||||
- PostResource: user_id FK → user.uuid 전환
|
||||
- CommentResource: author.id → author.uuid 전환
|
||||
- 관리자 레이아웃: author.id → author.uuid 참조 전환 (5개 파일)
|
||||
- 유저 게시글 라우트: 정수 제약 제거 (Route Model Binding 호환)
|
||||
- FormRequest: 정수 검증 → UUID 검증 전환 (StoreBoardRequest, StoreCommentRequest, UpdateBoardRequest)
|
||||
- BoardService: UUID→정수 FK 변환 처리 추가
|
||||
|
||||
## [0.6.7] - 2026-03-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- 공개 API 라우트 throttle을 60/분(댓글은 30/분)에서 600/분으로 일괄 상향 — 페이지 간 네비게이션 시 동일 카운터(sha1(userId)) 공유로 인한 429 Too Many Requests 방지 (게시글, 댓글, 첨부파일, 사용자 프로필, 마이페이지 활동 라우트 포함)
|
||||
|
||||
## [0.6.6] - 2026-03-21
|
||||
|
||||
### Added
|
||||
- 게시판 날짜 표기 방식 개선 — `standard`(표준형) / `relative`(유동형) 전역 설정 추가
|
||||
- 게시판 환경설정 `general` 탭 신규 추가 — 날짜 표시 방식 선택 UI
|
||||
- 블라인드/삭제 게시글 댓글 작성 폼 숨김 및 안내 메시지 표시
|
||||
|
||||
### Changed
|
||||
- `PostResource` `created_at` — 원본 datetime 반환으로 변경, 포맷된 날짜는 `created_at_formatted` 필드로 분리
|
||||
- `SearchPostsListener` — 게시판별 N번 쿼리 → 단일 쿼리로 리팩토링
|
||||
|
||||
### Fixed
|
||||
- 게시판 환경설정 권한 일괄적용 시 manager/step 역할이 초기화되던 문제 수정
|
||||
- 게시판 권한 자동주입 정책 변경 — 게시판 생성/수정 시에만 적용, 일괄적용 시 제외
|
||||
- 일괄적용 모달 열기 전 이전 오류 상태가 남아있던 문제 수정
|
||||
- 이미지 미리보기(preview) API throttle을 게시판 API 그룹(60/분)에서 분리하여 전용 그룹(300/분)으로 변경 — 갤러리 게시판에서 다수 썸네일 로드 시 429 Too Many Requests 방지
|
||||
|
||||
## [0.6.5] - 2026-03-18
|
||||
|
||||
### Added
|
||||
|
||||
- `SeoBoardSettingsCacheListener` — 게시판 모듈 설정 변경 시 관련 SEO 캐시(board/index, board/show, board/boards, sitemap) 선별 무효화
|
||||
|
||||
### Changed
|
||||
|
||||
- `SeoBoardCacheListener` — 게시판별 독립 무효화로 전환 (`invalidateByLayout` → `invalidateByUrl` URL 기반), `search/index`·`home` 무효화 추가, 생성/수정 시 해당 게시물 상세 캐시 즉시 재생성(`SeoCacheRegenerator`)
|
||||
|
||||
## [0.6.4] - 2026-03-16
|
||||
|
||||
### Changed
|
||||
|
||||
- 마이그레이션 통합 — 증분 마이그레이션을 테이블당 1개 create 마이그레이션으로 정리
|
||||
- 시더 디렉토리 분리 — 샘플 시더를 `Sample/` 하위로 이동
|
||||
- 라이선스 프로그램 명칭 정비
|
||||
- 게시판 환경설정 메일 템플릿 탭 레이아웃 개선
|
||||
|
||||
## [0.6.3] - 2026-03-13
|
||||
|
||||
### Added
|
||||
- 신고 접수 시 관리자에게 알림 이메일 발송 기능 추가
|
||||
- 신고 처리(블라인드/삭제/반려) 결과를 게시글 작성자에게 알림 이메일로 안내하는 기능 추가
|
||||
- 관리자 알림 발송 범위 설정 추가 — 게시글당 최초 신고 시에만 발송하거나, 신고가 들어올 때마다 발송하는 옵션 선택 가능
|
||||
- 관리자 역할별 신고 알림 수신 여부 설정 기능 추가
|
||||
- 이미 신고한 게시글/댓글의 신고 버튼 비활성화 기능 추가
|
||||
|
||||
### Changed
|
||||
- 신고 정책 설정 화면에서 알림 발송 범위를 더 직관적인 라디오 버튼 형태로 변경
|
||||
- 신고 반려 처리 알림 문구 개선 — "반려(복원)"으로 명확하게 안내하도록 변경
|
||||
- 블라인드 처리된 게시글 안내 메시지 통일
|
||||
- 신고 알림 수신 관리자 역할 지정 방식 개선 — 중복 지정 없이 정확하게 반영되도록 수정
|
||||
|
||||
### Fixed
|
||||
- 존재하지 않는 게시글 접근 시 404 페이지로 이동하지 않던 문제 수정
|
||||
- 신고 일괄 처리 드롭다운이 정상 작동하지 않던 문제 수정
|
||||
|
||||
## [0.6.2] - 2026-03-12
|
||||
|
||||
### Added
|
||||
- 신고 케이스 구조 전환 — 게시글/댓글당 케이스 1행 구조로 변경 (boards_reports + boards_report_logs 분리)
|
||||
- 신고자 목록 페이지네이션 API 추가
|
||||
- 신고 처리 이력 타임라인 표시
|
||||
- ReportLogResource 추가
|
||||
- manifest에 license 필드 및 LICENSE 파일 추가
|
||||
|
||||
### Changed
|
||||
- 신고 목록 정렬 기준을 마지막 신고 일시 기준으로 변경
|
||||
- 신고 일괄 처리 단순화 — 케이스 단위 직접 처리로 변경
|
||||
- 게시판 샘플 시더 게시판 6개 → 8개로 확장
|
||||
- 신고 샘플 시더 재신고자 없을 경우 방어 처리 추가
|
||||
|
||||
### Fixed
|
||||
- 재활성 사이클 카운트 버그 수정 — 이전 사이클 신고 로그가 현재 사이클에 포함되던 문제
|
||||
- 반려 시 복구 대상 케이스 상태 조건 불일치 수정
|
||||
|
||||
## [0.6.1] - 2026-03-11
|
||||
|
||||
### Changed
|
||||
|
||||
- `BoardResource` 응답의 `user_permissions` 키를 `user_abilities`로 변경 — `can_*` 형식 통일
|
||||
- `getUserBoardPermissions()` 메서드의 반환 키를 `posts_write`, `manager` 등 underscore 형식에서 `can_write`, `can_manage` 등 `can_*` 형식으로 변경
|
||||
- Admin/User PostController의 `include_user_permissions` 파라미터를 `include_user_abilities`로 변경
|
||||
|
||||
### Added
|
||||
|
||||
- `BoardUserAbilitiesTest` — BoardResource user_abilities 키 구조, PostResource/CommentResource abilities 키 검증 테스트 13개
|
||||
|
||||
## [0.6.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- 게시판별 동적 권한에 스코프(scope) 메타데이터 지원 추가 (`resource_route_key`, `owner_key`)
|
||||
- `config/board.php` 권한 정의에 `scope` 키 추가 (12개 권한: admin/user 각 6개)
|
||||
- `BoardPermissionService::ensureBoardPermissions()` — 권한 생성 시 scope 메타데이터 자동 설정
|
||||
- `PostService` — 목록/상세 조회 시 `$context` 파라미터 기반 스코프 필터링 적용
|
||||
- `CommentService::getCommentsByPostId()` — `$context` 파라미터 기반 스코프 필터링 적용
|
||||
- `AttachmentService::download()` — `$context` 파라미터 기반 스코프 접근 제어 적용
|
||||
- User 컨트롤러 전체에 `AccessDeniedHttpException` catch 및 403 응답 처리
|
||||
- Admin PostController/AttachmentController에 `AccessDeniedHttpException` catch 추가
|
||||
- `Upgrade_0_6_0.php` — 기존 게시판 동적 권한의 scope 메타데이터 백필 업그레이드 스크립트
|
||||
- `BoardPermissionScopeServiceTest` — 서비스 레이어 스코프 필터링 테스트 10개
|
||||
|
||||
### Fixed
|
||||
- User 컨트롤러에서 `getBoardBySlug()` 호출 시 `checkScope: false` 적용 — 매니저 역할 사용자 500 에러 수정
|
||||
- `PostRepository::countNormalPosts()` — 누락된 스코프 필터링 추가
|
||||
|
||||
## [0.5.5] - 2026-03-11
|
||||
|
||||
### Fixed
|
||||
- Board 모델 `$guarded`에서 `created_by`, `updated_by` 제거 후 `$fillable`로 이동 — 게시판 생성 시 생성자/수정자 ID가 DB에 저장되지 않던 문제 해결
|
||||
- `BoardService::createBoard()`에 `Auth::id()` 기반 `created_by`/`updated_by` 설정 추가
|
||||
- `BoardService::updateBoard()`에 `Auth::id()` 기반 `updated_by` 설정 추가
|
||||
|
||||
### Changed
|
||||
- `ModuleTestCase`에 `$migrated` 플래그 추가 — 마이그레이션 체크를 프로세스당 1회로 최적화
|
||||
|
||||
## [0.5.4] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `BoardService`, `ReportService`에 `PermissionHelper::applyPermissionScope()` / `checkScopeAccess()` 적용
|
||||
- `BoardController`, `ReportController` 상세/수정/삭제 엔드포인트에 `AccessDeniedHttpException` 처리 추가
|
||||
- 게시판 목록 조회 시 스코프 필터링 적용 (본인만/역할/전체)
|
||||
- 게시판 상세/수정/삭제/복사 시 스코프 접근 제어 적용
|
||||
- 신고 상세/상태변경/삭제 시 스코프 접근 제어 적용
|
||||
|
||||
### Fixed
|
||||
- `PostCollection::toArray()` 클로저에 `$request` 변수 미전달 버그 수정 (Undefined variable $request)
|
||||
- `BoardPermissionTest` 테스트 assertions를 `permissions` → `abilities` 키로 수정
|
||||
- `PostResourcePermissionTest` 테스트 assertions를 `permissions` → `abilities` 키로 수정
|
||||
|
||||
## [0.5.3] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- 게시판 설정 화면에 `isReadOnly` 패턴 적용 (읽기 전용 배너, 모든 폼 필드 비활성화, 저장 버튼 비활성화)
|
||||
- 게시판 폼 화면에 `isReadOnly` 패턴 적용 (기본정보/권한설정/목록설정/게시글설정/알림설정 탭 전체)
|
||||
- 게시글 폼 화면에 `isReadOnly` 패턴 적용 (제목/본문/카테고리/옵션/첨부파일 비활성화)
|
||||
- `BoardSettingsController`에 abilities 응답 추가 (`can_update`)
|
||||
- 게시판 목록 DS에 403 errorHandling 추가
|
||||
- 게시판 폼 DS에 403 errorHandling 및 roles DS fallback 추가
|
||||
- 게시판 설정 DS에 403 errorHandling 및 보조 DS fallback 추가
|
||||
|
||||
### Changed
|
||||
- 게시판 목록 "게시판 추가" 버튼: `if`(숨김) → `disabled`(비활성화)로 변경
|
||||
- 게시글 목록 "게시글 작성" 버튼: `if`(숨김) → `disabled`(비활성화)로 변경
|
||||
- 게시글 상세 답글 버튼: `if`(숨김) → `disabled`(비활성화)로 변경
|
||||
- 게시글 상세 ActionMenu: 권한 없는 항목을 숨김 대신 `disabled` 처리
|
||||
- 댓글 답글 버튼 및 ActionMenu: 권한 없는 항목을 숨김 대신 `disabled` 처리
|
||||
- 댓글 입력 영역: `if`에서 ability 조건 분리 → 내부 Textarea/Button `disabled` 처리
|
||||
- 신고 목록 일괄 작업 버튼: `if`(숨김) → `disabled`(비활성화)로 변경
|
||||
- 신고 상세 상태 변경 버튼: `if`에서 ability 조건 분리 → `disabled` 처리
|
||||
- 블라인드/삭제 댓글 원본 보기 버튼: `if`(숨김) → `disabled`(비활성화)로 변경
|
||||
- 게시판 유형 관리 모달에 `isReadOnly` 기반 disabled 추가
|
||||
|
||||
### Fixed
|
||||
- `ReportCollection`의 `permissions` → `abilities` 키 불일치 수정 (신고 목록 일괄 작업 미표시 버그)
|
||||
- `ReportDetailResource`의 `permissions` → `abilities` 키 불일치 수정 (신고 상세 상태 변경 버튼 미표시 버그)
|
||||
|
||||
## [0.5.2] - 2026-03-10
|
||||
|
||||
### Changed
|
||||
- 권한 카테고리에 `resource_route_key`, `owner_key` 스코프 메타데이터 추가 (boards, reports)
|
||||
- 라우트 미들웨어 `except:owner:*` 옵션 제거 (scope_type 데이터 기반 시스템으로 전환)
|
||||
- Repository 목록 조회에 `PermissionHelper::applyPermissionScope()` 적용 (Board, Post, Comment, Report)
|
||||
- `PermissionBypassable` 인터페이스 및 `getBypassUserId()` 제거 (Board, Post, Comment, Report, Attachment 모델)
|
||||
|
||||
## [0.5.1] - 2026-03-10
|
||||
|
||||
### Changed
|
||||
- API 리소스 권한 플래그 키 `permissions` → `abilities`로 변경 (코어 표준화)
|
||||
- `permissionMap()` → `abilityMap()`, `resolvePermissions()` → `resolveAbilities()` 메서드명 변경
|
||||
- `BoardCollection.withPermissions()` 응답 키 `permissions` → `abilities`
|
||||
- `PostCollection` 응답 키 `permissions` → `abilities`
|
||||
- 기존 `board_permissions` 키를 원래 이름 `permissions`로 원복 (충돌 해소)
|
||||
- 관리자 레이아웃 JSON의 `permissions.can_*` / `permissions?.can_*` 바인딩을 `abilities.can_*` / `abilities?.can_*`로 변경
|
||||
- 사용자 레이아웃 JSON의 `permissions?.can_view` 바인딩을 `abilities?.can_view`로 변경
|
||||
|
||||
### Fixed
|
||||
- 알림 채널(이메일/사이트 알림) 일괄 적용 시 채널 설정이 저장되지 않는 버그 수정
|
||||
- 일괄 적용 모달에서 알림 채널 항목 선택 시 체크박스가 정상 동작하지 않는 버그 수정
|
||||
- 일괄 적용 모달에서 알림 채널 항목명이 키 코드로 표시되는 다국어 누락 수정
|
||||
|
||||
## [0.5.0] - 2026-03-06
|
||||
|
||||
### Changed
|
||||
- 메일 템플릿 목록 API에 검색/필터/페이지네이션 지원 추가
|
||||
- 메일 템플릿 편집 모달 UX 개선 (blur_until_loaded, sticky footer, 함수형 body)
|
||||
- `getDefaultTemplateData()`를 Controller에서 Service로 이동 (Service-Repository 패턴 준수)
|
||||
- 메일 템플릿 탭 UI를 코어 환경설정과 동일한 구조로 변경
|
||||
|
||||
### Added
|
||||
- 메일 템플릿 미리보기(preview) API 엔드포인트 추가
|
||||
- 메일 템플릿 검색 기능 (제목/본문/전체)
|
||||
- 페이지당 항목 수 선택 및 페이지네이션
|
||||
- 메일 템플릿 관련 다국어 키 추가 (검색/필터/빈 상태/편집 모달)
|
||||
|
||||
## [0.4.0] - 2026-03-05
|
||||
|
||||
### Added
|
||||
- DB 기반 메일 템플릿 시스템 (5종: 새 댓글, 대댓글, 답변글, 관리자 처리, 관리자 새 게시글)
|
||||
- `board_mail_templates` 테이블, BoardMailTemplate 모델/서비스/리포지토리/컨트롤러
|
||||
- 메일 템플릿 관리 API 및 환경설정 UI
|
||||
- 업그레이드 스텝 (`Upgrade_0_4_0`) — 기존 설치에 메일 템플릿 초기 시딩
|
||||
|
||||
### Changed
|
||||
- 5개 알림 클래스 DbTemplateMail 기반으로 전환 (Blade → DB 기반)
|
||||
|
||||
### Removed
|
||||
- `BoardNotificationMail`, `board-notification.blade.php` 삭제
|
||||
|
||||
## [0.3.2] - 2026-03-06
|
||||
|
||||
### Added
|
||||
- 게시판 유형(BoardType)을 독립 엔티티로 분리 — `board_types` 테이블, `BoardType` 모델
|
||||
- 게시판 유형 API — `BoardTypeController`, `BoardTypeService`, `BoardTypeRepository` 추가
|
||||
- 게시판 유형 요청/응답 클래스 — `StoreBoardTypeRequest`, `UpdateBoardTypeRequest`, `BoardTypeResource` 추가
|
||||
- 환경설정 > 기본설정 탭에 게시판 유형 관리 모달 추가 (생성 / 수정 / 삭제)
|
||||
- 게시판 유형 삭제 시 사용 중인 게시판 수 검증 (사용 중이면 삭제 불가)
|
||||
- 기본값으로 설정된 게시판 유형 삭제 방지 로직 추가
|
||||
- 미지원 게시판 유형에 대한 fallback 블록 추가 (basic 레이아웃으로 표시)
|
||||
|
||||
### Changed
|
||||
- 게시판 생성/수정 폼에서 유형 관리 버튼 제거 — 환경설정 > 기본설정에서 일원 관리하도록 UX 변경
|
||||
- 게시판 폼 유형 Select 하단에 관리 안내 힌트 텍스트 추가
|
||||
- 게시판 폼 및 환경설정 기본 게시판 유형 Select에 slug 함께 표시 + 긴 목록 스크롤 처리
|
||||
- 게시판 유형 slug 허용 문자 규칙을 하이픈(-) 통일 (기존 언더스코어(_) 혼용에서 변경)
|
||||
|
||||
### Fixed
|
||||
- 게시판 유형 모달에서 저장 실패 시 필드별 에러 메시지 표시 및 빨간 테두리 강조
|
||||
- 테스트 환경에서 `Accept-Language` 헤더 미설정 시 `name.ko` 필수 검증이 통과되던 문제 수정
|
||||
|
||||
## [0.3.1] - 2026-03-05
|
||||
|
||||
### Added
|
||||
- 신고 남발 방지 기능 — 일일 신고 횟수 제한, 반려 누적 시 신고 차단, 연속 신고 쿨타임
|
||||
- 관리자 환경설정 신고 정책 탭을 "자동 숨김 설정" / "신고 제한 설정" 2개 카드로 분리
|
||||
- 연속 신고 쿨타임 설정 항목 추가 (기본 60초, 0이면 비활성화)
|
||||
- 각 설정 항목에 동작 설명 안내 텍스트 보강
|
||||
- 관리자 신고 목록에 마지막 신고일시 표시 (2건 이상 신고 시)
|
||||
- 관리자 신고 목록/상세에 비회원 신고자 배지 표시
|
||||
- 비회원 신고 샘플 시더 데이터 추가
|
||||
|
||||
### Changed
|
||||
- 쿨타임 설정 변경 시 즉시 반영되도록 설정 조회 방식 변경
|
||||
- 쿨타임 메시지를 신고 전용으로 분리
|
||||
|
||||
### Fixed
|
||||
- 신고 모달에서 사유 미선택 시 콘솔 오류가 발생하던 문제 수정
|
||||
- 댓글 신고 시 관리자 목록에서 게시글 제목이 표시되지 않던 문제 수정
|
||||
- 관리자 신고 목록에서 페이지/정렬 변경 시 체크박스 선택이 유지되던 문제 수정
|
||||
- 연속 신고 쿨타임 설정값이 저장되지 않던 문제 수정
|
||||
|
||||
### Removed
|
||||
- "신고 차단 기간" 설정 항목 제거 — 실제 동작과 불일치하여 혼란 유발
|
||||
|
||||
## [0.3.0] - 2026-03-03
|
||||
|
||||
### Added
|
||||
- 마이페이지 "내가 쓴 댓글" API (`GET /me/my-comments`) — Comment 기반 페이지네이션
|
||||
- `CommentRepository::getUserComments()` — 삭제 게시글·비활성 게시판 댓글 자동 제외
|
||||
- `PostRepository::findByBoardId()`, `CommentRepository::findByBoardId()` — 신고 처리용 Repository 메서드 추가
|
||||
|
||||
### Changed
|
||||
- `board_posts`, `board_comments`, `board_attachments` 단일 테이블 전환 (기존 슬러그별 동적 테이블 폐지)
|
||||
- `board_id` 파티션(`PARTITION BY LIST`)으로 게시판 분리 — 게시판 추가 시 파티션 동적 생성
|
||||
- `DynamicPost/Comment/Attachment` 모델 → `Post/Comment/Attachment` 단일 모델로 통합
|
||||
- Repository 전체 `setTableBySlug()` 패턴 → `where('board_id', ...)` 단일 쿼리 패턴으로 전환
|
||||
- 마이페이지 게시판 활동 조회 PHP `forPage()` → DB `paginate()` 전환 (메모리 부족·타임아웃 해결)
|
||||
- 통계 `total_comments` 집계 조건을 목록 API와 동일하게 INNER JOIN 적용 (삭제 게시글 댓글 제외)
|
||||
- Admin 라우트에 `admin` 미들웨어 추가 (5개 그룹)
|
||||
- `ReportService` — `logger()` 헬퍼 → `Log::` 파사드 교체
|
||||
- `BoardService::clearBoardCaches()` — `Cache::forget()` → `CacheService::forget()` 통일
|
||||
- `PostService`, `CommentService` — actionLog 배열 생성 중복 → `buildActionLog()` 메서드 공통화
|
||||
|
||||
### Fixed
|
||||
- raw SQL 내 테이블명에 DB prefix 미적용 문제 수정
|
||||
- `DB::table('x as y')` alias에 prefix 이중 적용되는 문제 수정
|
||||
- 답글 작성 시 삭제된 게시글 체크 후 조기 종료 누락 버그 수정, 공지 답글 금지 및 깊이 제한 추가
|
||||
- 댓글 작성 시 삭제된 댓글 체크 후 조기 종료 누락 버그 수정, 깊이 제한 추가
|
||||
- 게시글 목록에서 depth 2 이상 답글이 표시되지 않는 버그 수정
|
||||
- 관리자 게시글 목록 답글 depth 시각화 — depth에 비례한 들여쓰기 적용
|
||||
- 관리자 답글 버튼 — 공지글이 아니고 최대 답글 깊이 미만인 게시글에만 표시되도록 조건 수정
|
||||
- 인기글 목록이 HTTP 200이지만 빈 배열로 반환되는 버그 수정 (raw SQL DB prefix 누락)
|
||||
- 게시글·댓글 API 응답에서 비밀번호 필드 노출 방지
|
||||
- 게시글·댓글 트리거 타입을 Enum으로 올바르게 처리
|
||||
- 댓글 단건 조회 시 소프트 삭제된 댓글이 조회되지 않는 문제 수정
|
||||
- 신고 처리 시 DB 직접 접근 → Repository 패턴으로 교체
|
||||
- 게시글 상세 조회 비즈니스 로직을 Controller에서 Service로 이동
|
||||
- 신고 상태별 집계 로직을 Controller에서 Service로 이동
|
||||
- 신고 가능 여부·본인 콘텐츠 검증 로직을 Controller에서 Service로 이동
|
||||
|
||||
### Removed
|
||||
- `BoardTableService` — 동적 테이블 생성 서비스 삭제
|
||||
- `TableCreationException` 삭제
|
||||
- `DynamicPost`, `DynamicComment`, `DynamicAttachment` 모델 삭제
|
||||
- `getUserCommentedActivities()` (PostRepository) — 미사용 메서드 (마이댓글 API 전환으로 대체)
|
||||
|
||||
## [0.2.0] - 2026-02-26
|
||||
|
||||
### Added
|
||||
- 환경설정 페이지 (3탭: 기본 설정 / 신고 정책 / 스팸·보안)
|
||||
- 환경설정 CRUD API (`GET/PUT /admin/settings`, `GET /admin/settings/{category}`)
|
||||
- 일괄 적용 API (`POST /admin/settings/bulk-apply`) — 체크된 항목을 선택 게시판에 일괄 적용
|
||||
- 캐시 초기화 API (`POST /admin/settings/clear-cache`)
|
||||
- `BoardSettingsService` (ModuleSettingsInterface 구현)
|
||||
- `boards` 테이블에 `max_reply_depth`(기본 5), `max_comment_depth`(기본 10) 컬럼 추가
|
||||
- 게시판 폼에 답변글 최대 depth(1~5), 대댓글 최대 depth(1~10) 필드 추가
|
||||
- 게시판 폼 알림 탭에 알림별 채널 선택 UI 추가 (필터 훅 `sirsoft-board.notification.channels`로 확장 가능)
|
||||
|
||||
### Changed
|
||||
- `config/board.php`의 기본값(`defaults`, `default_board_permissions`, `blocked_keywords`, `view_count_*`)을 ModuleSettings 시스템으로 이관
|
||||
- `config/board.php`에서 `report_types` 제거 (dead code, `ReportReasonType` Enum이 원본)
|
||||
- 기존 코드의 `config()` 참조를 `g7_module_settings()` 헬퍼로 전환 (9개 파일)
|
||||
- `boards` 테이블 기본값 통일: `show_view_count=false`, `notify_admin_on_post=true`, `notify_author=true`
|
||||
- 조회수 중복 방지 방식을 단일 캐시 TTL 방식으로 통합 (세션 방식 제거, API 미들웨어에 `StartSession` 미적용)
|
||||
|
||||
### Fixed
|
||||
- 권한 TagInput 수정 시 저장 버튼이 표시되지 않는 문제 수정
|
||||
- 일괄 적용 모달에서 `fields` 값이 전달되지 않는 문제 수정
|
||||
- `.` 포함 권한 키(예: `sirsoft-board.posts.read`)가 `BulkApplySettingsRequest` 검증에서 422 오류 발생하는 문제 수정
|
||||
|
||||
## [0.1.2] - 2026-02-25
|
||||
|
||||
### Changed
|
||||
- 모듈 라우트 admin/user 분기 서빙 적용 (routes.json → routes/admin.json 이동)
|
||||
|
||||
## [0.1.1] - 2026-02-24
|
||||
|
||||
### Changed
|
||||
- 버전 체계 조정 (정식 출시 전 0.x 체계로 변경)
|
||||
|
||||
## [0.1.0] - 2026-02-23
|
||||
|
||||
### Added
|
||||
- 게시판 모듈 초기 구현
|
||||
- 게시판 CRUD (생성, 읽기, 수정, 삭제) 기능
|
||||
- 게시판별 스킨 설정, 카테고리 관리
|
||||
- 게시물 CRUD 기능 (작성, 조회, 수정, 삭제)
|
||||
- 비밀글 기능: 작성자/관리자만 열람 가능
|
||||
- 댓글 시스템: CRUD, 대댓글 (계층형 댓글)
|
||||
- 게시물 검색 기능 (제목, 내용, 작성자 검색)
|
||||
- 첨부파일: 이미지/파일 업로드, 다운로드, 인라인 표시
|
||||
- 카드/갤러리 레이아웃: 다양한 게시물 목록 표시 형태 지원
|
||||
- 인기글/최신글 위젯
|
||||
- SEO 메타 태그 자동 생성
|
||||
- 관리자 레이아웃 (게시판 관리, 게시물 관리, 댓글 관리)
|
||||
- 사용자 레이아웃 (게시판 목록, 상세, 작성/수정)
|
||||
- 권한 시스템 연동 (게시판별 읽기/쓰기/댓글/관리 권한)
|
||||
- 다국어 지원 (ko, en)
|
||||
@@ -0,0 +1,47 @@
|
||||
프로그램 명칭 : 그누보드7용 게시판 모듈 (sirsoft-board)
|
||||
|
||||
저작자 : (주)에스아이알소프트
|
||||
|
||||
----- MIT 라이선스 (한국어 번역) --------------------------------------------------------
|
||||
|
||||
MIT 라이선스
|
||||
|
||||
Copyright (c) 2026 (주)에스아이알소프트
|
||||
|
||||
이 소프트웨어와 관련 문서 파일(이하 "소프트웨어")의 복사본을 취득하는 모든 사람에게
|
||||
소프트웨어를 제한 없이 사용, 복사, 수정, 병합, 출판, 배포, 서브라이선스 허여 및/또는
|
||||
판매할 수 있는 권리를 무상으로 부여합니다. 다만, 소프트웨어를 제공받은 사람은 다음
|
||||
조건을 따라야 합니다:
|
||||
|
||||
위 저작권 고지와 본 허가 고지는 소프트웨어의 모든 복사본 또는 상당 부분에 포함되어야
|
||||
합니다.
|
||||
|
||||
소프트웨어는 "있는 그대로" 제공되며, 명시적이든 묵시적이든 어떠한 종류의 보증도 하지
|
||||
않습니다. 여기에는 상품성, 특정 목적에의 적합성 및 비침해에 대한 보증이 포함되나 이에
|
||||
국한되지 않습니다. 어떠한 경우에도 저작자 또는 저작권자는 소프트웨어나 소프트웨어의
|
||||
사용 또는 기타 거래로 인해 발생하는 계약, 불법행위 또는 기타 청구, 손해 또는 기타
|
||||
책임에 대해 책임을 지지 않습니다.
|
||||
|
||||
----- MIT License (English Original) --------------------------------------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2026 SIRSOFT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "modules/sirsoft-board",
|
||||
"description": "Board module for Gnuboard7",
|
||||
"type": "library",
|
||||
"version": "1.0.0-beta.1",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Sirsoft\\Board\\": "src/",
|
||||
"Modules\\Sirsoft\\Board\\Database\\Seeders\\": "database/seeders/",
|
||||
"Modules\\Sirsoft\\Board\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\Sirsoft\\Board\\Upgrades\\": "upgrades/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 첨부파일 저장 설정
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'attachment' => [
|
||||
'disk' => env('SIRSOFT_BOARD_ATTACHMENT_DISK', 'modules'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 게시판별 권한 정의 (name, description)
|
||||
|--------------------------------------------------------------------------
|
||||
| 게시판별 동적 권한 생성 시 사용되는 권한 메타데이터입니다.
|
||||
| BoardPermissionService에서 permissions 테이블에 레코드 생성 시 사용됩니다.
|
||||
|
|
||||
| 권한 구조:
|
||||
| - admin.* : 관리자 권한 (type: admin, identifier: sirsoft-board.{slug}.admin.*)
|
||||
| - 일반 키 : 사용자 권한 (type: user, identifier: sirsoft-board.{slug}.*)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'board_permission_definitions' => [
|
||||
// ========================================
|
||||
// 관리자 권한 (type: admin)
|
||||
// ========================================
|
||||
'admin.posts.read' => [
|
||||
'name' => ['ko' => '게시글 조회 (관리자)', 'en' => 'View Posts (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 게시글 조회', 'en' => 'View posts in admin page'],
|
||||
'scope' => ['resource_route_key' => 'post', 'owner_key' => 'user_id'],
|
||||
],
|
||||
'admin.posts.write' => [
|
||||
'name' => ['ko' => '게시글 작성/수정/삭제 (관리자)', 'en' => 'Create/Edit/Delete Posts (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 게시글 작성 및 수정/삭제', 'en' => 'Create and edit/delete posts in admin page'],
|
||||
],
|
||||
'admin.posts.read-secret' => [
|
||||
'name' => ['ko' => '비밀글 조회 (관리자)', 'en' => 'View Secret Posts (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 비밀글 조회', 'en' => 'View secret posts in admin page'],
|
||||
'scope' => ['resource_route_key' => 'post', 'owner_key' => 'user_id'],
|
||||
],
|
||||
'admin.comments.read' => [
|
||||
'name' => ['ko' => '댓글 조회 (관리자)', 'en' => 'View Comments (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 댓글 조회', 'en' => 'View comments in admin page'],
|
||||
'scope' => ['resource_route_key' => 'comment', 'owner_key' => 'user_id'],
|
||||
],
|
||||
'admin.comments.write' => [
|
||||
'name' => ['ko' => '댓글 작성/수정/삭제 (관리자)', 'en' => 'Create/Edit/Delete Comments (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 댓글 작성 및 수정/삭제', 'en' => 'Create and edit/delete comments in admin page'],
|
||||
],
|
||||
'admin.attachments.upload' => [
|
||||
'name' => ['ko' => '파일 업로드/삭제 (관리자)', 'en' => 'Upload/Delete Files (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 파일 업로드 및 삭제', 'en' => 'Upload and delete files in admin page'],
|
||||
'scope' => ['resource_route_key' => 'attachment', 'owner_key' => 'created_by'],
|
||||
],
|
||||
'admin.attachments.download' => [
|
||||
'name' => ['ko' => '파일 다운로드 (관리자)', 'en' => 'Download Files (Admin)'],
|
||||
'description' => ['ko' => '관리자 페이지에서 파일 다운로드', 'en' => 'Download files in admin page'],
|
||||
'scope' => ['resource_route_key' => 'attachment', 'owner_key' => 'created_by'],
|
||||
],
|
||||
'admin.manage' => [
|
||||
'name' => ['ko' => '게시판 관리 (관리자)', 'en' => 'Manage Board (Admin)'],
|
||||
'description' => ['ko' => '타인 게시글/댓글 관리, 블라인드, 복원, 공지 설정', 'en' => 'Manage others posts/comments, blind, restore, set notice'],
|
||||
'scope' => ['resource_route_key' => 'board', 'owner_key' => 'created_by'],
|
||||
],
|
||||
|
||||
// ========================================
|
||||
// 사용자 권한 (type: user)
|
||||
// ========================================
|
||||
'posts.read' => [
|
||||
'name' => ['ko' => '게시글 조회', 'en' => 'View Posts'],
|
||||
'description' => ['ko' => '게시글 목록 및 상세 조회', 'en' => 'View post list and details'],
|
||||
'scope' => ['resource_route_key' => 'post', 'owner_key' => 'user_id'],
|
||||
],
|
||||
'posts.write' => [
|
||||
'name' => ['ko' => '게시글 작성/수정/삭제', 'en' => 'Create/Edit/Delete Posts'],
|
||||
'description' => ['ko' => '게시글 작성 및 본인 글 수정/삭제', 'en' => 'Create posts and edit/delete own posts'],
|
||||
],
|
||||
'posts.read-secret' => [
|
||||
'name' => ['ko' => '비밀글 조회', 'en' => 'View Secret Posts'],
|
||||
'description' => ['ko' => '비밀글 조회', 'en' => 'View secret posts'],
|
||||
'scope' => ['resource_route_key' => 'post', 'owner_key' => 'user_id'],
|
||||
],
|
||||
'comments.read' => [
|
||||
'name' => ['ko' => '댓글 조회', 'en' => 'View Comments'],
|
||||
'description' => ['ko' => '댓글 목록 조회', 'en' => 'View comment list'],
|
||||
'scope' => ['resource_route_key' => 'comment', 'owner_key' => 'user_id'],
|
||||
],
|
||||
'comments.write' => [
|
||||
'name' => ['ko' => '댓글 작성/수정/삭제', 'en' => 'Create/Edit/Delete Comments'],
|
||||
'description' => ['ko' => '댓글 작성 및 본인 댓글 수정/삭제', 'en' => 'Create comments and edit/delete own comments'],
|
||||
],
|
||||
'attachments.upload' => [
|
||||
'name' => ['ko' => '파일 업로드/삭제', 'en' => 'Upload/Delete Files'],
|
||||
'description' => ['ko' => '게시글에 파일 첨부 및 삭제', 'en' => 'Attach and delete files to posts'],
|
||||
'scope' => ['resource_route_key' => 'attachment', 'owner_key' => 'created_by'],
|
||||
],
|
||||
'attachments.download' => [
|
||||
'name' => ['ko' => '파일 다운로드', 'en' => 'Download Files'],
|
||||
'description' => ['ko' => '첨부파일 다운로드', 'en' => 'Download attached files'],
|
||||
'scope' => ['resource_route_key' => 'attachment', 'owner_key' => 'created_by'],
|
||||
],
|
||||
'manager' => [
|
||||
'name' => ['ko' => '게시판 관리 (사용자)', 'en' => 'Manage Board (User)'],
|
||||
'description' => ['ko' => '사용자 페이지에서 게시판 관리', 'en' => 'Manage board in user page'],
|
||||
'scope' => ['resource_route_key' => 'board', 'owner_key' => 'created_by'],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 제한값
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'limits' => [
|
||||
'per_page_min' => 5,
|
||||
'per_page_max' => 100,
|
||||
|
||||
// 제목 길이 제한 (VARCHAR(200))
|
||||
'min_title_length_min' => 0,
|
||||
'min_title_length_max' => 200,
|
||||
'max_title_length_min' => 1,
|
||||
'max_title_length_max' => 200,
|
||||
|
||||
// 내용 길이 제한 (LONGTEXT)
|
||||
'min_content_length_min' => 0,
|
||||
'min_content_length_max' => 10000,
|
||||
'max_content_length_min' => 1,
|
||||
'max_content_length_max' => 50000,
|
||||
|
||||
// 댓글 길이 제한 (TEXT)
|
||||
'min_comment_length_min' => 0,
|
||||
'min_comment_length_max' => 1000,
|
||||
'max_comment_length_min' => 1,
|
||||
'max_comment_length_max' => 1000,
|
||||
|
||||
// 파일 업로드 제한 (MB 단위)
|
||||
'max_file_size_min' => 1,
|
||||
'max_file_size_max' => 200,
|
||||
'max_file_count_min' => 1,
|
||||
'max_file_count_max' => 20,
|
||||
|
||||
'category_max' => 50,
|
||||
|
||||
// 답글 깊이 제한
|
||||
'max_reply_depth_min' => 1,
|
||||
'max_reply_depth_max' => 10,
|
||||
|
||||
// 대댓글 깊이 제한
|
||||
'max_comment_depth_min' => 0,
|
||||
'max_comment_depth_max' => 10,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 캐시 설정
|
||||
|--------------------------------------------------------------------------
|
||||
| 게시판 통계, 최근 게시글, 인기 게시판 등에 적용되는 캐시 설정입니다.
|
||||
*/
|
||||
'cache' => [
|
||||
'enabled' => env('BOARD_CACHE_ENABLED', true),
|
||||
'ttl' => env('BOARD_CACHE_TTL', 60), // 통계/최근글/인기게시판 모두 적용 (초 단위)
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 성능 제한
|
||||
|--------------------------------------------------------------------------
|
||||
| 대용량 게시판 환경에서의 성능 최적화를 위한 설정입니다.
|
||||
*/
|
||||
'performance' => [
|
||||
'max_union_boards' => 50, // UNION 쿼리 최대 게시판 수
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": "1.0.0",
|
||||
"description": "게시판 모듈 환경설정",
|
||||
"categories": ["basic_defaults", "report_policy", "spam_security", "display"]
|
||||
},
|
||||
"defaults": {
|
||||
"basic_defaults": {
|
||||
"type": "basic",
|
||||
"per_page": 20,
|
||||
"per_page_mobile": 15,
|
||||
"order_by": "created_at",
|
||||
"order_direction": "DESC",
|
||||
"secret_mode": "disabled",
|
||||
"use_comment": true,
|
||||
"use_reply": true,
|
||||
"max_reply_depth": 5,
|
||||
"max_comment_depth": 10,
|
||||
"comment_order": "ASC",
|
||||
"show_view_count": true,
|
||||
"use_report": false,
|
||||
"min_title_length": 2,
|
||||
"max_title_length": 200,
|
||||
"min_content_length": 2,
|
||||
"max_content_length": 10000,
|
||||
"min_comment_length": 2,
|
||||
"max_comment_length": 1000,
|
||||
"use_file_upload": false,
|
||||
"max_file_size": 10,
|
||||
"max_file_count": 5,
|
||||
"allowed_extensions": ["jpg", "jpeg", "png", "gif", "webp", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "hwp", "txt", "zip"],
|
||||
"notify_admin_on_post": true,
|
||||
"notify_author": true,
|
||||
"new_display_hours": 24,
|
||||
"blocked_keywords": [
|
||||
"시발", "씨발", "ㅅㅂ", "ㅆㅂ", "좆", "ㅈㄴ", "존나", "씹",
|
||||
"지랄", "ㅈㄹ", "개새끼", "새끼", "ㅅㄲ", "개자식",
|
||||
"병신", "ㅂㅅ", "미친놈", "미친년", "느금마", "니애미", "느금",
|
||||
"엠창", "엠뒤", "빡대가리", "개같은", "개년", "개놈", "개돼지", "개씹", "개쓰레기",
|
||||
"닥쳐", "꺼져", "죽어", "자살해", "뒤져", "저능아", "정신병자",
|
||||
"썅", "쌍놈", "쌍년", "쉬발", "쒸발", "시팔", "씨팔",
|
||||
"한남", "한녀", "김치녀", "된장녀", "맘충", "틀딱", "느개비",
|
||||
"장애인놈", "병신새끼", "흑형", "짱깨", "쪽바리", "왜놈", "튀기", "똥남아",
|
||||
"야동", "포르노", "섹스", "야사", "음란", "성인사이트", "조건만남",
|
||||
"원나잇", "섹파", "오피", "키스방", "매춘", "성매매", "몸파는"
|
||||
],
|
||||
"notify_admin_on_post_channels": ["mail"],
|
||||
"notify_author_channels": ["mail"],
|
||||
"default_board_permissions": {
|
||||
"admin.posts.read": ["admin"],
|
||||
"admin.posts.write": ["admin"],
|
||||
"admin.posts.read-secret": ["admin"],
|
||||
"admin.comments.read": ["admin"],
|
||||
"admin.comments.write": ["admin"],
|
||||
"admin.attachments.upload": ["admin"],
|
||||
"admin.attachments.download": ["admin"],
|
||||
"admin.manage": ["admin"],
|
||||
"posts.read": ["admin", "user", "guest"],
|
||||
"posts.write": ["admin", "user"],
|
||||
"posts.read-secret": ["admin"],
|
||||
"comments.read": ["admin", "user", "guest"],
|
||||
"comments.write": ["admin", "user"],
|
||||
"attachments.upload": ["admin", "user"],
|
||||
"attachments.download": ["admin", "user", "guest"],
|
||||
"manager": ["admin"]
|
||||
}
|
||||
},
|
||||
"report_policy": {
|
||||
"auto_hide_threshold": 5,
|
||||
"auto_hide_target": "both",
|
||||
"daily_report_limit": 10,
|
||||
"rejection_limit_count": 5,
|
||||
"rejection_limit_days": 30,
|
||||
"notify_admin_on_report": true,
|
||||
"notify_admin_on_report_scope": "per_case",
|
||||
"notify_admin_on_report_channels": ["mail"],
|
||||
"notify_author_on_report_action": true,
|
||||
"notify_author_on_report_action_channels": ["mail"]
|
||||
},
|
||||
"spam_security": {
|
||||
"post_cooldown_seconds": 0,
|
||||
"comment_cooldown_seconds": 0,
|
||||
"report_cooldown_seconds": 60,
|
||||
"view_count_cache_ttl": 86400
|
||||
},
|
||||
"display": {
|
||||
"date_display_format": "standard"
|
||||
}
|
||||
},
|
||||
"frontend_schema": {
|
||||
"_comment": "프론트엔드에서 미참조 — 전체 카테고리 노출 제외",
|
||||
"basic_defaults": {
|
||||
"expose": false
|
||||
},
|
||||
"report_policy": {
|
||||
"expose": false
|
||||
},
|
||||
"spam_security": {
|
||||
"expose": false
|
||||
},
|
||||
"display": {
|
||||
"expose": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
|
||||
/**
|
||||
* 게시판 팩토리
|
||||
*
|
||||
* 테스트용 게시판 데이터 생성
|
||||
*/
|
||||
class BoardFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 모델 클래스
|
||||
*/
|
||||
protected $model = Board::class;
|
||||
|
||||
/**
|
||||
* 모델 기본 상태 정의
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'ko' => $this->faker->words(3, true),
|
||||
'en' => $this->faker->words(3, true),
|
||||
],
|
||||
'slug' => $this->faker->unique()->lexify('test-board-??????'),
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
'per_page' => 20,
|
||||
'per_page_mobile' => 15,
|
||||
'order_by' => 'created_at',
|
||||
'order_direction' => 'DESC',
|
||||
'secret_mode' => 'disabled',
|
||||
'use_comment' => true,
|
||||
'use_reply' => true,
|
||||
'comment_order' => 'ASC',
|
||||
'show_view_count' => false,
|
||||
'use_report' => false,
|
||||
'use_file_upload' => false,
|
||||
'max_file_size' => 10485760, // 10MB
|
||||
'max_file_count' => 5,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'zip'],
|
||||
'min_title_length' => 2,
|
||||
'max_title_length' => 200,
|
||||
'min_content_length' => 10,
|
||||
'max_content_length' => 10000,
|
||||
'min_comment_length' => 2,
|
||||
'max_comment_length' => 1000,
|
||||
'notify_admin_on_post' => true,
|
||||
'notify_author' => true,
|
||||
'blocked_keywords' => [],
|
||||
'categories' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Modules\Sirsoft\Board\Enums\ReportStatus;
|
||||
use Modules\Sirsoft\Board\Models\Report;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Modules\Sirsoft\Board\Models\Report>
|
||||
*/
|
||||
class ReportFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<\Illuminate\Database\Eloquent\Model>
|
||||
*/
|
||||
protected $model = Report::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
static $targetIdCounter = 0;
|
||||
|
||||
return [
|
||||
'board_id' => 1, // 테스트에서 오버라이드 필수
|
||||
'target_type' => fake()->randomElement(['post', 'comment']),
|
||||
'target_id' => ++$targetIdCounter,
|
||||
'author_id' => User::factory(),
|
||||
'status' => ReportStatus::Pending,
|
||||
'last_reported_at' => now(),
|
||||
'last_activated_at' => null,
|
||||
'processed_by' => null,
|
||||
'processed_at' => null,
|
||||
'process_histories' => null,
|
||||
'metadata' => [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* pending 상태로 설정
|
||||
*/
|
||||
public function pending(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ReportStatus::Pending,
|
||||
'processed_by' => null,
|
||||
'processed_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* review 상태로 설정
|
||||
*/
|
||||
public function review(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ReportStatus::Review,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* rejected 상태로 설정
|
||||
*/
|
||||
public function rejected(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ReportStatus::Rejected,
|
||||
'processed_by' => 1,
|
||||
'processed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* suspended 상태로 설정
|
||||
*/
|
||||
public function suspended(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ReportStatus::Suspended,
|
||||
'processed_by' => 1,
|
||||
'processed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* post 타입으로 설정
|
||||
*/
|
||||
public function post(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'target_type' => 'post',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* comment 타입으로 설정
|
||||
*/
|
||||
public function comment(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'target_type' => 'comment',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Modules\Sirsoft\Board\Enums\ReportReasonType;
|
||||
use Modules\Sirsoft\Board\Models\ReportLog;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Modules\Sirsoft\Board\Models\ReportLog>
|
||||
*/
|
||||
class ReportLogFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<\Illuminate\Database\Eloquent\Model>
|
||||
*/
|
||||
protected $model = ReportLog::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'report_id' => null, // 테스트에서 오버라이드 필수
|
||||
'reporter_id' => User::factory(),
|
||||
'snapshot' => [
|
||||
'board_name' => fake()->word(),
|
||||
'title' => fake()->sentence(),
|
||||
'content' => fake()->paragraph(),
|
||||
'content_mode' => 'text',
|
||||
'author_name' => fake()->name(),
|
||||
],
|
||||
'reason_type' => fake()->randomElement(ReportReasonType::cases())->value,
|
||||
'reason_detail' => fake()->sentence(),
|
||||
'metadata' => [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 게시판 유형
|
||||
Schema::create('board_types', function (Blueprint $table) {
|
||||
$table->id()->comment('게시판 유형 ID');
|
||||
$table->string('slug', 50)->unique()->comment('유형 식별자 (basic, card, gallery 등)');
|
||||
$table->text('name')->comment('유형명 (다국어: {"ko": "기본형", "en": "Basic List"})');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('board_types')) {
|
||||
Schema::dropIfExists('board_types');
|
||||
}
|
||||
}
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 게시판 설정
|
||||
Schema::create('boards', function (Blueprint $table) {
|
||||
$table->id()->comment('게시판 ID');
|
||||
$table->text('name')->comment('게시판명 (다국어 JSON)');
|
||||
$table->string('slug', 50)->unique()->comment('게시판 슬러그 (URL/테이블명)');
|
||||
$table->mediumText('description')->nullable()->comment('게시판 설명 (다국어 JSON)');
|
||||
$table->boolean('is_active')->default(true)->comment('게시판 활성화 여부 (1: 활성화, 0: 비활성화)');
|
||||
$table->unsignedInteger('per_page')->default(20)->comment('페이지당 게시글 수 (PC)');
|
||||
$table->unsignedInteger('per_page_mobile')->default(15)->comment('페이지당 게시글 수 (Mobile)');
|
||||
$table->string('order_by', 20)->default('created_at')->comment('정렬 기준 (created_at, view_count, title, author)');
|
||||
$table->string('order_direction', 4)->default('DESC')->comment('정렬 방향 (ASC, DESC)');
|
||||
$table->string('type', 50)->default('basic')->index()->comment('게시판 타입 (basic, gallery, card 등)');
|
||||
$table->text('categories')->nullable()->comment('분류 목록 (배열)');
|
||||
$table->boolean('show_view_count')->default(false)->comment('조회수 노출');
|
||||
$table->enum('secret_mode', ['disabled', 'enabled', 'always'])->default('disabled')->comment('비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정)');
|
||||
$table->boolean('use_comment')->default(true)->comment('댓글 기능 사용');
|
||||
$table->boolean('use_reply')->default(true)->comment('게시글 답변 기능 사용 (댓글에 대한 답글 아님)');
|
||||
$table->unsignedSmallInteger('max_reply_depth')->default(5)->comment('답변글 최대 깊이 (1~5)');
|
||||
$table->boolean('use_report')->default(false)->comment('게시글/댓글 신고 기능 사용');
|
||||
$table->unsignedInteger('new_display_hours')->default(24)->comment('신규 게시글 표시 기간 (시간 단위)');
|
||||
$table->unsignedInteger('min_title_length')->default(2)->comment('최소 제목 글자 수');
|
||||
$table->unsignedInteger('max_title_length')->default(200)->comment('최대 제목 글자 수');
|
||||
$table->unsignedInteger('min_content_length')->default(10)->comment('최소 게시글 글자 수');
|
||||
$table->unsignedInteger('max_content_length')->default(10000)->comment('최대 게시글 글자 수');
|
||||
$table->unsignedInteger('min_comment_length')->default(2)->comment('최소 댓글 글자 수');
|
||||
$table->unsignedInteger('max_comment_length')->default(1000)->comment('최대 댓글 글자 수');
|
||||
$table->boolean('use_file_upload')->default(false)->comment('파일 업로드 사용');
|
||||
$table->unsignedInteger('max_file_size')->nullable()->default(10)->comment('최대 파일 크기 (MB)');
|
||||
$table->unsignedInteger('max_file_count')->nullable()->default(5)->comment('최대 파일 개수');
|
||||
$table->text('allowed_extensions')->nullable()->comment('허용 확장자 배열');
|
||||
$table->string('comment_order', 4)->default('ASC')->comment('댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순)');
|
||||
$table->unsignedSmallInteger('max_comment_depth')->default(10)->comment('대댓글 최대 깊이 (1~10)');
|
||||
$table->boolean('notify_author')->default(true)->comment('작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시)');
|
||||
$table->boolean('notify_admin_on_post')->default(true)->comment('관리자 이메일 알림 (게시글 등록 시)');
|
||||
$table->text('notify_author_channels')->nullable()->comment('작성자 알림 채널 (배열, 예: ["mail", "database"])');
|
||||
$table->text('notify_admin_on_post_channels')->nullable()->comment('관리자 알림 채널 (배열, 예: ["mail", "database"])');
|
||||
$table->text('blocked_keywords')->nullable()->comment('금지어 목록 (배열)');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->index()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('created_by')->references('id')->on('users')->nullOnDelete();
|
||||
$table->foreign('updated_by')->references('id')->on('users')->nullOnDelete();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('boards', function (Blueprint $table) {
|
||||
$table->comment('게시판 설정 정보');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('boards')) {
|
||||
Schema::dropIfExists('boards');
|
||||
}
|
||||
}
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 사용자 알림 설정
|
||||
Schema::create('board_user_notification_settings', function (Blueprint $table) {
|
||||
$table->id()->comment('알림 설정 ID');
|
||||
$table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete();
|
||||
$table->boolean('notify_post_complete')->default(false)->comment('게시글 완료 알림');
|
||||
$table->boolean('notify_post_reply')->default(false)->comment('게시글 답변 알림');
|
||||
$table->boolean('notify_comment')->default(false)->comment('댓글 알림');
|
||||
$table->boolean('notify_reply_comment')->default(false)->comment('답글 알림');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('board_user_notification_settings', function (Blueprint $table) {
|
||||
$table->comment('게시판 사용자 알림 설정');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('board_user_notification_settings')) {
|
||||
Schema::dropIfExists('board_user_notification_settings');
|
||||
}
|
||||
}
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* MySQL/MariaDB 전용: PARTITION BY LIST (board_id) 파티션 적용
|
||||
* 파티션 테이블에는 FK 설정 불가 — 앱 계층에서 무결성 처리
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 게시글
|
||||
Schema::create('board_posts', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('id', true)->comment('게시글 ID');
|
||||
$table->unsignedBigInteger('board_id')->comment('게시판 ID (파티션 키)');
|
||||
$table->primary(['id', 'board_id']);
|
||||
|
||||
$table->string('category', 50)->nullable()->index()->comment('분류');
|
||||
$table->string('title', 200)->comment('제목');
|
||||
$table->longText('content')->comment('내용');
|
||||
$table->string('content_mode', 10)->default('text')->comment('콘텐츠 모드: text(텍스트), html(HTML)');
|
||||
|
||||
$table->unsignedBigInteger('user_id')->nullable()->index()->comment('회원 ID');
|
||||
$table->string('author_name', 50)->nullable()->comment('작성자명 (비회원)');
|
||||
$table->string('password')->nullable()->comment('비밀번호 (비회원, bcrypt)');
|
||||
$table->string('ip_address', 45)->comment('IP 주소');
|
||||
|
||||
$table->boolean('is_notice')->default(false)->comment('공지사항 여부 (1: 공지, 0: 일반)');
|
||||
$table->boolean('is_secret')->default(false)->comment('비밀글 여부 (1: 비밀, 0: 공개)');
|
||||
$table->enum('status', ['published', 'blinded', 'deleted'])->default('published')->comment('게시글 상태 (published: 게시됨, blinded: 블라인드 처리됨, deleted: 삭제됨)');
|
||||
$table->enum('trigger_type', ['report', 'admin', 'system', 'auto_hide', 'user'])->default('admin')->comment('조치 주체 (report: 신고, admin: 관리자, system: 시스템, auto_hide: 자동 블라인드, user: 사용자 직접 삭제)');
|
||||
$table->text('action_logs')->nullable()->comment('작업 이력 배열 (JSON)');
|
||||
|
||||
$table->unsignedInteger('view_count')->default(0)->index()->comment('조회수');
|
||||
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->index()->comment('부모 게시글 ID (답글용)');
|
||||
$table->unsignedInteger('depth')->default(0)->comment('답글 깊이 (0: 원글, 1+: 답글)');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('deleted_at');
|
||||
$table->index(['board_id', 'created_at'], 'idx_board_created');
|
||||
$table->index(['board_id', 'status'], 'idx_board_status');
|
||||
$table->index(['board_id', 'is_notice'], 'idx_board_notice');
|
||||
$table->index(['user_id', 'created_at'], 'idx_user_created');
|
||||
$table->index(['user_id', 'view_count'], 'idx_user_views');
|
||||
});
|
||||
|
||||
DB::statement("ALTER TABLE {$prefix}board_posts PARTITION BY LIST (board_id) (PARTITION p_default VALUES IN (0))");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('board_posts')) {
|
||||
Schema::dropIfExists('board_posts');
|
||||
}
|
||||
}
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* MySQL/MariaDB 전용: PARTITION BY LIST (board_id) 파티션 적용
|
||||
* 파티션 테이블에는 FK 설정 불가 — 앱 계층에서 무결성 처리
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 댓글
|
||||
Schema::create('board_comments', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('id', true)->comment('댓글 ID');
|
||||
$table->unsignedBigInteger('board_id')->comment('게시판 ID (파티션 키)');
|
||||
$table->primary(['id', 'board_id']);
|
||||
|
||||
$table->unsignedBigInteger('post_id')->comment('게시글 ID');
|
||||
$table->unsignedBigInteger('user_id')->nullable()->index()->comment('작성자 ID (회원)');
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->index()->comment('부모 댓글 ID (답글용)');
|
||||
$table->string('author_name', 50)->nullable()->comment('작성자명 (비회원용)');
|
||||
$table->string('password')->nullable()->comment('비밀번호 (비회원용, 해시 저장)');
|
||||
$table->text('content')->comment('댓글 내용');
|
||||
$table->boolean('is_secret')->default(false)->comment('비밀 댓글 여부 (1: 비밀, 0: 공개)');
|
||||
$table->enum('status', ['published', 'blinded', 'deleted'])->default('published')->comment('댓글 상태 (published: 게시됨, blinded: 블라인드 처리됨, deleted: 삭제됨)');
|
||||
$table->enum('trigger_type', ['report', 'admin', 'system', 'auto_hide', 'user'])->default('admin')->comment('조치 주체 (report: 신고, admin: 관리자, system: 시스템, auto_hide: 자동 블라인드, user: 사용자 직접 삭제)');
|
||||
$table->text('action_logs')->nullable()->comment('작업 이력 배열 (JSON)');
|
||||
$table->unsignedTinyInteger('depth')->default(0)->comment('댓글 깊이 (0: 댓글, 1: 답글)');
|
||||
$table->string('ip_address', 45)->nullable()->comment('작성자 IP');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('deleted_at');
|
||||
$table->index(['board_id', 'post_id'], 'idx_board_post');
|
||||
$table->index(['board_id', 'status'], 'idx_board_status');
|
||||
$table->index(['board_id', 'created_at'], 'idx_board_created');
|
||||
$table->index(['user_id', 'post_id'], 'idx_user_post');
|
||||
$table->index(['user_id', 'created_at'], 'idx_user_created_at');
|
||||
});
|
||||
|
||||
DB::statement("ALTER TABLE {$prefix}board_comments PARTITION BY LIST (board_id) (PARTITION p_default VALUES IN (0))");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('board_comments')) {
|
||||
Schema::dropIfExists('board_comments');
|
||||
}
|
||||
}
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* MySQL/MariaDB 전용: PARTITION BY LIST (board_id) 파티션 적용
|
||||
* 파티션 테이블에는 FK 설정 불가 — 앱 계층에서 무결성 처리
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 첨부파일
|
||||
Schema::create('board_attachments', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('id', true)->comment('첨부파일 ID');
|
||||
$table->unsignedBigInteger('board_id')->default(0)->comment('게시판 ID (파티션 키, 0: 임시 업로드)');
|
||||
$table->primary(['id', 'board_id']);
|
||||
|
||||
$table->unsignedBigInteger('post_id')->nullable()->comment('게시글 ID (임시 업로드 시 null)');
|
||||
$table->string('temp_key', 64)->nullable()->comment('임시 업로드 키 (게시글 저장 후 null)');
|
||||
$table->string('hash', 12)->comment('URL용 고유 해시 (12자)');
|
||||
$table->string('original_filename', 255)->comment('원본 파일명');
|
||||
$table->string('stored_filename', 255)->comment('저장된 파일명 (UUID 기반)');
|
||||
$table->string('disk', 50)->default('local')->comment('스토리지 디스크 (local, s3 등)');
|
||||
$table->string('path', 500)->comment('저장 경로 (디스크 기준 상대 경로)');
|
||||
$table->string('mime_type', 100)->comment('MIME 타입 (예: image/jpeg, application/pdf)');
|
||||
$table->unsignedBigInteger('size')->comment('파일 크기 (바이트)');
|
||||
$table->string('collection', 100)->default('default')->comment('첨부파일 컬렉션/그룹명');
|
||||
$table->unsignedInteger('order')->default(0)->comment('정렬 순서');
|
||||
$table->text('meta')->nullable()->comment('추가 메타데이터 JSON (썸네일 경로, 이미지 크기 등)');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('업로더 ID');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['board_id', 'hash'], 'uq_board_hash');
|
||||
$table->index(['board_id', 'post_id'], 'idx_board_post');
|
||||
$table->index(['board_id', 'temp_key'], 'idx_board_temp_key');
|
||||
$table->index('collection', 'idx_collection');
|
||||
$table->index('deleted_at', 'idx_deleted_at');
|
||||
$table->index('hash', 'idx_hash');
|
||||
});
|
||||
|
||||
DB::statement("ALTER TABLE {$prefix}board_attachments PARTITION BY LIST (board_id) (PARTITION p_default VALUES IN (0))");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('board_attachments')) {
|
||||
Schema::dropIfExists('board_attachments');
|
||||
}
|
||||
}
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 신고 케이스
|
||||
Schema::create('boards_reports', function (Blueprint $table) {
|
||||
$table->id()->comment('신고 케이스 ID');
|
||||
$table->unsignedBigInteger('board_id')->nullable()->comment('게시판 ID (게시판 삭제 시 NULL)');
|
||||
$table->string('target_type', 20)->comment('신고 대상 타입 (post, comment)');
|
||||
$table->unsignedBigInteger('target_id')->comment('신고 대상 ID (동적 테이블의 ID)');
|
||||
$table->unsignedBigInteger('author_id')->nullable()->comment('작성자 ID (작성자 삭제 시 NULL)');
|
||||
$table->string('status', 20)->default('pending')->comment('신고 상태 (pending, review, rejected, suspended)');
|
||||
$table->unsignedBigInteger('processed_by')->nullable()->comment('처리자 ID');
|
||||
$table->timestamp('processed_at')->nullable()->comment('처리 일시');
|
||||
$table->mediumText('process_histories')->nullable()->comment('처리 이력 배열 [{type, admin_id, admin_name, reason, reporter_count, created_at}]');
|
||||
$table->text('metadata')->nullable()->comment('메타데이터 (IP, User Agent 등)');
|
||||
$table->timestamp('last_reported_at')->nullable()->comment('마지막 신고 일시 — 목록 정렬용');
|
||||
$table->timestamp('last_activated_at')->nullable()->comment('케이스 재활성 일시 — 자동 블라인드 카운트 기준 (재신고 시 갱신)');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['board_id', 'target_type', 'target_id'], 'unique_report_case');
|
||||
$table->index('board_id', 'idx_board');
|
||||
$table->index(['board_id', 'target_type', 'target_id'], 'idx_target');
|
||||
$table->index('author_id', 'idx_author');
|
||||
$table->index('status', 'idx_status');
|
||||
$table->index('last_reported_at', 'idx_last_reported');
|
||||
|
||||
$table->foreign('board_id')->references('id')->on('boards')->nullOnDelete();
|
||||
$table->foreign('author_id')->references('id')->on('users')->nullOnDelete();
|
||||
$table->foreign('processed_by')->references('id')->on('users')->nullOnDelete();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('boards_reports', function (Blueprint $table) {
|
||||
$table->comment('게시판 신고 케이스 (게시글/댓글당 1행)');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('boards_reports')) {
|
||||
Schema::dropIfExists('boards_reports');
|
||||
}
|
||||
}
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 신고 로그
|
||||
Schema::create('boards_report_logs', function (Blueprint $table) {
|
||||
$table->id()->comment('신고 로그 ID');
|
||||
$table->foreignId('report_id')->constrained('boards_reports')->cascadeOnDelete()->comment('케이스 ID (boards_reports.id)');
|
||||
$table->unsignedBigInteger('reporter_id')->nullable()->comment('신고자 ID (탈퇴 시 NULL)');
|
||||
$table->mediumText('snapshot')->nullable()->comment('신고 당시 게시물 스냅샷 (JSON: board_name, title, content, content_mode, author_name)');
|
||||
$table->string('reason_type', 50)->nullable()->comment('신고 사유 유형 (abuse, hate_speech, spam, copyright, privacy, misinformation, sexual, violence, other)');
|
||||
$table->text('reason_detail')->nullable()->comment('신고 상세 사유 (신고자 입력)');
|
||||
$table->text('metadata')->nullable()->comment('메타데이터 (IP, User Agent 등)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['report_id', 'reporter_id'], 'unique_report_log');
|
||||
$table->index('report_id', 'idx_report');
|
||||
$table->index('reporter_id', 'idx_reporter');
|
||||
|
||||
$table->foreign('reporter_id')->references('id')->on('users')->nullOnDelete();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() == 'mysql') {
|
||||
Schema::table('boards_report_logs', function (Blueprint $table) {
|
||||
$table->comment('게시판 신고 로그 (신고자별 기록)');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('boards_report_logs')) {
|
||||
Schema::dropIfExists('boards_report_logs');
|
||||
}
|
||||
}
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('board_mail_templates', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->string('type', 100)->unique()->comment('템플릿 유형 (new_comment, reply_comment 등)');
|
||||
$table->text('subject')->comment('다국어 제목 ({"ko": "...", "en": "..."})');
|
||||
$table->mediumText('body')->comment('다국어 HTML 본문 ({"ko": "...", "en": "..."})');
|
||||
$table->text('variables')->nullable()->comment('사용 가능 변수 메타데이터 ([{key, description}])');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 여부');
|
||||
$table->boolean('is_default')->default(true)->comment('시더 생성 항목 여부');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('board_mail_templates')) {
|
||||
Schema::dropIfExists('board_mail_templates');
|
||||
}
|
||||
}
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use App\Search\Engines\DatabaseFulltextEngine;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* boards 테이블에 FULLTEXT 인덱스 추가
|
||||
*
|
||||
* name 컬럼에 대해 MATCH...AGAINST 검색을 지원합니다.
|
||||
* FULLTEXT 미지원 DBMS에서는 자동 스킵됩니다.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 마이그레이션을 실행합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DatabaseFulltextEngine::addFulltextIndex('boards', 'ft_boards_name', 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 마이그레이션을 롤백합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('boards')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$indexes = array_column(Schema::getIndexes('boards'), 'name');
|
||||
|
||||
Schema::table('boards', function (Blueprint $table) use ($indexes) {
|
||||
if (in_array('ft_boards_name', $indexes)) {
|
||||
$table->dropIndex('ft_boards_name');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use App\Search\Engines\DatabaseFulltextEngine;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* boards_report_logs 테이블에 FULLTEXT 인덱스 추가
|
||||
*
|
||||
* snapshot 컬럼에 대해 MATCH...AGAINST 검색을 지원합니다.
|
||||
* FULLTEXT 미지원 DBMS에서는 자동 스킵됩니다.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 마이그레이션을 실행합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DatabaseFulltextEngine::addFulltextIndex('boards_report_logs', 'ft_boards_report_logs_snapshot', 'snapshot');
|
||||
}
|
||||
|
||||
/**
|
||||
* 마이그레이션을 롤백합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('boards_report_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$indexes = array_column(Schema::getIndexes('boards_report_logs'), 'name');
|
||||
|
||||
Schema::table('boards_report_logs', function (Blueprint $table) use ($indexes) {
|
||||
if (in_array('ft_boards_report_logs_snapshot', $indexes)) {
|
||||
$table->dropIndex('ft_boards_report_logs_snapshot');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* board_posts 테이블 인덱스 추가. (LIST 파티션 테이블)
|
||||
*
|
||||
* - [board_id, author_name]: 비회원 작성자 검색 (파티션 프루닝)
|
||||
* - [board_id, status, created_at]: 게시판별 상태+날짜 필터 (가장 빈번한 패턴)
|
||||
*
|
||||
* 참고: [board_id, status, created_at]는 기존 [board_id, status] 인덱스를 포함합니다.
|
||||
* 기존 인덱스 제거는 별도 후속 작업으로 처리합니다.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('board_posts', function (Blueprint $table) {
|
||||
$table->index(['board_id', 'author_name'], 'idx_board_posts_board_author');
|
||||
$table->index(['board_id', 'status', 'created_at'], 'idx_board_posts_board_status_created');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('board_posts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingIndexes = array_column(Schema::getIndexes('board_posts'), 'name');
|
||||
|
||||
Schema::table('board_posts', function (Blueprint $table) use ($existingIndexes) {
|
||||
$indexes = [
|
||||
'idx_board_posts_board_author',
|
||||
'idx_board_posts_board_status_created',
|
||||
];
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
if (in_array($index, $existingIndexes)) {
|
||||
$table->dropIndex($index);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,760 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders;
|
||||
|
||||
use App\Enums\ActivityLogType;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Modules\Sirsoft\Board\Enums\PostStatus;
|
||||
use Modules\Sirsoft\Board\Enums\ReportStatus;
|
||||
use Modules\Sirsoft\Board\Models\Attachment;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Models\BoardType;
|
||||
use Modules\Sirsoft\Board\Models\Comment;
|
||||
use Modules\Sirsoft\Board\Models\Post;
|
||||
use Modules\Sirsoft\Board\Models\Report;
|
||||
|
||||
/**
|
||||
* 게시판 활동 로그 샘플 시더
|
||||
*
|
||||
* 리소스 중심 rand(1,50) 방식으로 ActivityLog를 생성합니다.
|
||||
* 각 리소스마다 rand(1,50)개의 랜덤 액션 로그를 생성합니다.
|
||||
*/
|
||||
class ActivityLogSampleSeeder extends Seeder
|
||||
{
|
||||
/** @var string 다국어 키 접두사 */
|
||||
private const PREFIX = 'sirsoft-board::activity_log.description.';
|
||||
|
||||
/** @var array<string> 샘플 IP 목록 */
|
||||
private const IPS = ['192.168.1.10', '10.0.0.5', '172.16.0.1', '192.168.0.100', '10.10.10.1'];
|
||||
|
||||
/** @var array<string> 샘플 User-Agent 목록 */
|
||||
private const USER_AGENTS = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/125.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) Firefox/126.0',
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더를 실행합니다.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$admins = User::whereHas('roles', fn ($q) => $q->where('identifier', 'admin'))->get();
|
||||
if ($admins->isEmpty()) {
|
||||
$this->command->warn('관리자 사용자가 없어 게시판 활동 로그 시더를 건너뜁니다.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$users = User::whereDoesntHave('roles', fn ($q) => $q->where('identifier', 'admin'))->get();
|
||||
if ($users->isEmpty()) {
|
||||
$users = $admins;
|
||||
}
|
||||
|
||||
// 기존 게시판 활동 로그 삭제
|
||||
$deleted = ActivityLog::where('description_key', 'like', 'sirsoft-board::%')->delete();
|
||||
if ($deleted > 0) {
|
||||
$this->command->info("기존 게시판 활동 로그 {$deleted}건 삭제.");
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
$count += $this->seedBoardLogs($admins);
|
||||
$count += $this->seedBoardTypeLogs($admins);
|
||||
$count += $this->seedPostLogs($admins, $users);
|
||||
$count += $this->seedCommentLogs($admins, $users);
|
||||
$count += $this->seedAttachmentLogs($admins, $users);
|
||||
$count += $this->seedReportLogs($admins);
|
||||
|
||||
$this->command->info("게시판 활동 로그 {$count}건 생성 완료.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 활동 로그 (리소스별 rand(1,50)건)
|
||||
*
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function seedBoardLogs(Collection $admins): int
|
||||
{
|
||||
$boards = Board::get();
|
||||
if ($boards->isEmpty()) {
|
||||
$this->command->warn('게시판 데이터가 없어 게시판 활동 로그를 건너뜁니다.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new Board)->getMorphClass();
|
||||
|
||||
$actions = [
|
||||
[
|
||||
'action' => 'board.create',
|
||||
'key' => 'board_create',
|
||||
'loggable' => true,
|
||||
'params' => fn (Board $board) => ['board_name' => $this->getLocalizedName($board->name)],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board.update',
|
||||
'key' => 'board_update',
|
||||
'loggable' => true,
|
||||
'params' => fn (Board $board) => ['board_name' => $this->getLocalizedName($board->name)],
|
||||
'changes' => fn (Board $board) => [
|
||||
['field' => 'name', 'label_key' => 'sirsoft-board::activity_log.fields.name', 'old' => $this->getLocalizedName($board->name).' (수정 전)', 'new' => $this->getLocalizedName($board->name), 'type' => 'text'],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board.show',
|
||||
'key' => 'board_show',
|
||||
'loggable' => true,
|
||||
'params' => fn (Board $board) => ['board_name' => $this->getLocalizedName($board->name)],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board.delete',
|
||||
'key' => 'board_delete',
|
||||
'loggable' => false,
|
||||
'params' => fn (Board $board) => ['board_name' => $this->getLocalizedName($board->name)],
|
||||
'changes' => null,
|
||||
'properties' => fn (Board $board) => ['deleted_id' => $board->id, 'slug' => $board->slug, 'name' => $this->getLocalizedName($board->name)],
|
||||
],
|
||||
[
|
||||
'action' => 'board.add_to_menu',
|
||||
'key' => 'board_add_to_menu',
|
||||
'loggable' => true,
|
||||
'params' => fn (Board $board) => ['board_name' => $this->getLocalizedName($board->name)],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board_settings.bulk_apply',
|
||||
'key' => 'board_settings_bulk_apply',
|
||||
'loggable' => true,
|
||||
'params' => fn (Board $board) => ['board_name' => $this->getLocalizedName($board->name)],
|
||||
'changes' => fn (Board $board) => [
|
||||
['field' => 'per_page', 'label_key' => 'sirsoft-board::activity_log.fields.per_page', 'old' => 10, 'new' => 20, 'type' => 'number'],
|
||||
['field' => 'use_comment', 'label_key' => 'sirsoft-board::activity_log.fields.use_comment', 'old' => false, 'new' => true, 'type' => 'boolean'],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->generateResourceLogs($boards, $admins, ActivityLogType::Admin, $morphType, $actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 유형 활동 로그 (리소스별 rand(1,50)건)
|
||||
*
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function seedBoardTypeLogs(Collection $admins): int
|
||||
{
|
||||
$types = BoardType::get();
|
||||
if ($types->isEmpty()) {
|
||||
$this->command->warn('게시판 유형 데이터가 없어 게시판 유형 활동 로그를 건너뜁니다.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new BoardType)->getMorphClass();
|
||||
|
||||
$actions = [
|
||||
[
|
||||
'action' => 'board_type.create',
|
||||
'key' => 'board_type_create',
|
||||
'loggable' => true,
|
||||
'params' => fn (BoardType $type) => ['type_name' => $this->getLocalizedName($type->name)],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board_type.update',
|
||||
'key' => 'board_type_update',
|
||||
'loggable' => true,
|
||||
'params' => fn (BoardType $type) => ['type_name' => $this->getLocalizedName($type->name)],
|
||||
'changes' => fn (BoardType $type) => [
|
||||
['field' => 'name', 'label_key' => 'sirsoft-board::activity_log.fields.name', 'old' => $this->getLocalizedName($type->name).' (수정 전)', 'new' => $this->getLocalizedName($type->name), 'type' => 'text'],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board_type.show',
|
||||
'key' => 'board_type_show',
|
||||
'loggable' => true,
|
||||
'params' => fn (BoardType $type) => ['type_name' => $this->getLocalizedName($type->name)],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'board_type.delete',
|
||||
'key' => 'board_type_delete',
|
||||
'loggable' => false,
|
||||
'params' => fn (BoardType $type) => ['type_name' => $this->getLocalizedName($type->name)],
|
||||
'changes' => null,
|
||||
'properties' => fn (BoardType $type) => ['deleted_id' => $type->id, 'slug' => $type->slug],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->generateResourceLogs($types, $admins, ActivityLogType::Admin, $morphType, $actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 활동 로그 (리소스별 rand(1,50)건)
|
||||
*
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @param Collection $users 일반 사용자 컬렉션
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function seedPostLogs(Collection $admins, Collection $users): int
|
||||
{
|
||||
$posts = Post::with('board')->get();
|
||||
if ($posts->isEmpty()) {
|
||||
$this->command->warn('게시글 데이터가 없어 게시글 활동 로그를 건너뜁니다.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new Post)->getMorphClass();
|
||||
|
||||
$adminActions = [
|
||||
[
|
||||
'action' => 'post.create',
|
||||
'key' => 'post_create',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Post $post) => ['title' => $post->title, 'board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음'],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'post.update',
|
||||
'key' => 'post_update',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Post $post) => ['title' => $post->title, 'board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음'],
|
||||
'changes' => fn (Post $post) => [
|
||||
['field' => 'title', 'label_key' => 'sirsoft-board::activity_log.fields.title', 'old' => $post->title.' (수정 전)', 'new' => $post->title, 'type' => 'text'],
|
||||
['field' => 'content', 'label_key' => 'sirsoft-board::activity_log.fields.content', 'old' => '이전 내용...', 'new' => $post->content ?? '수정된 내용', 'type' => 'text'],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'post.delete',
|
||||
'key' => 'post_delete',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => false,
|
||||
'params' => fn (Post $post) => ['board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음', 'post_id' => $post->id],
|
||||
'changes' => null,
|
||||
'properties' => fn (Post $post) => ['deleted_id' => $post->id, 'board_id' => $post->board_id, 'title' => $post->title, 'user_id' => $post->user_id],
|
||||
],
|
||||
[
|
||||
'action' => 'post.blind',
|
||||
'key' => 'post_blind',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Post $post) => ['board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음', 'post_id' => $post->id],
|
||||
'changes' => fn (Post $post) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => $post->status?->value ?? PostStatus::Published->value, 'new' => PostStatus::Blinded->value, 'type' => 'enum',
|
||||
'old_label_key' => PostStatus::Published->labelKey(), 'new_label_key' => PostStatus::Blinded->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'post.restore',
|
||||
'key' => 'post_restore',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Post $post) => ['board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음', 'post_id' => $post->id],
|
||||
'changes' => fn (Post $post) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => PostStatus::Blinded->value, 'new' => PostStatus::Published->value, 'type' => 'enum',
|
||||
'old_label_key' => PostStatus::Blinded->labelKey(), 'new_label_key' => PostStatus::Published->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
];
|
||||
|
||||
$userActions = [
|
||||
[
|
||||
'action' => 'post.create',
|
||||
'key' => 'post_create',
|
||||
'logType' => ActivityLogType::User,
|
||||
'loggable' => true,
|
||||
'params' => fn (Post $post) => ['title' => $post->title, 'board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음'],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
'userIdResolver' => fn (Post $post, Collection $actors) => $post->user_id ?? $actors->random()->id,
|
||||
],
|
||||
[
|
||||
'action' => 'post.update',
|
||||
'key' => 'post_update',
|
||||
'logType' => ActivityLogType::User,
|
||||
'loggable' => true,
|
||||
'params' => fn (Post $post) => ['title' => $post->title, 'board_name' => $post->board ? $this->getLocalizedName($post->board->name) : '알 수 없음'],
|
||||
'changes' => fn (Post $post) => [
|
||||
['field' => 'title', 'label_key' => 'sirsoft-board::activity_log.fields.title', 'old' => $post->title.' (수정 전)', 'new' => $post->title, 'type' => 'text'],
|
||||
],
|
||||
'properties' => null,
|
||||
'userIdResolver' => fn (Post $post, Collection $actors) => $post->user_id ?? $actors->random()->id,
|
||||
],
|
||||
];
|
||||
|
||||
$allActions = array_merge($adminActions, $userActions);
|
||||
|
||||
$count = $this->generateResourceLogsWithMixedTypes($posts, $admins, $users, $morphType, $allActions);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 활동 로그 (리소스별 rand(1,50)건)
|
||||
*
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @param Collection $users 일반 사용자 컬렉션
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function seedCommentLogs(Collection $admins, Collection $users): int
|
||||
{
|
||||
$comments = Comment::with('post.board')->get();
|
||||
if ($comments->isEmpty()) {
|
||||
$this->command->warn('댓글 데이터가 없어 댓글 활동 로그를 건너뜁니다.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new Comment)->getMorphClass();
|
||||
|
||||
$adminActions = [
|
||||
[
|
||||
'action' => 'comment.create',
|
||||
'key' => 'comment_create',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Comment $comment) => ['board_name' => $comment->post?->board ? $this->getLocalizedName($comment->post->board->name) : '알 수 없음', 'post_id' => $comment->post_id],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'comment.delete',
|
||||
'key' => 'comment_delete',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => false,
|
||||
'params' => fn (Comment $comment) => ['comment_id' => $comment->id],
|
||||
'changes' => null,
|
||||
'properties' => fn (Comment $comment) => ['deleted_id' => $comment->id, 'post_id' => $comment->post_id, 'user_id' => $comment->user_id],
|
||||
],
|
||||
[
|
||||
'action' => 'comment.blind',
|
||||
'key' => 'comment_blind',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Comment $comment) => ['comment_id' => $comment->id],
|
||||
'changes' => fn (Comment $comment) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => $comment->status?->value ?? PostStatus::Published->value, 'new' => PostStatus::Blinded->value, 'type' => 'enum',
|
||||
'old_label_key' => PostStatus::Published->labelKey(), 'new_label_key' => PostStatus::Blinded->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'comment.restore',
|
||||
'key' => 'comment_restore',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => true,
|
||||
'params' => fn (Comment $comment) => ['comment_id' => $comment->id],
|
||||
'changes' => fn (Comment $comment) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => PostStatus::Blinded->value, 'new' => PostStatus::Published->value, 'type' => 'enum',
|
||||
'old_label_key' => PostStatus::Blinded->labelKey(), 'new_label_key' => PostStatus::Published->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
];
|
||||
|
||||
$userActions = [
|
||||
[
|
||||
'action' => 'comment.create',
|
||||
'key' => 'comment_create',
|
||||
'logType' => ActivityLogType::User,
|
||||
'loggable' => true,
|
||||
'params' => fn (Comment $comment) => ['board_name' => $comment->post?->board ? $this->getLocalizedName($comment->post->board->name) : '알 수 없음', 'post_id' => $comment->post_id],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
'userIdResolver' => fn (Comment $comment, Collection $actors) => $comment->user_id ?? $actors->random()->id,
|
||||
],
|
||||
[
|
||||
'action' => 'comment.update',
|
||||
'key' => 'comment_update',
|
||||
'logType' => ActivityLogType::User,
|
||||
'loggable' => true,
|
||||
'params' => fn (Comment $comment) => ['comment_id' => $comment->id],
|
||||
'changes' => fn (Comment $comment) => [
|
||||
['field' => 'content', 'label_key' => 'sirsoft-board::activity_log.fields.content', 'old' => ($comment->content ?? '이전 댓글').' (수정 전)', 'new' => $comment->content ?? '수정된 댓글', 'type' => 'text'],
|
||||
],
|
||||
'properties' => null,
|
||||
'userIdResolver' => fn (Comment $comment, Collection $actors) => $comment->user_id ?? $actors->random()->id,
|
||||
],
|
||||
];
|
||||
|
||||
$allActions = array_merge($adminActions, $userActions);
|
||||
|
||||
return $this->generateResourceLogsWithMixedTypes($comments, $admins, $users, $morphType, $allActions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 첨부파일 활동 로그 (리소스별 rand(1,50)건)
|
||||
*
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @param Collection $users 일반 사용자 컬렉션
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function seedAttachmentLogs(Collection $admins, Collection $users): int
|
||||
{
|
||||
$attachments = Attachment::get();
|
||||
if ($attachments->isEmpty()) {
|
||||
$this->command->warn('첨부파일 데이터가 없어 첨부파일 활동 로그를 건너뜁니다.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new Attachment)->getMorphClass();
|
||||
|
||||
$allActions = [
|
||||
[
|
||||
'action' => 'attachment.upload',
|
||||
'key' => 'board_attachment_upload',
|
||||
'logType' => null,
|
||||
'loggable' => true,
|
||||
'params' => fn (Attachment $attachment) => ['post_id' => $attachment->attachable_id],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
'userIdResolver' => fn (Attachment $attachment, Collection $actors) => $attachment->created_by ?? $actors->random()->id,
|
||||
],
|
||||
[
|
||||
'action' => 'attachment.delete',
|
||||
'key' => 'board_attachment_delete',
|
||||
'logType' => ActivityLogType::Admin,
|
||||
'loggable' => false,
|
||||
'params' => fn (Attachment $attachment) => ['post_id' => $attachment->attachable_id],
|
||||
'changes' => null,
|
||||
'properties' => fn (Attachment $attachment) => ['deleted_id' => $attachment->id, 'original_filename' => $attachment->original_filename, 'mime_type' => $attachment->mime_type, 'size' => $attachment->size],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->generateResourceLogsWithMixedTypes($attachments, $admins, $users, $morphType, $allActions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 신고 활동 로그 (리소스별 rand(1,50)건)
|
||||
*
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function seedReportLogs(Collection $admins): int
|
||||
{
|
||||
$reports = Report::get();
|
||||
if ($reports->isEmpty()) {
|
||||
$this->command->warn('신고 데이터가 없어 신고 활동 로그를 건너뜁니다.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$morphType = (new Report)->getMorphClass();
|
||||
|
||||
$actions = [
|
||||
[
|
||||
'action' => 'report.create',
|
||||
'key' => 'report_create',
|
||||
'loggable' => true,
|
||||
'params' => fn (Report $report) => ['report_id' => $report->id],
|
||||
'changes' => null,
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'report.update_status',
|
||||
'key' => 'report_update_status',
|
||||
'loggable' => true,
|
||||
'params' => fn (Report $report) => ['report_id' => $report->id],
|
||||
'changes' => fn (Report $report) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => $this->pickDifferentEnum(ReportStatus::class, $report->status), 'new' => $report->status?->value ?? ReportStatus::Pending->value, 'type' => 'enum'],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'report.delete',
|
||||
'key' => 'report_delete',
|
||||
'loggable' => false,
|
||||
'params' => fn (Report $report) => ['report_id' => $report->id],
|
||||
'changes' => null,
|
||||
'properties' => fn (Report $report) => ['deleted_id' => $report->id, 'board_id' => $report->board_id],
|
||||
],
|
||||
[
|
||||
'action' => 'report.blind_content',
|
||||
'key' => 'report_blind_content',
|
||||
'loggable' => true,
|
||||
'params' => fn (Report $report) => ['report_id' => $report->id],
|
||||
'changes' => fn (Report $report) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => ReportStatus::Pending->value, 'new' => ReportStatus::Suspended->value, 'type' => 'enum',
|
||||
'old_label_key' => ReportStatus::Pending->labelKey(), 'new_label_key' => ReportStatus::Suspended->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'report.restore_content',
|
||||
'key' => 'report_restore_content',
|
||||
'loggable' => true,
|
||||
'params' => fn (Report $report) => ['report_id' => $report->id],
|
||||
'changes' => fn (Report $report) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => ReportStatus::Suspended->value, 'new' => ReportStatus::Rejected->value, 'type' => 'enum',
|
||||
'old_label_key' => ReportStatus::Suspended->labelKey(), 'new_label_key' => ReportStatus::Rejected->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'report.delete_content',
|
||||
'key' => 'report_delete_content',
|
||||
'loggable' => true,
|
||||
'params' => fn (Report $report) => ['report_id' => $report->id],
|
||||
'changes' => fn (Report $report) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => ReportStatus::Pending->value, 'new' => ReportStatus::Deleted->value, 'type' => 'enum',
|
||||
'old_label_key' => ReportStatus::Pending->labelKey(), 'new_label_key' => ReportStatus::Deleted->labelKey()],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
[
|
||||
'action' => 'report.bulk_update_status',
|
||||
'key' => 'report_bulk_update_status',
|
||||
'loggable' => true,
|
||||
'params' => fn (Report $report) => ['count' => rand(2, 8)],
|
||||
'changes' => fn (Report $report) => [
|
||||
['field' => 'status', 'label_key' => 'sirsoft-board::activity_log.fields.status', 'old' => $this->pickDifferentEnum(ReportStatus::class, $report->status), 'new' => $report->status?->value ?? ReportStatus::Pending->value, 'type' => 'enum'],
|
||||
],
|
||||
'properties' => null,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->generateResourceLogs($reports, $admins, ActivityLogType::Admin, $morphType, $actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스 중심 랜덤 로그를 생성합니다.
|
||||
*
|
||||
* 각 리소스마다 rand(1,50)개의 랜덤 액션을 선택(중복 허용)하여 로그를 생성합니다.
|
||||
*
|
||||
* @param Collection $resources 리소스 컬렉션
|
||||
* @param Collection $actors 액터(사용자) 컬렉션
|
||||
* @param ActivityLogType $logType 로그 유형
|
||||
* @param string $morphType 모델 morph 타입
|
||||
* @param array $actions 액션 템플릿 배열
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function generateResourceLogs(
|
||||
Collection $resources,
|
||||
Collection $actors,
|
||||
ActivityLogType $logType,
|
||||
string $morphType,
|
||||
array $actions,
|
||||
): int {
|
||||
$count = 0;
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
$logCount = rand(1, 50);
|
||||
|
||||
for ($i = 0; $i < $logCount; $i++) {
|
||||
$action = $actions[array_rand($actions)];
|
||||
|
||||
$loggableType = $action['loggable'] ? $morphType : null;
|
||||
$loggableId = $action['loggable'] ? $resource->id : null;
|
||||
|
||||
$params = ($action['params'])($resource);
|
||||
$changes = $action['changes'] ? ($action['changes'])($resource) : null;
|
||||
$properties = $action['properties'] ? ($action['properties'])($resource) : null;
|
||||
|
||||
$this->createLog(
|
||||
$logType,
|
||||
$loggableType,
|
||||
$loggableId,
|
||||
$actors->random()->id,
|
||||
$action['action'],
|
||||
self::PREFIX.$action['key'],
|
||||
$params,
|
||||
$changes,
|
||||
$properties,
|
||||
);
|
||||
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 혼합 로그 유형(Admin/User)을 가진 리소스 중심 랜덤 로그를 생성합니다.
|
||||
*
|
||||
* 각 리소스마다 rand(1,50)개의 랜덤 액션을 선택(중복 허용)하여 로그를 생성합니다.
|
||||
* 각 액션 템플릿에 logType 필드가 있으면 해당 유형을 사용합니다.
|
||||
*
|
||||
* @param Collection $resources 리소스 컬렉션
|
||||
* @param Collection $admins 관리자 컬렉션
|
||||
* @param Collection $users 일반 사용자 컬렉션
|
||||
* @param string $morphType 모델 morph 타입
|
||||
* @param array $actions 액션 템플릿 배열 (logType, userIdResolver 포함 가능)
|
||||
* @return int 생성된 로그 수
|
||||
*/
|
||||
private function generateResourceLogsWithMixedTypes(
|
||||
Collection $resources,
|
||||
Collection $admins,
|
||||
Collection $users,
|
||||
string $morphType,
|
||||
array $actions,
|
||||
): int {
|
||||
$count = 0;
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
$logCount = rand(1, 50);
|
||||
|
||||
for ($i = 0; $i < $logCount; $i++) {
|
||||
$action = $actions[array_rand($actions)];
|
||||
|
||||
// logType이 null이면 랜덤으로 Admin/User 결정 (attachment.upload 패턴)
|
||||
$logType = $action['logType'] ?? (rand(0, 1) ? ActivityLogType::Admin : ActivityLogType::User);
|
||||
|
||||
$loggableType = $action['loggable'] ? $morphType : null;
|
||||
$loggableId = $action['loggable'] ? $resource->id : null;
|
||||
|
||||
// userIdResolver가 있으면 사용, 없으면 logType에 따라 결정
|
||||
$actorPool = $logType === ActivityLogType::Admin ? $admins : $users;
|
||||
|
||||
if (isset($action['userIdResolver'])) {
|
||||
$userId = ($action['userIdResolver'])($resource, $actorPool);
|
||||
} else {
|
||||
$userId = $actorPool->random()->id;
|
||||
}
|
||||
|
||||
$params = ($action['params'])($resource);
|
||||
$changes = $action['changes'] ? ($action['changes'])($resource) : null;
|
||||
$properties = $action['properties'] ? ($action['properties'])($resource) : null;
|
||||
|
||||
$this->createLog(
|
||||
$logType,
|
||||
$loggableType,
|
||||
$loggableId,
|
||||
$userId,
|
||||
$action['action'],
|
||||
self::PREFIX.$action['key'],
|
||||
$params,
|
||||
$changes,
|
||||
$properties,
|
||||
);
|
||||
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityLog 레코드를 생성합니다.
|
||||
*
|
||||
* @param ActivityLogType $logType 로그 유형
|
||||
* @param string|null $loggableType 모델 morph 타입
|
||||
* @param int|null $loggableId 모델 ID
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $action 액션명
|
||||
* @param string $descriptionKey 다국어 키
|
||||
* @param array $descriptionParams 다국어 파라미터
|
||||
* @param array|null $changes 변경 이력
|
||||
* @param array|null $properties 추가 속성
|
||||
*/
|
||||
private function createLog(
|
||||
ActivityLogType $logType,
|
||||
?string $loggableType,
|
||||
?int $loggableId,
|
||||
int $userId,
|
||||
string $action,
|
||||
string $descriptionKey,
|
||||
array $descriptionParams,
|
||||
?array $changes = null,
|
||||
?array $properties = null,
|
||||
): ActivityLog {
|
||||
return ActivityLog::create([
|
||||
'log_type' => $logType,
|
||||
'loggable_type' => $loggableType,
|
||||
'loggable_id' => $loggableId,
|
||||
'user_id' => $userId,
|
||||
'action' => $action,
|
||||
'description_key' => $descriptionKey,
|
||||
'description_params' => $descriptionParams,
|
||||
'changes' => $changes,
|
||||
'properties' => $properties,
|
||||
'ip_address' => self::IPS[array_rand(self::IPS)],
|
||||
'user_agent' => self::USER_AGENTS[array_rand(self::USER_AGENTS)],
|
||||
'created_at' => Carbon::now()->subDays(rand(1, 30))->subHours(rand(0, 23))->subMinutes(rand(0, 59)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다국어 name 배열에서 현재 로케일 값을 가져옵니다.
|
||||
*
|
||||
* @param mixed $name name 속성 (배열 또는 문자열)
|
||||
*/
|
||||
private function getLocalizedName(mixed $name): string
|
||||
{
|
||||
if (is_string($name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
if (is_array($name)) {
|
||||
$locale = app()->getLocale();
|
||||
|
||||
return $name[$locale] ?? $name['ko'] ?? $name[array_key_first($name)] ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum에서 현재 값과 다른 값을 반환합니다.
|
||||
*
|
||||
* @param class-string $enumClass Enum 클래스
|
||||
* @param mixed $currentValue 현재 값
|
||||
*/
|
||||
private function pickDifferentEnum(string $enumClass, mixed $currentValue): string
|
||||
{
|
||||
$cases = $enumClass::cases();
|
||||
$currentStr = $currentValue instanceof \BackedEnum ? $currentValue->value : (string) $currentValue;
|
||||
|
||||
$others = array_filter($cases, fn ($c) => $c->value !== $currentStr);
|
||||
|
||||
if (empty($others)) {
|
||||
return $cases[0]->value;
|
||||
}
|
||||
|
||||
return $others[array_rand($others)]->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 바이트 수를 사람이 읽기 쉬운 형식으로 변환합니다.
|
||||
*
|
||||
* @param int $bytes 바이트 수
|
||||
*/
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1048576) {
|
||||
return round($bytes / 1048576, 1).' MB';
|
||||
}
|
||||
|
||||
if ($bytes >= 1024) {
|
||||
return round($bytes / 1024, 1).' KB';
|
||||
}
|
||||
|
||||
return $bytes.' B';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\Sirsoft\Board\Models\BoardMailTemplate;
|
||||
|
||||
/**
|
||||
* 게시판 메일 템플릿을 시딩합니다 (5종).
|
||||
*/
|
||||
class BoardMailTemplateSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 게시판 메일 템플릿을 시딩합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('게시판 메일 템플릿 시딩 시작...');
|
||||
|
||||
$templates = $this->getDefaultTemplates();
|
||||
|
||||
foreach ($templates as $data) {
|
||||
BoardMailTemplate::updateOrCreate(
|
||||
['type' => $data['type']],
|
||||
[
|
||||
'subject' => $data['subject'],
|
||||
'body' => $data['body'],
|
||||
'variables' => $data['variables'],
|
||||
'is_active' => true,
|
||||
'is_default' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$this->command->info(" - {$data['type']} 템플릿 등록 완료");
|
||||
}
|
||||
|
||||
$this->command->info('게시판 메일 템플릿 시딩 완료 (' . count($templates) . '종)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 템플릿 데이터를 반환합니다.
|
||||
*
|
||||
* @return array<int, array{type: string, subject: array, body: array, variables: array}>
|
||||
*/
|
||||
public function getDefaultTemplates(): array
|
||||
{
|
||||
return [
|
||||
$this->newCommentTemplate(),
|
||||
$this->replyCommentTemplate(),
|
||||
$this->postReplyTemplate(),
|
||||
$this->postActionTemplate(),
|
||||
$this->newPostAdminTemplate(),
|
||||
$this->reportReceivedAdminTemplate(),
|
||||
$this->reportActionTemplate(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 댓글 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function newCommentTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_comment',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] 게시글에 새 댓글이 등록되었습니다',
|
||||
'en' => '[{board_name}] New comment on your post',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판의 게시글에 <strong>{comment_author}</strong>님이 댓글을 남겼습니다.</p>'
|
||||
. '<blockquote style="border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;">{comment_content}</blockquote>'
|
||||
. $this->button('게시글 보기', '{post_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p><strong>{comment_author}</strong> commented on your post in <strong>{board_name}</strong>.</p>'
|
||||
. '<blockquote style="border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;">{comment_content}</blockquote>'
|
||||
. $this->button('View Post', '{post_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '게시글 제목'],
|
||||
['key' => 'comment_author', 'description' => '댓글 작성자'],
|
||||
['key' => 'comment_content', 'description' => '댓글 내용 (200자)'],
|
||||
['key' => 'post_url', 'description' => '게시글 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 대댓글 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replyCommentTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'reply_comment',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] 댓글에 답글이 등록되었습니다',
|
||||
'en' => '[{board_name}] Reply to your comment',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판에서 <strong>{comment_author}</strong>님이 댓글에 답글을 남겼습니다.</p>'
|
||||
. '<blockquote style="border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;">{comment_content}</blockquote>'
|
||||
. $this->button('게시글 보기', '{post_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p><strong>{comment_author}</strong> replied to your comment in <strong>{board_name}</strong>.</p>'
|
||||
. '<blockquote style="border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;">{comment_content}</blockquote>'
|
||||
. $this->button('View Post', '{post_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '게시글 제목'],
|
||||
['key' => 'comment_author', 'description' => '답글 작성자'],
|
||||
['key' => 'comment_content', 'description' => '답글 내용 (200자)'],
|
||||
['key' => 'post_url', 'description' => '게시글 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 답변글 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function postReplyTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'post_reply',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] 게시글에 답변글이 등록되었습니다',
|
||||
'en' => '[{board_name}] Reply to your post',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판의 게시글 "<strong>{post_title}</strong>"에 답변글이 등록되었습니다.</p>'
|
||||
. $this->button('게시글 보기', '{post_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p>A reply has been posted to your post "<strong>{post_title}</strong>" in <strong>{board_name}</strong>.</p>'
|
||||
. $this->button('View Post', '{post_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '게시글 제목'],
|
||||
['key' => 'post_url', 'description' => '게시글 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 처리 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function postActionTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'post_action',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] 게시글이 {action_type} 처리되었습니다',
|
||||
'en' => '[{board_name}] Your post has been {action_type}',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판의 게시글 "<strong>{post_title}</strong>"이(가) 관리자에 의해 <strong>{action_type}</strong> 처리되었습니다.</p>'
|
||||
. '<p>문의사항이 있으시면 관리자에게 연락해 주세요.</p>'
|
||||
. $this->button('게시글 보기', '{post_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p>Your post "<strong>{post_title}</strong>" in <strong>{board_name}</strong> has been <strong>{action_type}</strong> by an administrator.</p>'
|
||||
. '<p>If you have any questions, please contact the administrator.</p>'
|
||||
. $this->button('View Post', '{post_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '게시글 제목'],
|
||||
['key' => 'action_type', 'description' => '처리 유형 (블라인드/삭제/복원)'],
|
||||
['key' => 'post_url', 'description' => '게시글 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 새 게시글 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function newPostAdminTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_post_admin',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] 새 게시글이 등록되었습니다',
|
||||
'en' => '[{board_name}] New post has been created',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판에 <strong>{post_author}</strong>님이 새 게시글을 등록했습니다.</p>'
|
||||
. '<p>게시글 제목: <strong>{post_title}</strong></p>'
|
||||
. $this->button('게시글 보기', '{post_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p><strong>{post_author}</strong> created a new post in <strong>{board_name}</strong>.</p>'
|
||||
. '<p>Title: <strong>{post_title}</strong></p>'
|
||||
. $this->button('View Post', '{post_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름 (관리자)'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '게시글 제목'],
|
||||
['key' => 'post_author', 'description' => '게시글 작성자'],
|
||||
['key' => 'post_url', 'description' => '게시글 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 신고 처리 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function reportActionTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'report_action',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] 회원님의 {target_type} "{post_title}"이 {action_type} 처리되었습니다',
|
||||
'en' => '[{board_name}] Your {target_type} "{post_title}" has been {action_type}',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판에서 회원님의 <strong>{target_type}</strong> "<strong>{post_title}</strong>"이(가) 신고로 인해 <strong>{action_type}</strong> 처리되었습니다.</p>'
|
||||
. '<p>문의사항이 있으시면 관리자에게 연락해 주세요.</p>'
|
||||
. $this->button('게시글 보기', '{post_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p>Your <strong>{target_type}</strong> "<strong>{post_title}</strong>" in <strong>{board_name}</strong> has been <strong>{action_type}</strong> due to reports.</p>'
|
||||
. '<p>If you have any questions, please contact the administrator.</p>'
|
||||
. $this->button('View Post', '{post_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '게시글 제목'],
|
||||
['key' => 'action_type', 'description' => '처리 유형 (블라인드/삭제/복원)'],
|
||||
['key' => 'target_type', 'description' => '처리 대상 (게시글/댓글)'],
|
||||
['key' => 'post_url', 'description' => '게시글 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 신고 접수 관리자 알림 템플릿을 반환합니다.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function reportReceivedAdminTemplate(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'report_received_admin',
|
||||
'subject' => [
|
||||
'ko' => '[{board_name}] "{post_title}"에 대한 신고가 접수되었습니다',
|
||||
'en' => '[{board_name}] A new report has been received for "{post_title}"',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
|
||||
. '<p><strong>{board_name}</strong> 게시판에서 <strong>{target_type}</strong> "<strong>{post_title}</strong>"에 대한 새 신고가 접수되었습니다.</p>'
|
||||
. '<p>신고 사유: <strong>{reason_type}</strong></p>'
|
||||
. $this->button('신고 관리 페이지로 이동', '{report_url}')
|
||||
. '<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
'en' => '<h1>Hello, {name}.</h1>'
|
||||
. '<p>A new report has been received for the <strong>{target_type}</strong> "<strong>{post_title}</strong>" in <strong>{board_name}</strong>.</p>'
|
||||
. '<p>Reason: <strong>{reason_type}</strong></p>'
|
||||
. $this->button('Go to Report Management', '{report_url}')
|
||||
. '<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
|
||||
],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트명'],
|
||||
['key' => 'board_name', 'description' => '게시판 이름'],
|
||||
['key' => 'post_title', 'description' => '신고 대상 게시글 제목'],
|
||||
['key' => 'target_type', 'description' => '신고 대상 유형 (게시글/댓글)'],
|
||||
['key' => 'reason_type', 'description' => '신고 사유'],
|
||||
['key' => 'report_url', 'description' => '신고 관리 페이지 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 호환 CTA 버튼 HTML을 생성합니다.
|
||||
*
|
||||
* @param string $text 버튼 텍스트
|
||||
* @param string $url 버튼 링크 URL
|
||||
* @return string 인라인 스타일 버튼 HTML
|
||||
*/
|
||||
private function button(string $text, string $url): string
|
||||
{
|
||||
return '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;">'
|
||||
. '<tr><td align="center">'
|
||||
. '<a href="' . $url . '" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">'
|
||||
. $text
|
||||
. '</a>'
|
||||
. '</td></tr></table>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\Sirsoft\Board\Models\BoardType;
|
||||
|
||||
/**
|
||||
* 게시판 유형 초기 시더
|
||||
*
|
||||
* 기본 3개 유형(basic, gallery, card)을 생성합니다.
|
||||
* module.php getSeeders()에서 직접 등록됩니다.
|
||||
*/
|
||||
class BoardTypeSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 초기 게시판 유형 데이터
|
||||
*/
|
||||
private const DEFAULT_BOARD_TYPES = [
|
||||
[
|
||||
'slug' => 'basic',
|
||||
'name' => ['ko' => '기본형', 'en' => 'Basic List'],
|
||||
],
|
||||
[
|
||||
'slug' => 'gallery',
|
||||
'name' => ['ko' => '갤러리형', 'en' => 'Gallery'],
|
||||
],
|
||||
[
|
||||
'slug' => 'card',
|
||||
'name' => ['ko' => '카드형', 'en' => 'Card'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
foreach (self::DEFAULT_BOARD_TYPES as $boardTypeData) {
|
||||
if (BoardType::where('slug', $boardTypeData['slug'])->exists()) {
|
||||
$this->command->warn(" 게시판 유형 '{$boardTypeData['slug']}' 이미 존재합니다. 건너뜁니다.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
BoardType::create($boardTypeData);
|
||||
$this->command->info(" 게시판 유형 '{$boardTypeData['slug']}' 생성 완료.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders;
|
||||
|
||||
use App\Traits\HasSampleSeeders;
|
||||
use App\Traits\HasSeederCounts;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* 게시판 모듈 메인 시더
|
||||
*
|
||||
* 설치 필수 시더는 항상 실행되며, 샘플 시더는 --sample 옵션 시에만 실행됩니다.
|
||||
*
|
||||
* 설치 시더:
|
||||
* - InstallSeeder (BoardMailTemplateSeeder 포함)
|
||||
* - BoardTypeSeeder
|
||||
*
|
||||
* 샘플 시더 (--sample 옵션 시):
|
||||
* 1. BoardSampleSeeder - 게시판 8개 (테이블 + 권한 자동 생성)
|
||||
* 2. PostSampleSeeder - 게시글/댓글 샘플 (총 160건)
|
||||
* 3. ReportSampleSeeder - 신고 샘플 15~20건
|
||||
* 4. BoardMailSendLogSeeder - 메일 발송 이력 샘플
|
||||
*
|
||||
* 실행 방법:
|
||||
* php artisan module:seed sirsoft-board # 설치 시더만
|
||||
* php artisan module:seed sirsoft-board --sample # 설치 + 샘플
|
||||
*/
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use HasSampleSeeders;
|
||||
use HasSeederCounts;
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('========================================');
|
||||
$this->command->info(' 게시판 모듈 시더 시작');
|
||||
$this->command->info('========================================');
|
||||
$this->command->info('');
|
||||
|
||||
// 설치 필수 시더 (항상 실행)
|
||||
$this->command->info('[설치] 게시판 타입 및 메일 템플릿 생성');
|
||||
$this->call([
|
||||
InstallSeeder::class,
|
||||
BoardTypeSeeder::class,
|
||||
]);
|
||||
$this->command->info('');
|
||||
|
||||
// 샘플 시더 (--sample 옵션 시에만 실행)
|
||||
if ($this->shouldIncludeSample()) {
|
||||
$this->command->info('--- 게시판 샘플 시더 실행 ---');
|
||||
$this->command->info('');
|
||||
|
||||
// 1. 게시판 샘플 생성 (테이블 + 권한 자동, count 불필요)
|
||||
$this->command->info('[1/5] 게시판 샘플 생성');
|
||||
$this->call(Sample\BoardSampleSeeder::class);
|
||||
$this->command->info('');
|
||||
|
||||
// 2. 게시글/댓글 샘플 생성 (count-aware)
|
||||
$this->command->info('[2/5] 게시글/댓글 샘플 생성');
|
||||
$this->callWithCounts(Sample\PostSampleSeeder::class);
|
||||
$this->command->info('');
|
||||
|
||||
// 3. 신고 샘플 생성 (count 불필요)
|
||||
$this->command->info('[3/5] 신고 샘플 생성');
|
||||
$this->call(Sample\ReportSampleSeeder::class);
|
||||
$this->command->info('');
|
||||
|
||||
// 4. 메일 발송 이력 샘플
|
||||
$this->command->info('[4/5] 메일 발송 이력 샘플 생성');
|
||||
$this->call(Sample\BoardMailSendLogSeeder::class);
|
||||
$this->command->info('');
|
||||
|
||||
// 5. 활동 로그 샘플 (모든 샘플 데이터 생성 후 마지막에 실행)
|
||||
$this->command->info('[5/5] 활동 로그 샘플 생성');
|
||||
$this->call(ActivityLogSampleSeeder::class);
|
||||
$this->command->info('');
|
||||
}
|
||||
|
||||
$this->command->info('========================================');
|
||||
$this->command->info(' 게시판 모듈 시더 완료!');
|
||||
$this->command->info('========================================');
|
||||
$this->command->info('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 카운트 옵션을 전파하며 단일 시더를 실행합니다.
|
||||
*
|
||||
* @param class-string $class 시더 클래스
|
||||
*/
|
||||
private function callWithCounts(string $class): void
|
||||
{
|
||||
$seeder = $this->resolve($class);
|
||||
|
||||
if (method_exists($seeder, 'setSeederCounts')) {
|
||||
$seeder->setSeederCounts($this->seederCounts);
|
||||
}
|
||||
|
||||
$seeder->__invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* 게시판 모듈 설치 시더
|
||||
*
|
||||
* 모듈 설치 시 실행되는 시더입니다.
|
||||
* 테스트용 더미 데이터는 포함하지 않습니다.
|
||||
*
|
||||
* 테스트 데이터가 필요한 경우 DatabaseSeeder를 수동으로 실행하세요:
|
||||
* php artisan module:seed sirsoft-board
|
||||
*/
|
||||
class InstallSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 시더 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(BoardMailTemplateSeeder::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders\Sample;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Enums\MailSendStatus;
|
||||
use App\Models\MailSendLog;
|
||||
use App\Traits\HasSeederCounts;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* 게시판 모듈 메일 발송 이력 개발용 시더
|
||||
*
|
||||
* 게시판 메일 템플릿(new_comment, reply_comment, post_reply, post_action, new_post_admin)을
|
||||
* 기반으로 발송 이력 샘플 데이터를 생성합니다.
|
||||
*
|
||||
* 개발/테스트 용도이므로 설치 시 자동 실행되지 않습니다.
|
||||
* 수동 실행: php artisan module:seed sirsoft-board --class=BoardMailSendLogSeeder
|
||||
*/
|
||||
class BoardMailSendLogSeeder extends Seeder
|
||||
{
|
||||
use HasSeederCounts;
|
||||
|
||||
/**
|
||||
* 게시판 메일 템플릿 유형별 제목 패턴
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $templateSubjects = [
|
||||
'new_comment' => '[그누보드7] 게시글에 새 댓글이 등록되었습니다',
|
||||
'reply_comment' => '[그누보드7] 댓글에 답글이 등록되었습니다',
|
||||
'post_reply' => '[그누보드7] 게시글에 답변글이 등록되었습니다',
|
||||
'post_action' => '[그누보드7] 게시글 관리자 처리 안내',
|
||||
'new_post_admin' => '[그누보드7] 새 게시글이 등록되었습니다',
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$count = $this->getSeederCount('mail_send_logs', 25);
|
||||
|
||||
$this->command->info('게시판 메일 발송 이력 시딩 시작...');
|
||||
|
||||
$templateTypes = array_keys($this->templateSubjects);
|
||||
$recipients = $this->getRecipients();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$templateType = $templateTypes[array_rand($templateTypes)];
|
||||
$recipient = $recipients[array_rand($recipients)];
|
||||
$status = $this->randomStatus();
|
||||
$sentAt = $this->randomSentAt();
|
||||
|
||||
MailSendLog::create([
|
||||
'recipient_email' => $recipient['email'],
|
||||
'recipient_name' => $recipient['name'],
|
||||
'subject' => $this->templateSubjects[$templateType],
|
||||
'template_type' => $templateType,
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'source' => 'notification',
|
||||
'status' => $status->value,
|
||||
'error_message' => $status === MailSendStatus::Failed
|
||||
? $this->randomErrorMessage()
|
||||
: null,
|
||||
'sent_at' => $sentAt,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info("게시판 메일 발송 이력 시딩 완료 ({$count}건)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 샘플 수신자 목록을 반환합니다.
|
||||
*
|
||||
* @return array<int, array{email: string, name: string}>
|
||||
*/
|
||||
private function getRecipients(): array
|
||||
{
|
||||
return [
|
||||
['email' => 'hong@example.com', 'name' => '홍길동'],
|
||||
['email' => 'kim@example.com', 'name' => '김철수'],
|
||||
['email' => 'lee@example.com', 'name' => '이영희'],
|
||||
['email' => 'park@example.com', 'name' => '박민수'],
|
||||
['email' => 'choi@example.com', 'name' => '최지은'],
|
||||
['email' => 'admin@example.com', 'name' => '관리자'],
|
||||
['email' => 'john@example.com', 'name' => 'John Doe'],
|
||||
['email' => 'jane@example.com', 'name' => 'Jane Smith'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 가중치 기반 랜덤 상태를 반환합니다.
|
||||
*
|
||||
* @return MailSendStatus
|
||||
*/
|
||||
private function randomStatus(): MailSendStatus
|
||||
{
|
||||
$rand = mt_rand(1, 100);
|
||||
|
||||
if ($rand <= 80) {
|
||||
return MailSendStatus::Sent;
|
||||
} elseif ($rand <= 95) {
|
||||
return MailSendStatus::Failed;
|
||||
}
|
||||
|
||||
return MailSendStatus::Skipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 90일 내 랜덤 발송 시각을 반환합니다.
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
private function randomSentAt(): Carbon
|
||||
{
|
||||
return Carbon::now()->subDays(mt_rand(0, 90))->subHours(mt_rand(0, 23))->subMinutes(mt_rand(0, 59));
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 에러 메시지를 반환합니다.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function randomErrorMessage(): string
|
||||
{
|
||||
$messages = [
|
||||
'Connection timed out after 30 seconds',
|
||||
'SMTP server returned 550: Mailbox not found',
|
||||
'DNS resolution failed for mail server',
|
||||
'Authentication failed: Invalid credentials',
|
||||
'Message rejected: Recipient address rejected',
|
||||
];
|
||||
|
||||
return $messages[array_rand($messages)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders\Sample;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Services\BoardService;
|
||||
|
||||
/**
|
||||
* 게시판 샘플 시더
|
||||
*
|
||||
* 다양한 설정의 게시판을 생성합니다.
|
||||
* BoardService를 사용하여 테이블과 권한이 자동으로 생성됩니다.
|
||||
*/
|
||||
class BoardSampleSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 게시판 샘플 데이터
|
||||
*
|
||||
* 테스트 시나리오 (8개 게시판):
|
||||
* 1. notice - 공지사항 : 관리자만 글쓰기, 댓글/파일 없음, mail 알림
|
||||
* 2. free - 자유게시판 : 비회원 개방, 비밀글 선택, 입력 제한 커스텀
|
||||
* 3. gallery - 갤러리 : gallery 타입, 이미지만, 최신댓글 상단(DESC), 조회수 정렬
|
||||
* 4. event - 이벤트 : card 타입, 댓글/파일 없음, 오래된 순 정렬(ASC)
|
||||
* 5. qna - Q&A : 카테고리, 답글 depth, 비밀글 선택, 회원 비밀글 읽기 허용
|
||||
* 6. members - 회원전용 : 비회원 차단, secret_mode:always + 댓글 허용
|
||||
* 7. inquiry - 1:1 문의 : 비밀글 필수, 답글 depth:1, 비회원 글쓰기, mail 알림
|
||||
* 8. archive - 아카이브 : 비활성(is_active:false), blocked_keywords:null(defaults 45개 적용), ASC 정렬
|
||||
*
|
||||
* 커버하는 설정 케이스:
|
||||
* - type : basic ✓ / gallery ✓ / card ✓
|
||||
* - secret_mode : disabled ✓ / enabled ✓ / always ✓
|
||||
* - use_comment : true ✓ / false ✓
|
||||
* - use_reply : true(depth 1/3/5) ✓ / false ✓
|
||||
* - use_report : true ✓ / false ✓
|
||||
* - use_file_upload : true ✓ / false ✓
|
||||
* - is_active : true ✓ / false ✓
|
||||
* - order_by : created_at ✓ / view_count ✓
|
||||
* - order_direction : ASC ✓ / DESC ✓
|
||||
* - comment_order : ASC ✓ / DESC ✓
|
||||
* - notify channels : mail ✓ / database ✓
|
||||
* - blocked_keywords: null(defaults 적용) ✓ / 커스텀 ✓
|
||||
* - 입력 제한 : 기본값 ✓ / 커스텀 ✓
|
||||
* - secret+댓글 조합: always+댓글허용 ✓
|
||||
* - 권한 : posts.read-secret에 user 추가 ✓
|
||||
*
|
||||
* 권한 설정 원칙:
|
||||
* - 기본값: g7_module_settings('sirsoft-board', 'basic_defaults.default_board_permissions') 사용
|
||||
* - permissions 배열: 기본값에서 변경이 필요한 권한만 오버라이드
|
||||
* - admin은 모든 권한에 포함 (기본값에서 보장)
|
||||
*/
|
||||
private const SAMPLE_BOARDS = [
|
||||
// =========================================================
|
||||
// 1. 공지사항
|
||||
// 검증: 관리자 전용 글쓰기 / 댓글·답글·파일 없음 / mail 알림 채널
|
||||
// notify_*_channels: defaults는 ['mail']이지만 시더에서 명시적으로 검증
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '공지사항', 'en' => 'Notice'],
|
||||
'slug' => 'notice',
|
||||
'description' => ['ko' => '중요한 공지사항을 확인하세요', 'en' => 'Check important announcements'],
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'disabled',
|
||||
'use_comment' => false,
|
||||
'use_reply' => false,
|
||||
'use_report' => false,
|
||||
'use_file_upload' => false,
|
||||
'show_view_count' => true,
|
||||
'per_page' => 15,
|
||||
'new_display_hours' => 72,
|
||||
// 알림: mail 채널 — defaults.json 기본값과 동일하나 명시적으로 검증
|
||||
'notify_author' => false,
|
||||
'notify_admin_on_post' => true,
|
||||
'notify_admin_on_post_channels' => ['mail'],
|
||||
// 기본값: posts_write = ['admin', 'user'] → user 제거 (관리자만 글쓰기)
|
||||
'permissions' => [
|
||||
'posts_write' => ['roles' => ['admin']],
|
||||
],
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 2. 자유게시판
|
||||
// 검증: 비회원 전체 개방 / 비밀글 선택 / 입력 제한 커스텀값 / 커스텀 금지어
|
||||
// 입력 제한: 제목(5~100자), 내용(0~2000자), 댓글(0~500자)
|
||||
// min_content_length: 0 → 내용 없이도 등록 가능
|
||||
// min_comment_length: 0 → 짧은 댓글 허용 (예: "👍", "+1")
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '자유게시판', 'en' => 'Free Board'],
|
||||
'slug' => 'free',
|
||||
'description' => ['ko' => '자유롭게 이야기를 나누세요', 'en' => 'Share your thoughts freely'],
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'enabled',
|
||||
'use_comment' => true,
|
||||
'use_reply' => false,
|
||||
'use_report' => true,
|
||||
'use_file_upload' => true,
|
||||
'max_file_size' => 5242880, // 5MB
|
||||
'max_file_count' => 5,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'zip'],
|
||||
'show_view_count' => true,
|
||||
'per_page' => 20,
|
||||
'new_display_hours' => 24,
|
||||
// 입력 제한 커스텀 — 기본값과 다른 값으로 StorePostRequest 검증 동작 확인
|
||||
'min_title_length' => 5,
|
||||
'max_title_length' => 100,
|
||||
'min_content_length' => 0, // 내용 제한 없음 (링크/이미지만 올리는 경우)
|
||||
'max_content_length' => 2000,
|
||||
'min_comment_length' => 0, // 짧은 반응 허용
|
||||
'max_comment_length' => 500,
|
||||
// 알림: database 채널 (defaults의 mail과 다른 케이스)
|
||||
'notify_author' => true,
|
||||
'notify_author_channels' => ['database'],
|
||||
'notify_admin_on_post' => false,
|
||||
// 금지어: 커스텀 — 기본 45개 대신 광고/스팸 특화
|
||||
'blocked_keywords' => ['광고', '홍보', 'spam', '도박', '대출'],
|
||||
// 기본값: posts_write = ['admin', 'user'] → guest 추가 (비회원도 글쓰기/댓글)
|
||||
'permissions' => [
|
||||
'posts_write' => ['roles' => ['admin', 'user', 'guest']],
|
||||
'comments_write' => ['roles' => ['admin', 'user', 'guest']],
|
||||
],
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 3. 갤러리
|
||||
// 검증: gallery 타입 / 이미지 파일만 / 조회수 정렬(view_count) / 최신 댓글 상단(comment_order:DESC)
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '갤러리', 'en' => 'Gallery'],
|
||||
'slug' => 'gallery',
|
||||
'description' => ['ko' => '사진과 이미지를 공유하세요', 'en' => 'Share your photos and images'],
|
||||
'type' => 'gallery',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'disabled',
|
||||
'use_comment' => true,
|
||||
'use_reply' => false,
|
||||
'use_report' => true,
|
||||
'use_file_upload' => true,
|
||||
'max_file_size' => 10485760, // 10MB
|
||||
'max_file_count' => 10,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
|
||||
'show_view_count' => true,
|
||||
'per_page' => 12,
|
||||
'new_display_hours' => 24,
|
||||
// 조회수 기준 내림차순 정렬 — 인기글이 상단에 노출
|
||||
'order_by' => 'view_count',
|
||||
'order_direction' => 'DESC',
|
||||
// 최신 댓글 상단 — 뉴스피드형 게시판에서 사용
|
||||
'comment_order' => 'DESC',
|
||||
'max_comment_depth' => 3,
|
||||
'notify_author' => true,
|
||||
'notify_author_channels' => ['database'],
|
||||
'notify_admin_on_post' => false,
|
||||
// 기본값 그대로 — 회원만 글쓰기/댓글, 전체 읽기/다운로드
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 4. 이벤트
|
||||
// 검증: card 타입 / 댓글·답글·파일 없음 / 오래된 순 정렬(order_direction:ASC)
|
||||
// order_direction:ASC — 이벤트 번호순, FAQ 순서 고정형 게시판에서 사용
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '이벤트', 'en' => 'Event'],
|
||||
'slug' => 'event',
|
||||
'description' => ['ko' => '진행 중인 이벤트를 확인하세요', 'en' => 'Check ongoing events'],
|
||||
'type' => 'card',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'disabled',
|
||||
'use_comment' => false,
|
||||
'use_reply' => false,
|
||||
'use_report' => false,
|
||||
'use_file_upload' => true,
|
||||
'max_file_size' => 10485760, // 10MB
|
||||
'max_file_count' => 5,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
|
||||
'show_view_count' => true,
|
||||
'per_page' => 12,
|
||||
'new_display_hours' => 72,
|
||||
// 등록순 오름차순 — 이벤트 번호순 노출
|
||||
'order_by' => 'created_at',
|
||||
'order_direction' => 'ASC',
|
||||
'notify_author' => false,
|
||||
'notify_admin_on_post' => false,
|
||||
// 기본값: posts_write = ['admin', 'user'] → user 제거 (관리자만 등록)
|
||||
'permissions' => [
|
||||
'posts_write' => ['roles' => ['admin']],
|
||||
],
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 5. Q&A
|
||||
// 검증: 카테고리 4개 / 답글 다단계(depth 3) / 비밀글 선택
|
||||
// posts.read-secret에 user 추가 — 비밀글을 작성자 외 회원도 열람 가능
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => 'Q&A', 'en' => 'Q&A'],
|
||||
'slug' => 'qna',
|
||||
'description' => ['ko' => '궁금한 점을 질문하고 답변을 받으세요', 'en' => 'Ask questions and get answers'],
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'enabled',
|
||||
'use_comment' => true,
|
||||
'use_reply' => true,
|
||||
'max_reply_depth' => 3,
|
||||
'max_comment_depth' => 5,
|
||||
'use_report' => true,
|
||||
'use_file_upload' => true,
|
||||
'max_file_size' => 5242880, // 5MB
|
||||
'max_file_count' => 3,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'pdf', 'zip', 'txt'],
|
||||
'show_view_count' => true,
|
||||
'per_page' => 20,
|
||||
'new_display_hours' => 24,
|
||||
'categories' => ['일반문의', '기술문의', '결제문의', '기타'],
|
||||
'notify_author' => true,
|
||||
'notify_author_channels' => ['database'],
|
||||
'notify_admin_on_post' => true,
|
||||
'notify_admin_on_post_channels' => ['database'],
|
||||
'blocked_keywords' => ['욕설테스트', 'badword'],
|
||||
// 기본값: posts.read-secret = ['admin'] → user 추가
|
||||
// 비밀글을 작성자 외 로그인 회원도 읽을 수 있는 케이스 (고객센터 담당자 등)
|
||||
'permissions' => [
|
||||
'posts_read-secret' => ['roles' => ['admin', 'user']],
|
||||
],
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 6. 회원전용
|
||||
// 검증: 비회원 읽기/쓰기 차단 / secret_mode:always + use_comment:true 조합
|
||||
// secret_mode:always이면서 댓글까지 허용 — 비밀게시판 내 댓글 동작 확인
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '회원전용', 'en' => 'Members Only'],
|
||||
'slug' => 'members',
|
||||
'description' => ['ko' => '회원만 이용 가능한 게시판입니다', 'en' => 'Members only board'],
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'always',
|
||||
'use_comment' => true,
|
||||
'use_reply' => true,
|
||||
'max_reply_depth' => 5,
|
||||
'max_comment_depth' => 10,
|
||||
'use_report' => true,
|
||||
'use_file_upload' => true,
|
||||
'max_file_size' => 5242880, // 5MB
|
||||
'max_file_count' => 5,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'zip'],
|
||||
'show_view_count' => true,
|
||||
'per_page' => 20,
|
||||
'new_display_hours' => 24,
|
||||
'notify_author' => true,
|
||||
'notify_author_channels' => ['database'],
|
||||
'notify_admin_on_post' => false,
|
||||
// 금지어: 기본 45개 대신 스팸 특화 커스텀
|
||||
'blocked_keywords' => ['스팸', 'advertisement'],
|
||||
// 기본값: posts.read = ['admin', 'user', 'guest'] → guest 제거 (비회원 읽기 차단)
|
||||
'permissions' => [
|
||||
'posts_read' => ['roles' => ['admin', 'user']],
|
||||
'comments_read' => ['roles' => ['admin', 'user']],
|
||||
'attachments_download' => ['roles' => ['admin', 'user']],
|
||||
],
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 7. 1:1 문의
|
||||
// 검증: 비밀글 필수(always) / 댓글 없음 / 답글 depth:1 / 비회원 글쓰기 / mail 알림
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '1:1 문의', 'en' => 'Contact Us'],
|
||||
'slug' => 'inquiry',
|
||||
'description' => ['ko' => '1:1로 문의하실 수 있습니다', 'en' => 'Contact us directly'],
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
'secret_mode' => 'always',
|
||||
'use_comment' => false,
|
||||
'use_reply' => true,
|
||||
'max_reply_depth' => 1,
|
||||
'max_comment_depth' => 1,
|
||||
'use_report' => false,
|
||||
'use_file_upload' => true,
|
||||
'max_file_size' => 5242880, // 5MB
|
||||
'max_file_count' => 3,
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'zip'],
|
||||
'show_view_count' => false,
|
||||
'per_page' => 20,
|
||||
'new_display_hours' => 24,
|
||||
// 알림: mail 채널 — 문의 접수 즉시 이메일로 확인
|
||||
'notify_author' => true,
|
||||
'notify_author_channels' => ['mail'],
|
||||
'notify_admin_on_post' => true,
|
||||
'notify_admin_on_post_channels' => ['mail'],
|
||||
// 기본값: posts_write = ['admin', 'user'] → guest 추가 (비회원도 문의 가능)
|
||||
'permissions' => [
|
||||
'posts_write' => ['roles' => ['admin', 'user', 'guest']],
|
||||
'attachments_upload' => ['roles' => ['admin', 'user', 'guest']],
|
||||
],
|
||||
],
|
||||
|
||||
// =========================================================
|
||||
// 8. 아카이브
|
||||
// 검증: is_active:false (비활성) / blocked_keywords:null (defaults.json 45개 적용)
|
||||
// order_direction:ASC (오래된 순)
|
||||
// is_active:false — 프론트에서 접근 시 403 동작 확인
|
||||
// blocked_keywords:null — 금지어를 명시하지 않았을 때 defaults.json 기본 45개가 적용되는지 확인
|
||||
// =========================================================
|
||||
[
|
||||
'name' => ['ko' => '아카이브', 'en' => 'Archive'],
|
||||
'slug' => 'archive',
|
||||
'description' => ['ko' => '과거 게시글 아카이브입니다 (현재 비활성)', 'en' => 'Archive of past posts (currently inactive)'],
|
||||
'type' => 'basic',
|
||||
'is_active' => false, // 비활성 게시판
|
||||
'secret_mode' => 'disabled',
|
||||
'use_comment' => true,
|
||||
'use_reply' => false,
|
||||
'use_report' => false,
|
||||
'use_file_upload' => false,
|
||||
'show_view_count' => true,
|
||||
'per_page' => 30,
|
||||
'new_display_hours' => 0,
|
||||
// 오래된 순 정렬 — 아카이브는 시간순 탐색
|
||||
'order_by' => 'created_at',
|
||||
'order_direction' => 'ASC',
|
||||
'notify_author' => false,
|
||||
'notify_admin_on_post' => false,
|
||||
// blocked_keywords: null → defaults.json의 기본 45개 금지어 적용 확인
|
||||
'blocked_keywords' => null,
|
||||
// 기본값 그대로 사용
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('게시판 샘플 데이터 생성 중...');
|
||||
|
||||
// 관리자 사용자로 인증 (생성자 기록용)
|
||||
$admin = User::whereHas('roles', function ($query) {
|
||||
$query->where('identifier', 'admin');
|
||||
})->first();
|
||||
|
||||
if ($admin) {
|
||||
Auth::login($admin);
|
||||
}
|
||||
|
||||
/** @var BoardService $boardService */
|
||||
$boardService = app(BoardService::class);
|
||||
|
||||
// admin 사용자 UUID 목록 조회 → board_manager_ids 동적 주입
|
||||
$adminIds = User::whereHas('roles', function ($query) {
|
||||
$query->where('identifier', 'admin');
|
||||
})->pluck('uuid')->toArray();
|
||||
|
||||
if (empty($adminIds)) {
|
||||
$this->command->warn(' ⚠ admin 역할 사용자가 없습니다. board_manager_ids가 비어있게 됩니다.');
|
||||
}
|
||||
|
||||
foreach (self::SAMPLE_BOARDS as $boardData) {
|
||||
$boardData['board_manager_ids'] = $adminIds;
|
||||
// 이미 존재하는 게시판은 스킵
|
||||
if (Board::where('slug', $boardData['slug'])->exists()) {
|
||||
$this->command->warn(" - {$boardData['slug']} 게시판이 이미 존재합니다. 스킵합니다.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$board = $boardService->createBoard($boardData);
|
||||
$this->command->info(" - {$board->slug} 게시판 생성 완료 (테이블 + 권한 포함)");
|
||||
} catch (\Exception $e) {
|
||||
$this->command->error(" - {$boardData['slug']} 게시판 생성 실패: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($admin) {
|
||||
Auth::logout();
|
||||
}
|
||||
|
||||
$this->command->info('게시판 샘플 데이터 생성 완료: '.count(self::SAMPLE_BOARDS).'개');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,835 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Database\Seeders\Sample;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Sirsoft\Board\Enums\ReportReasonType;
|
||||
use Modules\Sirsoft\Board\Enums\ReportStatus;
|
||||
use Modules\Sirsoft\Board\Enums\ReportType;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Models\Report;
|
||||
use Modules\Sirsoft\Board\Models\ReportLog;
|
||||
|
||||
/**
|
||||
* 신고 샘플 시더
|
||||
*
|
||||
* 1케이스 구조: boards_reports(케이스 1행) + boards_report_logs(신고자 기록 N행)
|
||||
* 다양한 상태와 사유의 신고 케이스 데이터를 생성합니다.
|
||||
*/
|
||||
class ReportSampleSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* 신고 상세 사유 템플릿
|
||||
*/
|
||||
private const REASON_DETAILS = [
|
||||
ReportReasonType::Abuse->value => '욕설과 비방이 포함된 게시글입니다. 다른 사용자를 모욕하는 내용이 있습니다.',
|
||||
ReportReasonType::HateSpeech->value => '특정 집단에 대한 혐오 발언이 포함되어 있습니다.',
|
||||
ReportReasonType::Spam->value => '광고성 스팸 게시글입니다. 동일한 내용이 반복적으로 게시되고 있습니다.',
|
||||
ReportReasonType::Copyright->value => '저작권이 있는 이미지/콘텐츠를 무단으로 사용하고 있습니다.',
|
||||
ReportReasonType::Privacy->value => '개인정보(전화번호, 주소 등)가 동의 없이 노출되어 있습니다.',
|
||||
ReportReasonType::Misinformation->value => '사실과 다른 허위 정보를 유포하고 있습니다.',
|
||||
ReportReasonType::Sexual->value => '성적인 콘텐츠가 포함되어 있습니다.',
|
||||
ReportReasonType::Violence->value => '폭력적인 내용이 포함된 게시글입니다.',
|
||||
ReportReasonType::Other->value => '기타 사유로 신고합니다. 커뮤니티 가이드라인 위반으로 보입니다.',
|
||||
];
|
||||
|
||||
/**
|
||||
* 처리 메모 템플릿
|
||||
*/
|
||||
private const PROCESS_NOTES = [
|
||||
ReportStatus::Review->value => '신고 내용을 검토 중입니다.',
|
||||
ReportStatus::Rejected->value => '신고 내용을 검토한 결과, 커뮤니티 가이드라인 위반에 해당하지 않습니다.',
|
||||
ReportStatus::Suspended->value => '신고 내용 확인 결과 규정 위반으로 판단되어 해당 콘텐츠를 숨김 처리하였습니다.',
|
||||
];
|
||||
|
||||
/**
|
||||
* 시더 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('신고 샘플 데이터 생성 중...');
|
||||
|
||||
// 기존 데이터 삭제 (개발 단계 — truncate 금지, delete 사용)
|
||||
ReportLog::query()->delete();
|
||||
Report::withTrashed()->forceDelete();
|
||||
|
||||
// 신고 기능이 활성화된 게시판 조회
|
||||
$boards = Board::where('use_report', true)->get();
|
||||
|
||||
if ($boards->isEmpty()) {
|
||||
$this->command->warn('신고 기능이 활성화된 게시판이 없습니다.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 테스트 사용자 조회
|
||||
$users = User::whereIn('email', [
|
||||
'gildong@example.com',
|
||||
'chulsoo@example.com',
|
||||
'younghee@example.com',
|
||||
])->get();
|
||||
|
||||
if ($users->count() < 3) {
|
||||
$users = User::limit(5)->get();
|
||||
}
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->command->error('사용자가 없습니다. UserSampleSeeder를 먼저 실행하세요.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 관리자 조회 (처리자용)
|
||||
$admin = User::whereHas('roles', function ($query) {
|
||||
$query->where('identifier', 'admin');
|
||||
})->first();
|
||||
|
||||
$caseCount = 0;
|
||||
|
||||
// 각 상태별로 케이스 생성 (1케이스 = boards_reports 1행 + boards_report_logs N행)
|
||||
foreach (ReportStatus::cases() as $status) {
|
||||
// 각 상태별로 3~4건 생성
|
||||
$countForStatus = rand(3, 4);
|
||||
|
||||
for ($i = 0; $i < $countForStatus; $i++) {
|
||||
$board = $boards->random();
|
||||
$targetType = ReportType::cases()[array_rand(ReportType::cases())];
|
||||
|
||||
// 게시글/댓글 ID 조회
|
||||
$targetId = $this->getRandomTargetId($board, $targetType);
|
||||
|
||||
if (! $targetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 케이스가 있는 대상인지 확인 (UNIQUE 제약)
|
||||
$exists = Report::withTrashed()
|
||||
->where('board_id', $board->id)
|
||||
->where('target_type', $targetType)
|
||||
->where('target_id', $targetId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 스냅샷 데이터 생성
|
||||
$snapshot = $this->getSnapshotData($board, $targetType, $targetId);
|
||||
$createdAt = now()->subDays(rand(1, 30));
|
||||
$lastReportedAt = $createdAt;
|
||||
$reporterUser = $users->random();
|
||||
|
||||
// boards_reports 케이스 생성
|
||||
$caseData = [
|
||||
'board_id' => $board->id,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'author_id' => $snapshot['author_id'],
|
||||
'status' => $status,
|
||||
'last_reported_at' => $lastReportedAt,
|
||||
'last_activated_at' => null,
|
||||
'metadata' => [
|
||||
'ip' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Sample Browser',
|
||||
],
|
||||
'process_histories' => [
|
||||
[
|
||||
'type' => 'reported',
|
||||
'action_label' => '신고 접수',
|
||||
'processor_id' => null,
|
||||
'reason' => null,
|
||||
'reporter_count' => 1,
|
||||
'created_at' => $createdAt->format('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => now()->subDays(rand(0, 15)),
|
||||
];
|
||||
|
||||
// 처리된 상태인 경우 처리 정보 추가
|
||||
if ($status !== ReportStatus::Pending && $admin) {
|
||||
$processedAt = now()->subDays(rand(0, 7));
|
||||
$caseData['processed_by'] = $admin->id;
|
||||
$caseData['processed_at'] = $processedAt;
|
||||
$caseData['process_histories'][] = [
|
||||
'type' => $status->value,
|
||||
'action_label' => $status->label(),
|
||||
'processor_id' => $admin->id,
|
||||
'reason' => self::PROCESS_NOTES[$status->value] ?? null,
|
||||
'reporter_count' => null,
|
||||
'created_at' => $processedAt->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
$case = Report::create($caseData);
|
||||
|
||||
// boards_report_logs 신고자 기록 생성 (1~3명)
|
||||
$reporterCount = rand(1, min(3, $users->count()));
|
||||
$logReporters = $users->shuffle()->take($reporterCount);
|
||||
$reasonTypes = ReportReasonType::cases();
|
||||
|
||||
foreach ($logReporters as $idx => $logReporter) {
|
||||
$reasonType = $reasonTypes[$idx % count($reasonTypes)];
|
||||
ReportLog::create([
|
||||
'report_id' => $case->id,
|
||||
'reporter_id' => $logReporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => $board->getLocalizedName(),
|
||||
'title' => $snapshot['title'],
|
||||
'content' => $snapshot['content'],
|
||||
'content_mode' => $snapshot['content_mode'] ?? 'text',
|
||||
'author_name' => $snapshot['author_name'],
|
||||
],
|
||||
'reason_type' => $reasonType,
|
||||
'reason_detail' => self::REASON_DETAILS[$reasonType->value],
|
||||
'metadata' => [
|
||||
'ip' => '127.0.0.'.($idx + 1),
|
||||
'user_agent' => 'Mozilla/5.0 Sample Browser',
|
||||
],
|
||||
'created_at' => $createdAt->copy()->addMinutes($idx * 10),
|
||||
'updated_at' => $createdAt->copy()->addMinutes($idx * 10),
|
||||
]);
|
||||
}
|
||||
|
||||
$caseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("신고 케이스 생성 완료: {$caseCount}건");
|
||||
$this->command->info(' - 상태별 분포:');
|
||||
|
||||
foreach (ReportStatus::cases() as $status) {
|
||||
$count = Report::where('status', $status)->count();
|
||||
$this->command->info(" - {$status->value}: {$count}건");
|
||||
}
|
||||
|
||||
// 비회원 작성 콘텐츠에 대한 신고 데이터 생성
|
||||
$this->createGuestAuthorReports($boards, $users);
|
||||
|
||||
// 다중 신고자 테스트용 케이스 생성 (동일 대상에 여러 신고자)
|
||||
$this->createMultiReporterCases($boards, $users, $admin);
|
||||
|
||||
// 재접수 케이스 생성 (반려/중단 후 재신고로 재활성된 케이스)
|
||||
$this->createReactivatedCases($boards, $users, $admin);
|
||||
|
||||
// 대량 신고자 케이스 생성 (단일 게시글에 20명 이상 신고)
|
||||
$this->createManyReportersCase($boards, $admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비회원 작성 콘텐츠에 대한 신고 케이스를 생성합니다.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $boards 게시판 목록
|
||||
* @param \Illuminate\Database\Eloquent\Collection $users 사용자 목록
|
||||
*/
|
||||
private function createGuestAuthorReports($boards, $users): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('비회원 작성 콘텐츠 신고 데이터 생성 중...');
|
||||
|
||||
$guestNames = ['익명사용자', '방문자123', 'Guest User'];
|
||||
$count = 0;
|
||||
|
||||
foreach ($guestNames as $index => $guestName) {
|
||||
$board = $boards->random();
|
||||
$reporter = $users->random();
|
||||
$reasonType = ReportReasonType::cases()[$index % count(ReportReasonType::cases())];
|
||||
$targetType = ReportType::cases()[$index % count(ReportType::cases())];
|
||||
|
||||
$targetId = $this->getRandomTargetId($board, $targetType);
|
||||
|
||||
if (! $targetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// UNIQUE 제약 확인
|
||||
$exists = Report::withTrashed()
|
||||
->where('board_id', $board->id)
|
||||
->where('target_type', $targetType)
|
||||
->where('target_id', $targetId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$createdAt = now()->subDays(rand(1, 10));
|
||||
|
||||
$case = Report::create([
|
||||
'board_id' => $board->id,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'author_id' => null,
|
||||
'status' => ReportStatus::Pending,
|
||||
'last_reported_at' => $createdAt,
|
||||
'last_activated_at' => null,
|
||||
'metadata' => [
|
||||
'ip' => '192.168.1.'.($index + 100),
|
||||
'user_agent' => 'Mozilla/5.0 Guest-Author-Test',
|
||||
],
|
||||
'process_histories' => [
|
||||
[
|
||||
'type' => 'reported',
|
||||
'action_label' => '신고 접수',
|
||||
'processor_id' => null,
|
||||
'reason' => null,
|
||||
'reporter_count' => 1,
|
||||
'created_at' => $createdAt->format('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
|
||||
ReportLog::create([
|
||||
'report_id' => $case->id,
|
||||
'reporter_id' => $reporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => $board->getLocalizedName(),
|
||||
'title' => '비회원 작성 테스트 게시글 '.($index + 1),
|
||||
'content' => '비회원이 작성한 콘텐츠입니다.',
|
||||
'content_mode' => 'text',
|
||||
'author_name' => $guestName,
|
||||
],
|
||||
'reason_type' => $reasonType,
|
||||
'reason_detail' => self::REASON_DETAILS[$reasonType->value],
|
||||
'metadata' => [
|
||||
'ip' => '192.168.1.'.($index + 100),
|
||||
'user_agent' => 'Mozilla/5.0 Guest-Author-Test',
|
||||
],
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->line("비회원 작성 콘텐츠 신고 케이스 생성 완료: {$count}건");
|
||||
}
|
||||
|
||||
/**
|
||||
* 다중 신고자 테스트용 케이스를 생성합니다.
|
||||
* 동일 케이스에 여러 신고자가 logs를 추가한 상태를 테스트합니다.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $boards 게시판 목록
|
||||
* @param \Illuminate\Database\Eloquent\Collection $users 사용자 목록
|
||||
* @param User|null $admin 관리자
|
||||
*/
|
||||
private function createMultiReporterCases($boards, $users, $admin): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('다중 신고자 테스트용 케이스 생성 중...');
|
||||
|
||||
$caseCount = 0;
|
||||
$targetBoards = $boards->take(min(2, $boards->count()));
|
||||
|
||||
foreach ($targetBoards as $board) {
|
||||
foreach ([ReportType::Post, ReportType::Comment] as $targetType) {
|
||||
$targetId = $this->getRandomTargetId($board, $targetType);
|
||||
|
||||
if (! $targetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// UNIQUE 제약 확인
|
||||
$exists = Report::withTrashed()
|
||||
->where('board_id', $board->id)
|
||||
->where('target_type', $targetType)
|
||||
->where('target_id', $targetId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->getSnapshotData($board, $targetType, $targetId);
|
||||
$createdAt = now()->subDays(rand(5, 14));
|
||||
$lastReportedAt = $createdAt->copy()->addHours($users->count() - 1);
|
||||
$reasonTypes = ReportReasonType::cases();
|
||||
|
||||
$case = Report::create([
|
||||
'board_id' => $board->id,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'author_id' => $snapshot['author_id'],
|
||||
'status' => ReportStatus::Pending,
|
||||
'last_reported_at' => $lastReportedAt,
|
||||
'last_activated_at' => null,
|
||||
'metadata' => [
|
||||
'ip' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 Multi-Reporter-Test',
|
||||
],
|
||||
'process_histories' => [
|
||||
[
|
||||
'type' => 'reported',
|
||||
'action_label' => '신고 접수',
|
||||
'processor_id' => null,
|
||||
'reason' => null,
|
||||
'reporter_count' => $users->count(),
|
||||
'created_at' => $createdAt->format('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $lastReportedAt,
|
||||
]);
|
||||
|
||||
// 모든 사용자가 각각 신고 (boards_report_logs)
|
||||
foreach ($users as $idx => $reporter) {
|
||||
$reasonType = $reasonTypes[$idx % count($reasonTypes)];
|
||||
$logCreatedAt = $createdAt->copy()->addHours($idx);
|
||||
|
||||
ReportLog::create([
|
||||
'report_id' => $case->id,
|
||||
'reporter_id' => $reporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => $board->getLocalizedName(),
|
||||
'title' => $snapshot['title'],
|
||||
'content' => $snapshot['content'],
|
||||
'content_mode' => $snapshot['content_mode'] ?? 'text',
|
||||
'author_name' => $snapshot['author_name'],
|
||||
],
|
||||
'reason_type' => $reasonType,
|
||||
'reason_detail' => self::REASON_DETAILS[$reasonType->value],
|
||||
'metadata' => [
|
||||
'ip' => '127.0.0.'.($idx + 1),
|
||||
'user_agent' => 'Mozilla/5.0 Multi-Reporter-Test',
|
||||
],
|
||||
'created_at' => $logCreatedAt,
|
||||
'updated_at' => $logCreatedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
$boardName = $board->getLocalizedName();
|
||||
$this->command->line(" - [{$boardName}] {$targetType->value} #{$targetId}: {$users->count()}명 신고");
|
||||
$caseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->line("다중 신고자 테스트 케이스 생성 완료: {$caseCount}건");
|
||||
}
|
||||
|
||||
/**
|
||||
* 재접수 케이스를 생성합니다.
|
||||
*
|
||||
* 시나리오:
|
||||
* A. 반려 후 재신고 → 현재 pending (단순 재접수)
|
||||
* B. 반려 후 재신고 → 다시 반려 (재접수 후 재처리)
|
||||
* C. 게시 중단 후 재신고 → 현재 pending (중단 후 재접수)
|
||||
* D. 반려 후 재신고 → 중단 처리 (재접수 후 중단)
|
||||
* E. 게시판 삭제 후 남은 신고 케이스 (board_id null — 스냅샷만 남음)
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $boards 게시판 목록
|
||||
* @param \Illuminate\Database\Eloquent\Collection $users 사용자 목록
|
||||
* @param User|null $admin 관리자
|
||||
*/
|
||||
private function createReactivatedCases($boards, $users, $admin): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('재접수 케이스 생성 중...');
|
||||
|
||||
$caseCount = 0;
|
||||
$reasonTypes = ReportReasonType::cases();
|
||||
|
||||
// 시나리오별 정의 [현재 status, 처리 이력 타입 목록, last_activated_at 설정 여부]
|
||||
$scenarios = [
|
||||
'A' => ['status' => ReportStatus::Pending, 'histories' => ['reported', 'rejected', 're_reported'], 'label' => '반려 후 재접수 (현재: 접수)'],
|
||||
'B' => ['status' => ReportStatus::Rejected, 'histories' => ['reported', 'rejected', 're_reported', 'rejected'], 'label' => '반려 후 재접수 후 재반려'],
|
||||
'C' => ['status' => ReportStatus::Pending, 'histories' => ['reported', 'suspended', 're_reported'], 'label' => '중단 후 재접수 (현재: 접수)'],
|
||||
'D' => ['status' => ReportStatus::Suspended, 'histories' => ['reported', 'rejected', 're_reported', 'suspended'], 'label' => '재접수 후 중단 처리'],
|
||||
];
|
||||
|
||||
foreach ($scenarios as $key => $scenario) {
|
||||
$board = $boards->random();
|
||||
$targetType = ReportType::cases()[array_rand(ReportType::cases())];
|
||||
$targetId = $this->getRandomTargetId($board, $targetType);
|
||||
|
||||
if (! $targetId) {
|
||||
$this->command->line(" - 시나리오 {$key}: 대상 없음 (건너뜀)");
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = Report::withTrashed()
|
||||
->where('board_id', $board->id)
|
||||
->where('target_type', $targetType)
|
||||
->where('target_id', $targetId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
$this->command->line(" - 시나리오 {$key}: 이미 케이스 존재 (건너뜀)");
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->getSnapshotData($board, $targetType, $targetId);
|
||||
|
||||
// 시간 흐름: 최초 신고 → 처리 → 재신고 → (재처리)
|
||||
$firstReportedAt = now()->subDays(rand(20, 30));
|
||||
$firstProcessedAt = $firstReportedAt->copy()->addDays(rand(1, 3));
|
||||
$reReportedAt = $firstProcessedAt->copy()->addDays(rand(1, 5));
|
||||
$lastProcessedAt = $reReportedAt->copy()->addDays(rand(1, 3));
|
||||
|
||||
// process_histories 구성
|
||||
$histories = [];
|
||||
$reporterCount = 0;
|
||||
foreach ($scenario['histories'] as $idx => $type) {
|
||||
$reporterCount++;
|
||||
$isReportEvent = in_array($type, ['reported', 're_reported']);
|
||||
$entryTime = match ($idx) {
|
||||
0 => $firstReportedAt,
|
||||
1 => $firstProcessedAt,
|
||||
2 => $reReportedAt,
|
||||
default => $lastProcessedAt,
|
||||
};
|
||||
|
||||
$histories[] = [
|
||||
'type' => $type,
|
||||
'action_label' => $type,
|
||||
'processor_id' => ($isReportEvent || ! $admin) ? null : $admin->id,
|
||||
'reason' => $isReportEvent ? null : (self::PROCESS_NOTES[$type] ?? null),
|
||||
'reporter_count' => $isReportEvent ? $reporterCount : null,
|
||||
'created_at' => $entryTime->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
// 현재 status에 따른 처리자 정보
|
||||
$currentStatus = $scenario['status'];
|
||||
$isCurrentlyProcessed = in_array($currentStatus, [ReportStatus::Rejected, ReportStatus::Suspended]);
|
||||
|
||||
$caseData = [
|
||||
'board_id' => $board->id,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'author_id' => $snapshot['author_id'],
|
||||
'status' => $currentStatus,
|
||||
'processed_by' => ($isCurrentlyProcessed && $admin) ? $admin->id : null,
|
||||
'processed_at' => $isCurrentlyProcessed ? $lastProcessedAt : null,
|
||||
'last_reported_at' => $reReportedAt,
|
||||
'last_activated_at' => $reReportedAt,
|
||||
'metadata' => ['ip' => '127.0.0.1', 'user_agent' => 'Mozilla/5.0 Reactivated-Test'],
|
||||
'process_histories' => $histories,
|
||||
'created_at' => $firstReportedAt,
|
||||
'updated_at' => $isCurrentlyProcessed ? $lastProcessedAt : $reReportedAt,
|
||||
];
|
||||
|
||||
$case = Report::create($caseData);
|
||||
|
||||
// 최초 신고자 log
|
||||
$firstReporter = $users->random();
|
||||
ReportLog::create([
|
||||
'report_id' => $case->id,
|
||||
'reporter_id' => $firstReporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => $board->getLocalizedName(),
|
||||
'title' => $snapshot['title'],
|
||||
'content' => $snapshot['content'],
|
||||
'content_mode' => $snapshot['content_mode'] ?? 'text',
|
||||
'author_name' => $snapshot['author_name'],
|
||||
],
|
||||
'reason_type' => $reasonTypes[0],
|
||||
'reason_detail' => self::REASON_DETAILS[$reasonTypes[0]->value],
|
||||
'metadata' => ['ip' => '127.0.0.1', 'user_agent' => 'Mozilla/5.0'],
|
||||
'created_at' => $firstReportedAt,
|
||||
'updated_at' => $firstReportedAt,
|
||||
]);
|
||||
|
||||
// 재신고자 log (다른 사용자 — 없으면 건너뜀)
|
||||
$filteredUsers = $users->filter(fn ($u) => $u->id !== $firstReporter->id);
|
||||
if ($filteredUsers->isEmpty()) {
|
||||
$caseCount++;
|
||||
continue;
|
||||
}
|
||||
$reReporter = $filteredUsers->random();
|
||||
ReportLog::create([
|
||||
'report_id' => $case->id,
|
||||
'reporter_id' => $reReporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => $board->getLocalizedName(),
|
||||
'title' => $snapshot['title'],
|
||||
'content' => $snapshot['content'],
|
||||
'content_mode' => $snapshot['content_mode'] ?? 'text',
|
||||
'author_name' => $snapshot['author_name'],
|
||||
],
|
||||
'reason_type' => $reasonTypes[1 % count($reasonTypes)],
|
||||
'reason_detail' => self::REASON_DETAILS[$reasonTypes[1 % count($reasonTypes)]->value],
|
||||
'metadata' => ['ip' => '127.0.0.2', 'user_agent' => 'Mozilla/5.0'],
|
||||
'created_at' => $reReportedAt,
|
||||
'updated_at' => $reReportedAt,
|
||||
]);
|
||||
|
||||
$boardName = $board->getLocalizedName();
|
||||
$this->command->line(" - 시나리오 {$key} [{$boardName}] {$targetType->value} #{$targetId}: {$scenario['label']}");
|
||||
$caseCount++;
|
||||
}
|
||||
|
||||
// 시나리오 E: board_id null (게시판 삭제 후 남은 신고 케이스)
|
||||
$this->command->line(' - 시나리오 E: 게시판 삭제 후 남은 신고 케이스 생성');
|
||||
$reporter = $users->random();
|
||||
$orphanAt = now()->subDays(rand(10, 20));
|
||||
|
||||
$orphanCase = Report::create([
|
||||
'board_id' => null,
|
||||
'target_type' => ReportType::Post,
|
||||
'target_id' => 99999,
|
||||
'author_id' => null,
|
||||
'status' => ReportStatus::Pending,
|
||||
'last_reported_at' => $orphanAt,
|
||||
'last_activated_at' => null,
|
||||
'metadata' => ['ip' => '10.0.0.1', 'user_agent' => 'Mozilla/5.0 Orphan-Test'],
|
||||
'process_histories' => [
|
||||
[
|
||||
'type' => 'reported',
|
||||
'action_label' => '신고 접수',
|
||||
'processor_id' => null,
|
||||
'reason' => null,
|
||||
'reporter_count' => 1,
|
||||
'created_at' => $orphanAt->format('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
'created_at' => $orphanAt,
|
||||
'updated_at' => $orphanAt,
|
||||
]);
|
||||
|
||||
ReportLog::create([
|
||||
'report_id' => $orphanCase->id,
|
||||
'reporter_id' => $reporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => '삭제된 게시판',
|
||||
'title' => '삭제된 게시판의 게시글',
|
||||
'content' => '게시판이 삭제되어 원본을 확인할 수 없는 신고 케이스입니다.',
|
||||
'content_mode' => 'text',
|
||||
'author_name' => '알 수 없음',
|
||||
],
|
||||
'reason_type' => ReportReasonType::Spam,
|
||||
'reason_detail' => self::REASON_DETAILS[ReportReasonType::Spam->value],
|
||||
'metadata' => ['ip' => '10.0.0.1', 'user_agent' => 'Mozilla/5.0'],
|
||||
'created_at' => $orphanAt,
|
||||
'updated_at' => $orphanAt,
|
||||
]);
|
||||
|
||||
$caseCount++;
|
||||
$this->command->line("재접수/특수 케이스 생성 완료: {$caseCount}건");
|
||||
}
|
||||
|
||||
/**
|
||||
* 대량 신고자 케이스를 생성합니다.
|
||||
* 단일 게시글(또는 댓글)에 20명 이상이 신고한 시나리오입니다.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $boards 게시판 목록
|
||||
* @param User|null $admin 관리자
|
||||
*/
|
||||
private function createManyReportersCase($boards, $admin): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('대량 신고자 케이스 생성 중 (20명 이상)...');
|
||||
|
||||
// 사용자 최대 25명 조회 (20명 이상 확보)
|
||||
$manyUsers = User::orderBy('id')->limit(25)->get();
|
||||
|
||||
if ($manyUsers->count() < 5) {
|
||||
$this->command->warn('사용자가 5명 미만입니다. 대량 신고자 케이스 생성을 건너뜁니다.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$board = $boards->first();
|
||||
$reasonTypes = ReportReasonType::cases();
|
||||
$caseCount = 0;
|
||||
|
||||
foreach ([ReportType::Post, ReportType::Comment] as $targetType) {
|
||||
$targetId = $this->getRandomTargetId($board, $targetType);
|
||||
|
||||
if (! $targetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// UNIQUE 제약 확인
|
||||
$exists = Report::withTrashed()
|
||||
->where('board_id', $board->id)
|
||||
->where('target_type', $targetType)
|
||||
->where('target_id', $targetId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->getSnapshotData($board, $targetType, $targetId);
|
||||
$createdAt = now()->subDays(rand(3, 7));
|
||||
$lastReportedAt = $createdAt->copy()->addMinutes(($manyUsers->count() - 1) * 5);
|
||||
|
||||
$case = Report::create([
|
||||
'board_id' => $board->id,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'author_id' => $snapshot['author_id'],
|
||||
'status' => ReportStatus::Pending,
|
||||
'last_reported_at' => $lastReportedAt,
|
||||
'last_activated_at' => null,
|
||||
'metadata' => [
|
||||
'ip' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 Many-Reporters-Test',
|
||||
],
|
||||
'process_histories' => [
|
||||
[
|
||||
'type' => 'reported',
|
||||
'action_label' => '신고 접수',
|
||||
'processor_id' => null,
|
||||
'reason' => null,
|
||||
'reporter_count' => $manyUsers->count(),
|
||||
'created_at' => $createdAt->format('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $lastReportedAt,
|
||||
]);
|
||||
|
||||
// 각 사용자별 신고 log 생성
|
||||
foreach ($manyUsers as $idx => $reporter) {
|
||||
$reasonType = $reasonTypes[$idx % count($reasonTypes)];
|
||||
$logCreatedAt = $createdAt->copy()->addMinutes($idx * 5);
|
||||
|
||||
ReportLog::create([
|
||||
'report_id' => $case->id,
|
||||
'reporter_id' => $reporter->id,
|
||||
'snapshot' => [
|
||||
'board_name' => $board->getLocalizedName(),
|
||||
'title' => $snapshot['title'],
|
||||
'content' => $snapshot['content'],
|
||||
'content_mode' => $snapshot['content_mode'] ?? 'text',
|
||||
'author_name' => $snapshot['author_name'],
|
||||
],
|
||||
'reason_type' => $reasonType,
|
||||
'reason_detail' => self::REASON_DETAILS[$reasonType->value],
|
||||
'metadata' => [
|
||||
'ip' => '10.10.'.intdiv($idx, 255).'.'.($idx % 255 + 1),
|
||||
'user_agent' => 'Mozilla/5.0 Many-Reporters-Test',
|
||||
],
|
||||
'created_at' => $logCreatedAt,
|
||||
'updated_at' => $logCreatedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
$boardName = $board->getLocalizedName();
|
||||
$this->command->line(" - [{$boardName}] {$targetType->value} #{$targetId}: {$manyUsers->count()}명 신고");
|
||||
$caseCount++;
|
||||
|
||||
// 게시글 타입 1건 생성 후 종료 (댓글도 성공하면 2건)
|
||||
}
|
||||
|
||||
$this->command->line("대량 신고자 케이스 생성 완료: {$caseCount}건 (신고자 수: {$manyUsers->count()}명)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 대상 ID를 조회합니다.
|
||||
*
|
||||
* @param Board $board 게시판
|
||||
* @param ReportType $targetType 대상 타입
|
||||
* @return int|null 대상 ID
|
||||
*/
|
||||
private function getRandomTargetId(Board $board, ReportType $targetType): ?int
|
||||
{
|
||||
if ($targetType === ReportType::Post) {
|
||||
$record = DB::table('board_posts')
|
||||
->where('board_id', $board->id)
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
} else {
|
||||
$record = DB::table('board_comments')
|
||||
->where('board_id', $board->id)
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
}
|
||||
|
||||
return $record?->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 스냅샷 데이터를 조회합니다.
|
||||
*
|
||||
* @param Board $board 게시판
|
||||
* @param ReportType $targetType 대상 타입
|
||||
* @param int $targetId 대상 ID
|
||||
* @return array<string, mixed> 스냅샷 데이터
|
||||
*/
|
||||
private function getSnapshotData(Board $board, ReportType $targetType, int $targetId): array
|
||||
{
|
||||
if ($targetType === ReportType::Post) {
|
||||
$record = DB::table('board_posts')
|
||||
->where('board_id', $board->id)
|
||||
->where('id', $targetId)
|
||||
->first();
|
||||
|
||||
if ($record) {
|
||||
$authorId = $record->user_id ?? null;
|
||||
|
||||
// 외래키 제약: 존재하지 않는 유저 ID는 null 처리
|
||||
if ($authorId && ! User::find($authorId)) {
|
||||
$authorId = null;
|
||||
}
|
||||
|
||||
$authorName = $record->author_name;
|
||||
|
||||
if (! $authorName && $authorId) {
|
||||
$user = User::find($authorId);
|
||||
$authorName = $user?->name ?? '알 수 없음';
|
||||
}
|
||||
|
||||
return [
|
||||
'author_id' => $authorId,
|
||||
'title' => $record->title,
|
||||
'content' => strip_tags($record->content),
|
||||
'content_mode' => $record->content_mode ?? 'text',
|
||||
'author_name' => $authorName ?? '알 수 없음',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$comment = DB::table('board_comments')
|
||||
->where('board_id', $board->id)
|
||||
->where('id', $targetId)
|
||||
->first();
|
||||
|
||||
if ($comment) {
|
||||
$authorId = $comment->user_id ?? null;
|
||||
|
||||
// 외래키 제약: 존재하지 않는 유저 ID는 null 처리
|
||||
if ($authorId && ! User::find($authorId)) {
|
||||
$authorId = null;
|
||||
}
|
||||
|
||||
$authorName = $comment->author_name;
|
||||
|
||||
if (! $authorName && $authorId) {
|
||||
$user = User::find($authorId);
|
||||
$authorName = $user?->name ?? '알 수 없음';
|
||||
}
|
||||
|
||||
// 댓글이 속한 게시글 제목 조회
|
||||
$postTitle = null;
|
||||
if ($comment->post_id) {
|
||||
$post = DB::table('board_posts')
|
||||
->where('board_id', $board->id)
|
||||
->where('id', $comment->post_id)
|
||||
->first();
|
||||
$postTitle = $post?->title;
|
||||
}
|
||||
|
||||
return [
|
||||
'author_id' => $authorId,
|
||||
'title' => $postTitle,
|
||||
'content' => $comment->content,
|
||||
'content_mode' => 'text',
|
||||
'author_name' => $authorName ?? '알 수 없음',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'author_id' => null,
|
||||
'title' => '삭제된 콘텐츠',
|
||||
'content' => '원본 콘텐츠가 삭제되었습니다.',
|
||||
'content_mode' => 'text',
|
||||
'author_name' => '알 수 없음',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"identifier": "sirsoft-board",
|
||||
"vendor": "sirsoft",
|
||||
"name": {
|
||||
"ko": "게시판",
|
||||
"en": "Board"
|
||||
},
|
||||
"version": "1.0.0-beta.1",
|
||||
"license": "MIT",
|
||||
"description": {
|
||||
"ko": "게시판 관리를 위한 모듈",
|
||||
"en": "Module for board management"
|
||||
},
|
||||
"g7_version": ">=7.0.0-beta.1",
|
||||
"dependencies": {
|
||||
"modules": {},
|
||||
"plugins": {}
|
||||
},
|
||||
"github_url": "https://github.com/gnuboard/g7-module-sirsoft-board",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-module-sirsoft-board/blob/main/CHANGELOG.md"
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board;
|
||||
|
||||
use App\Extension\AbstractModule;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\Sirsoft\Board\Database\Seeders\BoardTypeSeeder;
|
||||
use Modules\Sirsoft\Board\Database\Seeders\InstallSeeder;
|
||||
use Modules\Sirsoft\Board\Listeners\ActivityLogDescriptionResolver;
|
||||
use Modules\Sirsoft\Board\Listeners\BoardActivityLogListener;
|
||||
use Modules\Sirsoft\Board\Listeners\BoardNotificationChannelListener;
|
||||
use Modules\Sirsoft\Board\Listeners\BoardNotificationListener;
|
||||
use Modules\Sirsoft\Board\Listeners\EcommerceInquiryHookListener;
|
||||
use Modules\Sirsoft\Board\Listeners\SearchPostsListener;
|
||||
use Modules\Sirsoft\Board\Listeners\SeoBoardCacheListener;
|
||||
use Modules\Sirsoft\Board\Listeners\SeoBoardSettingsCacheListener;
|
||||
use Modules\Sirsoft\Board\Listeners\UserNotificationSettingsListener;
|
||||
|
||||
class Module extends AbstractModule
|
||||
{
|
||||
/**
|
||||
* 모듈 제거 시 동적으로 생성된 게시판별 역할을 정리합니다.
|
||||
*
|
||||
* 게시판 생성 시 동적으로 생성된 sirsoft-board.{slug}.manager/step 역할은
|
||||
* getRoles()에 포함되지 않으므로, ModuleManager::removeModuleRoles()에서 처리되지 않습니다.
|
||||
* 따라서 uninstall()에서 직접 정리합니다.
|
||||
*
|
||||
* @return bool 제거 성공 여부
|
||||
*/
|
||||
public function uninstall(): bool
|
||||
{
|
||||
// 게시판별 동적 역할 정리 (sirsoft-board.*.manager, sirsoft-board.*.step)
|
||||
Role::where('extension_type', 'module')
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->each(function (Role $role) {
|
||||
$role->permissions()->detach();
|
||||
$role->users()->detach();
|
||||
$role->delete();
|
||||
});
|
||||
|
||||
return parent::uninstall();
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 권한 목록 반환 (계층형 구조, 다국어 지원)
|
||||
*
|
||||
* 구조: 모듈(1레벨) → 카테고리(2레벨) → 개별 권한(3레벨)
|
||||
* identifier는 자동 생성됨: {module}.{category}.{action}
|
||||
* 게시판별 권한(sirsoft-board.{slug}.*)은 게시판 생성 시 BoardPermissionService에서 동적으로 생성됨
|
||||
*
|
||||
* 모듈 레벨 권한만 정의:
|
||||
* - sirsoft-board.boards.* (게시판 관리)
|
||||
* - sirsoft-board.reports.* (신고 관리)
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'ko' => '게시판',
|
||||
'en' => 'Board',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 모듈 권한',
|
||||
'en' => 'Board module permissions',
|
||||
],
|
||||
'categories' => [
|
||||
// 게시판 관리 권한 (type: admin)
|
||||
[
|
||||
'identifier' => 'boards',
|
||||
'resource_route_key' => 'board',
|
||||
'owner_key' => 'created_by',
|
||||
'name' => [
|
||||
'ko' => '게시판 관리',
|
||||
'en' => 'Board Management',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 관리 권한',
|
||||
'en' => 'Board management permissions',
|
||||
],
|
||||
'permissions' => [
|
||||
[
|
||||
'action' => 'read',
|
||||
'name' => [
|
||||
'ko' => '게시판 조회',
|
||||
'en' => 'View Boards',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 목록 및 상세 조회',
|
||||
'en' => 'View board list and details',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin', 'manager'],
|
||||
],
|
||||
[
|
||||
'action' => 'create',
|
||||
'name' => [
|
||||
'ko' => '게시판 생성',
|
||||
'en' => 'Create Board',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '새 게시판 생성',
|
||||
'en' => 'Create new board',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin', 'manager'],
|
||||
],
|
||||
[
|
||||
'action' => 'update',
|
||||
'name' => [
|
||||
'ko' => '게시판 수정',
|
||||
'en' => 'Update Board',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 설정 수정',
|
||||
'en' => 'Update board settings',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin', 'manager'],
|
||||
],
|
||||
[
|
||||
'action' => 'delete',
|
||||
'name' => [
|
||||
'ko' => '게시판 삭제',
|
||||
'en' => 'Delete Board',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 삭제',
|
||||
'en' => 'Delete board',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin'],
|
||||
],
|
||||
],
|
||||
],
|
||||
// 환경설정 권한 (type: admin)
|
||||
[
|
||||
'identifier' => 'settings',
|
||||
'name' => [
|
||||
'ko' => '환경설정',
|
||||
'en' => 'Settings',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 환경설정 권한',
|
||||
'en' => 'Board settings permissions',
|
||||
],
|
||||
'permissions' => [
|
||||
[
|
||||
'action' => 'read',
|
||||
'name' => [
|
||||
'ko' => '환경설정 조회',
|
||||
'en' => 'View Settings',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 환경설정 조회',
|
||||
'en' => 'View board settings',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin'],
|
||||
],
|
||||
[
|
||||
'action' => 'update',
|
||||
'name' => [
|
||||
'ko' => '환경설정 수정',
|
||||
'en' => 'Update Settings',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 환경설정 수정',
|
||||
'en' => 'Update board settings',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin'],
|
||||
],
|
||||
],
|
||||
],
|
||||
// 신고 관리 권한 (type: admin)
|
||||
[
|
||||
'identifier' => 'reports',
|
||||
'resource_route_key' => 'report',
|
||||
'owner_key' => 'reporter_id',
|
||||
'name' => [
|
||||
'ko' => '게시판 신고 관리',
|
||||
'en' => 'Report Management',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '게시판 신고 관리 권한',
|
||||
'en' => 'Board report management permissions',
|
||||
],
|
||||
'permissions' => [
|
||||
[
|
||||
'action' => 'view',
|
||||
'name' => [
|
||||
'ko' => '신고 조회',
|
||||
'en' => 'View Reports',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '신고 목록 및 상세 조회',
|
||||
'en' => 'View report list and details',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin', 'manager'],
|
||||
],
|
||||
[
|
||||
'action' => 'manage',
|
||||
'name' => [
|
||||
'ko' => '신고 처리',
|
||||
'en' => 'Manage Reports',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '신고 상태 변경 및 처리',
|
||||
'en' => 'Update report status and process',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 설정 파일 목록 반환
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'sirsoft-board' => $this->getModulePath().'/config/board.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 설치 시 실행할 시더 목록 반환
|
||||
*
|
||||
* BoardTypeSeeder: 기본 게시판 유형(basic, gallery, card) 생성
|
||||
* InstallSeeder: 기본 메일 템플릿 5종 생성
|
||||
*
|
||||
* 그 외 시더(DatabaseSeeder, BoardSampleSeeder, PostSampleSeeder, ReportSampleSeeder)는
|
||||
* 테스트용 더미 데이터 시더이므로 설치 시 실행하지 않습니다.
|
||||
* 테스트 데이터가 필요한 경우 수동으로 실행하세요:
|
||||
* php artisan module:seed sirsoft-board
|
||||
*
|
||||
* @return array<class-string<Seeder>> 시더 클래스명 배열 (FQCN)
|
||||
*/
|
||||
public function getSeeders(): array
|
||||
{
|
||||
return [
|
||||
BoardTypeSeeder::class,
|
||||
InstallSeeder::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 훅 리스너 목록 반환
|
||||
*/
|
||||
public function getHookListeners(): array
|
||||
{
|
||||
return [
|
||||
UserNotificationSettingsListener::class,
|
||||
SearchPostsListener::class,
|
||||
BoardNotificationListener::class,
|
||||
BoardNotificationChannelListener::class,
|
||||
BoardActivityLogListener::class,
|
||||
ActivityLogDescriptionResolver::class,
|
||||
SeoBoardCacheListener::class,
|
||||
SeoBoardSettingsCacheListener::class,
|
||||
EcommerceInquiryHookListener::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스토리지 디스크 설정 반환
|
||||
*/
|
||||
public function getStorageDisk(): string
|
||||
{
|
||||
return config('sirsoft-board.attachment.disk', 'modules');
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 메뉴 정의
|
||||
*/
|
||||
public function getAdminMenus(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => [
|
||||
'ko' => '게시판 관리',
|
||||
'en' => 'Board Management',
|
||||
],
|
||||
'slug' => 'sirsoft-board',
|
||||
'url' => null,
|
||||
'icon' => 'fas fa-clipboard-list',
|
||||
'order' => 30,
|
||||
'children' => [
|
||||
[
|
||||
'name' => [
|
||||
'ko' => '환경설정',
|
||||
'en' => 'Settings',
|
||||
],
|
||||
'slug' => 'sirsoft-board-settings',
|
||||
'url' => '/admin/boards/settings',
|
||||
'icon' => 'fas fa-cog',
|
||||
'order' => 1,
|
||||
'permission' => 'sirsoft-board.settings.read',
|
||||
],
|
||||
[
|
||||
'name' => [
|
||||
'ko' => '게시판 목록',
|
||||
'en' => 'Board List',
|
||||
],
|
||||
'slug' => 'sirsoft-board-list',
|
||||
'url' => '/admin/boards',
|
||||
'icon' => 'fas fa-list',
|
||||
'order' => 2,
|
||||
'permission' => 'sirsoft-board.boards.read',
|
||||
],
|
||||
[
|
||||
'name' => [
|
||||
'ko' => '게시판 신고현황',
|
||||
'en' => 'Board Reports',
|
||||
],
|
||||
'slug' => 'sirsoft-board-reports',
|
||||
'url' => '/admin/boards/reports',
|
||||
'icon' => 'fas fa-flag',
|
||||
'order' => 3,
|
||||
'permission' => 'sirsoft-board.reports.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+1881
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@g7/sirsoft-board",
|
||||
"version": "1.0.0-beta.1",
|
||||
"description": "그누보드7 게시판 모듈 프론트엔드 에셋",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^27.4.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.4.14",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "board_list_for_inquiry",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/boards",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"params": {
|
||||
"per_page": "200"
|
||||
},
|
||||
"fallback": null
|
||||
}
|
||||
],
|
||||
"extension_point": "inquiry_board_setting",
|
||||
"mode": "replace",
|
||||
"components": [
|
||||
{
|
||||
"id": "inquiry_board_setting_active",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inquiry_board_setting_description",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.description"
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-1.5"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inquiry_board_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.board_label"
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_select",
|
||||
"type": "basic",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "inquiry.board_slug",
|
||||
"value": "{{_local.form?.inquiry?.board_slug ?? ''}}",
|
||||
"options": "{{[{value: '', label: $t('sirsoft-board.admin.ecommerce_settings.inquiry.board_placeholder')}].concat((board_list_for_inquiry?.data?.data ?? []).map(b => ({value: b.slug, label: b.name})))}}",
|
||||
"className": "{{_local.errors?.['inquiry.board_slug'] ? 'input input-error' : 'input'}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_select_error",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.['inquiry.board_slug']}}",
|
||||
"props": {
|
||||
"className": "text-red-500 text-xs mt-1 block"
|
||||
},
|
||||
"text": "{{_local.errors?.['inquiry.board_slug']?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_hints",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inquiry_board_hint_secret",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.hint_secret"
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_hint_category",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.hint_category"
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_actions",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{!!(board_list_for_inquiry?.data?.data ?? []).find(b => b.slug === (settings?.data?.inquiry?.board_slug ?? ''))?.id}}",
|
||||
"props": {
|
||||
"className": "flex flex-col md:flex-row gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inquiry_board_go_edit_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col justify-between gap-3 flex-1 p-3 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "flex items-start gap-1.5 text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": { "name": "gear", "className": "w-3 h-3 shrink-0 mt-0.5" }
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.go_to_board_edit_desc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_go_edit",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "self-start inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": { "name": "pen-to-square", "className": "w-3 h-3" }
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.go_to_board_edit"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/{{(board_list_for_inquiry?.data?.data ?? []).find(b => b.slug === (settings?.data?.inquiry?.board_slug ?? ''))?.id}}/edit"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_go_posts_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col justify-between gap-3 flex-1 p-3 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "flex items-start gap-1.5 text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": { "name": "clipboard-list", "className": "w-3 h-3 shrink-0 mt-0.5" }
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.go_to_posts_desc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "inquiry_board_go_posts",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": { "name": "list", "className": "w-3 h-3" }
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.go_to_posts"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{settings?.data?.inquiry?.board_slug}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inquiry_board_add_menu",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-900 border border-blue-500 dark:border-blue-400 rounded hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": { "name": "plus", "className": "w-3 h-3" }
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.ecommerce_settings.inquiry.add_to_admin_menu"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "apiCall",
|
||||
"target": "/api/modules/sirsoft-board/admin/boards/{{(board_list_for_inquiry?.data?.data ?? []).find(b => b.slug === (settings?.data?.inquiry?.board_slug ?? ''))?.id}}/add-to-menu",
|
||||
"params": { "method": "POST" },
|
||||
"auth_required": true,
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": { "type": "success", "message": "{{result.message}}" }
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": { "dataSourceId": "admin_menu", "sync": true }
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": { "type": "error", "message": "{{error.message}}" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"priority": 100
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"target_layout": "admin_user_detail",
|
||||
"injections": [
|
||||
{
|
||||
"target_id": "user_detail_tabs",
|
||||
"position": "inject_props",
|
||||
"props": {
|
||||
"tabs": {
|
||||
"_append": [
|
||||
{
|
||||
"id": "ext_notification",
|
||||
"label": "$t:sirsoft-board.admin.users.form.sections.notification_settings",
|
||||
"iconName": "cubes"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"target_id": "extension_tab_content",
|
||||
"position": "append_child",
|
||||
"components": [
|
||||
{
|
||||
"id": "ext_notification_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"id": "ext_notification",
|
||||
"className": "scroll-mt-32"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_notification_settings",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "bell",
|
||||
"className": "w-5 h-5 text-gray-600 dark:text-gray-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.sections.notification_settings"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "ExtensionBadge",
|
||||
"props": {
|
||||
"type": "module",
|
||||
"identifier": "sirsoft-board",
|
||||
"installedModules": "{{_global.installedModules}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_notify_post_complete",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.post_complete"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{user?.data?.notify_post_complete ? '$t:admin.users.detail.enabled' : '$t:admin.users.detail.disabled'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_post_reply",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.post_reply"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{user?.data?.notify_post_reply ? '$t:admin.users.detail.enabled' : '$t:admin.users.detail.disabled'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.comment"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{user?.data?.notify_comment ? '$t:admin.users.detail.enabled' : '$t:admin.users.detail.disabled'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_reply_comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.reply_comment"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{user?.data?.notify_reply_comment ? '$t:admin.users.detail.enabled' : '$t:admin.users.detail.disabled'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"priority": 300
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"target_layout": "admin_user_form",
|
||||
"injections": [
|
||||
{
|
||||
"target_id": "extension_slot",
|
||||
"position": "append_child",
|
||||
"components": [
|
||||
{
|
||||
"id": "section_notification_settings",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "bell",
|
||||
"className": "w-5 h-5 text-gray-600 dark:text-gray-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.sections.notification_settings"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "ExtensionBadge",
|
||||
"props": {
|
||||
"type": "module",
|
||||
"identifier": "sirsoft-board",
|
||||
"installedModules": "{{_global.installedModules}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_notify_post_complete",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"name": "notify_post_complete",
|
||||
"checked": "{{_local.form?.notify_post_complete ?? false}}",
|
||||
"className": "w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.post_complete"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_post_reply",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"name": "notify_post_reply",
|
||||
"checked": "{{_local.form?.notify_post_reply ?? false}}",
|
||||
"className": "w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.post_reply"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"name": "notify_comment",
|
||||
"checked": "{{_local.form?.notify_comment ?? false}}",
|
||||
"className": "w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.comment"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_reply_comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"name": "notify_reply_comment",
|
||||
"checked": "{{_local.form?.notify_reply_comment ?? false}}",
|
||||
"className": "w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.users.form.notifications.reply_comment"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"priority": 300
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 게시판 생성/수정 폼 레이아웃 JSON 구조 검증 테스트
|
||||
*
|
||||
* @description
|
||||
* - 기본 탭(_tab_basic): use_comment/use_reply 토글 제거, secret_mode→show_view_count 순서
|
||||
* - 게시글 탭(_tab_post): section_reply 분리, use_comment 서브필드 분리, condition→if 마이그레이션
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// 레이아웃 JSON 임포트
|
||||
import tabBasic from '../../../layouts/admin/partials/admin_board_form/_tab_basic.json';
|
||||
import tabPost from '../../../layouts/admin/partials/admin_board_form/_tab_post.json';
|
||||
import tabPermissions from '../../../layouts/admin/partials/admin_board_form/_tab_permissions.json';
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 ID를 가진 노드를 재귀적으로 찾습니다.
|
||||
*/
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node) return null;
|
||||
if (node.id === id) return node;
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
const found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
const found = findById(child as any, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 name을 가진 컴포넌트를 모두 찾습니다.
|
||||
*/
|
||||
function findByName(node: any, name: string): any[] {
|
||||
const results: any[] = [];
|
||||
if (!node) return results;
|
||||
|
||||
if (node.name === name) {
|
||||
results.push(node);
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findByName(child, name));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
results.push(...findByName(child as any, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 트리에서 form name prop으로 Input/Select/Toggle를 찾습니다.
|
||||
*/
|
||||
function findFormFields(node: any): string[] {
|
||||
const names: string[] = [];
|
||||
if (!node) return names;
|
||||
|
||||
if ((node.name === 'Input' || node.name === 'Select' || node.name === 'Toggle') && node.props?.name) {
|
||||
names.push(node.props.name);
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
names.push(...findFormFields(child));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
names.push(...findFormFields(child as any));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 트리에서 deprecated `condition` 속성을 가진 노드를 모두 찾습니다.
|
||||
*/
|
||||
function findConditionNodes(node: any): any[] {
|
||||
const results: any[] = [];
|
||||
if (!node) return results;
|
||||
|
||||
if (node.condition !== undefined) {
|
||||
results.push(node);
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findConditionNodes(child));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
results.push(...findConditionNodes(child as any));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// _tab_basic.json 테스트
|
||||
// ============================================================
|
||||
|
||||
describe('admin_board_form/_tab_basic.json', () => {
|
||||
it('파일이 유효한 JSON 구조이다', () => {
|
||||
expect(tabBasic).toBeDefined();
|
||||
expect(tabBasic.id).toBe('tab_content_basic');
|
||||
});
|
||||
|
||||
it('기본 섹션 카드에 use_comment, use_reply 토글이 없다 (게시글 탭으로 분리됨)', () => {
|
||||
// _tab_basic.json의 section_basic_card 하위에 use_comment, use_reply 없어야 함
|
||||
const fields = findFormFields(tabBasic);
|
||||
expect(fields).not.toContain('use_comment');
|
||||
expect(fields).not.toContain('use_reply');
|
||||
});
|
||||
|
||||
it('기본 탭에서 secret_mode가 show_view_count보다 앞에 위치한다', () => {
|
||||
// section_basic_card 하위 children에서 순서 확인
|
||||
const card = findById(tabBasic, 'section_basic_card');
|
||||
expect(card).toBeDefined();
|
||||
|
||||
const children = card.children ?? [];
|
||||
const secretIdx = children.findIndex((c: any) => c.id === 'field_secret_mode');
|
||||
const viewCountIdx = children.findIndex((c: any) => c.id === 'field_show_view_count');
|
||||
|
||||
expect(secretIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(viewCountIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(secretIdx).toBeLessThan(viewCountIdx);
|
||||
});
|
||||
|
||||
it('기본 탭에 deprecated condition 속성이 없다 (if로 마이그레이션 완료)', () => {
|
||||
const conditionNodes = findConditionNodes(tabBasic);
|
||||
expect(conditionNodes, 'condition 속성 노드가 남아있음 (if로 교체 필요)').toHaveLength(0);
|
||||
});
|
||||
|
||||
it('use_report 토글이 기본 탭에 있다', () => {
|
||||
const fields = findFormFields(tabBasic);
|
||||
expect(fields).toContain('use_report');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// _tab_post.json 테스트
|
||||
// ============================================================
|
||||
|
||||
describe('admin_board_form/_tab_post.json', () => {
|
||||
it('파일이 유효한 JSON 구조이다', () => {
|
||||
expect(tabPost).toBeDefined();
|
||||
expect(tabPost.id).toBe('tab_content_post');
|
||||
});
|
||||
|
||||
it('게시글 탭에 deprecated condition 속성이 없다 (if로 마이그레이션 완료)', () => {
|
||||
const conditionNodes = findConditionNodes(tabPost);
|
||||
expect(conditionNodes, 'condition 속성 노드가 남아있음 (if로 교체 필요)').toHaveLength(0);
|
||||
});
|
||||
|
||||
it('답변글 서브섹션(post_reply_section)이 존재한다', () => {
|
||||
const replySection = findById(tabPost, 'post_reply_section');
|
||||
expect(replySection).toBeDefined();
|
||||
});
|
||||
|
||||
it('답변글 서브섹션에 use_reply 토글과 max_reply_depth 필드가 있다', () => {
|
||||
const replySection = findById(tabPost, 'post_reply_section');
|
||||
expect(replySection).toBeDefined();
|
||||
const fields = findFormFields(replySection);
|
||||
|
||||
expect(fields).toContain('use_reply');
|
||||
expect(fields).toContain('max_reply_depth');
|
||||
});
|
||||
|
||||
it('max_reply_depth 서브필드가 use_reply 토글 ON 시에만 표시된다 (if 조건)', () => {
|
||||
// max_reply_depth_field Div 컨테이너에 if 조건이 적용됨
|
||||
const maxReplyDepthField = findById(tabPost, 'max_reply_depth_field');
|
||||
expect(maxReplyDepthField).toBeDefined();
|
||||
expect(maxReplyDepthField.if).toBeDefined();
|
||||
expect(maxReplyDepthField.if).toContain('use_reply');
|
||||
});
|
||||
|
||||
it('댓글 서브섹션(post_comment_section)이 존재하고 use_comment 토글을 포함한다', () => {
|
||||
const commentSection = findById(tabPost, 'post_comment_section');
|
||||
expect(commentSection).toBeDefined();
|
||||
const fields = findFormFields(commentSection);
|
||||
|
||||
expect(fields).toContain('use_comment');
|
||||
});
|
||||
|
||||
it('댓글 서브 필드들이 use_comment ON 시에만 표시된다 (if 조건)', () => {
|
||||
// use_comment_sub_fields 컨테이너에 if 조건이 적용됨
|
||||
const card = findById(tabPost, 'section_post_card');
|
||||
expect(card).toBeDefined();
|
||||
|
||||
// use_comment를 참조하는 if 속성을 가진 Div 컨테이너 확인
|
||||
function findIfContaining(node: any, keyword: string): any[] {
|
||||
const results: any[] = [];
|
||||
if (node?.if?.includes(keyword)) results.push(node);
|
||||
for (const c of node?.children ?? []) results.push(...findIfContaining(c, keyword));
|
||||
return results;
|
||||
}
|
||||
|
||||
const useCommentConditionals = findIfContaining(card, 'use_comment');
|
||||
expect(useCommentConditionals.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('post_reply_section이 post_comment_section보다 앞에 위치한다', () => {
|
||||
// section_post_card 하위 children에서 순서 확인
|
||||
const card = findById(tabPost, 'section_post_card');
|
||||
expect(card).toBeDefined();
|
||||
const children = card.children ?? [];
|
||||
const replyIdx = children.findIndex((c: any) => c.id === 'post_reply_section');
|
||||
const commentIdx = children.findIndex((c: any) => c.id === 'post_comment_section');
|
||||
|
||||
expect(replyIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(commentIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(replyIdx).toBeLessThan(commentIdx);
|
||||
});
|
||||
|
||||
it('게시글 탭에 use_file_upload 토글이 있다', () => {
|
||||
const fields = findFormFields(tabPost);
|
||||
expect(fields).toContain('use_file_upload');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// _tab_permissions.json 테스트 — board_manager_ids/board_step_ids uuid 참조
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 id를 가진 TagInput 컴포넌트를 찾습니다.
|
||||
* (_tab_permissions.json의 TagInput은 name prop 대신 id로 식별됨)
|
||||
*/
|
||||
function findTagInputById(node: any, id: string): any | null {
|
||||
if (!node) return null;
|
||||
if (node.name === 'TagInput' && node.id === id) return node;
|
||||
|
||||
for (const child of node.children ?? []) {
|
||||
const found = findTagInputById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
for (const slotChildren of Object.values(node.slots ?? {})) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
const found = findTagInputById(child as any, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TagInput의 actions 배열에서 type: "change" 핸들러의 특정 form 필드 표현식을 반환합니다.
|
||||
*/
|
||||
function getChangeActionFormField(tagInput: any, fieldName: string): string | null {
|
||||
if (!tagInput || !Array.isArray(tagInput.actions)) return null;
|
||||
for (const action of tagInput.actions) {
|
||||
if (action.type === 'change' && action.params?.form?.[fieldName]) {
|
||||
return action.params.form[fieldName] as string;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('admin_board_form/_tab_permissions.json', () => {
|
||||
it('파일이 유효한 JSON 구조이다', () => {
|
||||
expect(tabPermissions).toBeDefined();
|
||||
expect((tabPermissions as any).id).toBe('tab_content_permissions');
|
||||
});
|
||||
|
||||
it('manager TagInput options 표현식이 u.id가 아닌 u.uuid를 value로 사용한다', () => {
|
||||
const managerInput = findTagInputById(tabPermissions, 'permissions_managers_taginput');
|
||||
expect(managerInput).not.toBeNull();
|
||||
const options: string = managerInput.props.options ?? '';
|
||||
expect(options).not.toMatch(/\bu\.id\b/);
|
||||
expect(options).toContain('u.uuid');
|
||||
});
|
||||
|
||||
it('manager change 핸들러의 board_managers 표현식이 u.uuid로 필터링한다', () => {
|
||||
const managerInput = findTagInputById(tabPermissions, 'permissions_managers_taginput');
|
||||
expect(managerInput).not.toBeNull();
|
||||
const expr = getChangeActionFormField(managerInput, 'board_managers');
|
||||
expect(expr).not.toBeNull();
|
||||
expect(expr).not.toMatch(/\bu\.id\b/);
|
||||
expect(expr).toContain('u.uuid');
|
||||
});
|
||||
|
||||
it('step TagInput options 표현식이 u.uuid를 value로 사용한다', () => {
|
||||
const stepInput = findTagInputById(tabPermissions, 'permissions_steps_taginput');
|
||||
expect(stepInput).not.toBeNull();
|
||||
const options: string = stepInput.props.options ?? '';
|
||||
expect(options).not.toMatch(/\bu\.id\b/);
|
||||
expect(options).toContain('u.uuid');
|
||||
});
|
||||
|
||||
it('step change 핸들러의 board_steps 표현식이 u.uuid로 필터링한다', () => {
|
||||
const stepInput = findTagInputById(tabPermissions, 'permissions_steps_taginput');
|
||||
expect(stepInput).not.toBeNull();
|
||||
const expr = getChangeActionFormField(stepInput, 'board_steps');
|
||||
expect(expr).not.toBeNull();
|
||||
expect(expr).not.toMatch(/\bu\.id\b/);
|
||||
expect(expr).toContain('u.uuid');
|
||||
});
|
||||
});
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* 게시판 첨부파일 업로드 방식 변경 - 레이아웃 JSON 구조 검증 테스트
|
||||
*
|
||||
* @description
|
||||
* - 관리자 첨부파일 partial: autoUpload=false, uploadTriggerEvent 설정 검증
|
||||
* - 관리자 폼 저장 sequence: emitEvent가 apiCall 앞에 위치하는지 검증
|
||||
* - 사용자 폼 동일 패턴 적용 검증
|
||||
*
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// 레이아웃 JSON 임포트
|
||||
import adminAttachments from '../../../layouts/admin/partials/admin_board_post_form/_attachments.json';
|
||||
import adminPostForm from '../../../layouts/admin/admin_board_post_form.json';
|
||||
|
||||
// 사용자 폼은 templates 디렉토리에 있으므로 상대경로 사용
|
||||
// JSON 파일은 직접 import (vitest가 자동 파싱)
|
||||
import userPostForm from '../../../../../../templates/sirsoft-basic/layouts/partials/board/form/_post_form.json';
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 타입의 컴포넌트를 재귀적으로 찾습니다.
|
||||
*/
|
||||
function findComponentsByName(node: any, name: string): any[] {
|
||||
const results: any[] = [];
|
||||
if (!node) return results;
|
||||
|
||||
if (node.name === name) {
|
||||
results.push(node);
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findComponentsByName(child, name));
|
||||
}
|
||||
}
|
||||
|
||||
// slots 내부도 검색
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
results.push(...findComponentsByName(child as any, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 ID를 가진 노드를 재귀적으로 찾습니다.
|
||||
*/
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node) return null;
|
||||
if (node.id === id) return node;
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
const found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
const found = findById(child as any, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* sequence 액션 내에서 handler 목록을 추출합니다.
|
||||
*/
|
||||
function getSequenceHandlers(actions: any[]): string[] {
|
||||
for (const action of actions) {
|
||||
if (action.handler === 'sequence' && action.actions) {
|
||||
return action.actions.map((a: any) => a.handler);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
describe('관리자 첨부파일 Partial (_attachments.json)', () => {
|
||||
it('FileUploader에 autoUpload: false가 설정되어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(adminAttachments, 'FileUploader');
|
||||
expect(fileUploaders.length).toBeGreaterThan(0);
|
||||
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.autoUpload).toBe(false);
|
||||
});
|
||||
|
||||
it('FileUploader에 uploadTriggerEvent가 설정되어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(adminAttachments, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.uploadTriggerEvent).toBe('upload:board_attachments');
|
||||
});
|
||||
|
||||
it('FileUploader에 uploadParams가 temp_key를 포함해야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(adminAttachments, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.uploadParams).toBeDefined();
|
||||
expect(uploader.props.uploadParams.temp_key).toBeDefined();
|
||||
expect(uploader.props.uploadParams.temp_key).toContain('_local.tempKey');
|
||||
});
|
||||
|
||||
it('FileUploader의 upload URL에 쿼리스트링이 없어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(adminAttachments, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
const uploadUrl = uploader.props.apiEndpoints.upload;
|
||||
|
||||
// URL에 쿼리스트링 파라미터가 포함되지 않아야 함 (?.은 optional chaining이므로 허용)
|
||||
expect(uploadUrl).not.toMatch(/\?[a-z_]+=/) // ?key= 형태의 쿼리스트링 금지
|
||||
expect(uploadUrl).not.toContain('temp_key=');
|
||||
expect(uploadUrl).not.toContain('post_id=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('관리자 폼 저장 버튼 (admin_board_post_form.json)', () => {
|
||||
it('header_save_button의 sequence에 emitEvent가 포함되어야 합니다', () => {
|
||||
const headerButton = findById(adminPostForm, 'header_save_button');
|
||||
expect(headerButton).not.toBeNull();
|
||||
|
||||
const handlers = getSequenceHandlers(headerButton.actions);
|
||||
expect(handlers).toContain('emitEvent');
|
||||
});
|
||||
|
||||
it('header_save_button의 emitEvent가 apiCall 앞에 위치해야 합니다', () => {
|
||||
const headerButton = findById(adminPostForm, 'header_save_button');
|
||||
const handlers = getSequenceHandlers(headerButton.actions);
|
||||
|
||||
const emitIndex = handlers.indexOf('emitEvent');
|
||||
const apiCallIndex = handlers.indexOf('apiCall');
|
||||
|
||||
expect(emitIndex).toBeGreaterThan(-1);
|
||||
expect(apiCallIndex).toBeGreaterThan(-1);
|
||||
expect(emitIndex).toBeLessThan(apiCallIndex);
|
||||
});
|
||||
|
||||
it('header_save_button의 emitEvent가 upload:board_attachments 이벤트를 발행해야 합니다', () => {
|
||||
const headerButton = findById(adminPostForm, 'header_save_button');
|
||||
const sequenceAction = headerButton.actions.find((a: any) => a.handler === 'sequence');
|
||||
const emitAction = sequenceAction.actions.find((a: any) => a.handler === 'emitEvent');
|
||||
|
||||
expect(emitAction.params.event).toBe('upload:board_attachments');
|
||||
});
|
||||
|
||||
it('footer_save_button의 sequence에 emitEvent가 포함되어야 합니다', () => {
|
||||
const footerButton = findById(adminPostForm, 'footer_save_button');
|
||||
expect(footerButton).not.toBeNull();
|
||||
|
||||
const handlers = getSequenceHandlers(footerButton.actions);
|
||||
expect(handlers).toContain('emitEvent');
|
||||
});
|
||||
|
||||
it('footer_save_button의 emitEvent가 apiCall 앞에 위치해야 합니다', () => {
|
||||
const footerButton = findById(adminPostForm, 'footer_save_button');
|
||||
const handlers = getSequenceHandlers(footerButton.actions);
|
||||
|
||||
const emitIndex = handlers.indexOf('emitEvent');
|
||||
const apiCallIndex = handlers.indexOf('apiCall');
|
||||
|
||||
expect(emitIndex).toBeGreaterThan(-1);
|
||||
expect(apiCallIndex).toBeGreaterThan(-1);
|
||||
expect(emitIndex).toBeLessThan(apiCallIndex);
|
||||
});
|
||||
|
||||
it('저장 sequence 순서: setState → emitEvent → apiCall', () => {
|
||||
const headerButton = findById(adminPostForm, 'header_save_button');
|
||||
const handlers = getSequenceHandlers(headerButton.actions);
|
||||
|
||||
expect(handlers[0]).toBe('setState');
|
||||
expect(handlers[1]).toBe('emitEvent');
|
||||
expect(handlers[2]).toBe('apiCall');
|
||||
});
|
||||
|
||||
it('apiCall body에 temp_key가 포함되어야 합니다', () => {
|
||||
const headerButton = findById(adminPostForm, 'header_save_button');
|
||||
const sequenceAction = headerButton.actions.find((a: any) => a.handler === 'sequence');
|
||||
const apiCallAction = sequenceAction.actions.find((a: any) => a.handler === 'apiCall');
|
||||
|
||||
expect(apiCallAction.params.body).toContain('temp_key');
|
||||
expect(apiCallAction.params.body).toContain('_local.tempKey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('사용자 폼 (_post_form.json)', () => {
|
||||
it('FileUploader에 autoUpload: false가 설정되어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(userPostForm, 'FileUploader');
|
||||
expect(fileUploaders.length).toBeGreaterThan(0);
|
||||
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.autoUpload).toBe(false);
|
||||
});
|
||||
|
||||
it('FileUploader에 uploadTriggerEvent가 설정되어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(userPostForm, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.uploadTriggerEvent).toBe('upload:board_attachments');
|
||||
});
|
||||
|
||||
it('FileUploader에 uploadParams가 temp_key를 포함해야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(userPostForm, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.uploadParams).toBeDefined();
|
||||
expect(uploader.props.uploadParams.temp_key).toContain('_local.tempKey');
|
||||
});
|
||||
|
||||
it('FileUploader의 upload URL에 쿼리스트링이 없어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(userPostForm, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
const uploadUrl = uploader.props.apiEndpoints.upload;
|
||||
|
||||
expect(uploadUrl).not.toContain('?');
|
||||
expect(uploadUrl).not.toContain('temp_key=');
|
||||
expect(uploadUrl).not.toContain('post_id=');
|
||||
});
|
||||
|
||||
it('저장 버튼의 sequence에 emitEvent가 포함되어야 합니다', () => {
|
||||
// 사용자 폼에서 저장 버튼은 children 마지막 영역에 위치
|
||||
const allButtons = findComponentsByName(userPostForm, 'Button');
|
||||
|
||||
// sequence + apiCall 패턴을 가진 버튼 찾기
|
||||
const saveButton = allButtons.find((btn: any) => {
|
||||
if (!btn.actions) return false;
|
||||
return btn.actions.some((a: any) =>
|
||||
a.handler === 'sequence' &&
|
||||
a.actions?.some((sa: any) => sa.handler === 'apiCall')
|
||||
);
|
||||
});
|
||||
|
||||
expect(saveButton).toBeDefined();
|
||||
const handlers = getSequenceHandlers(saveButton.actions);
|
||||
expect(handlers).toContain('emitEvent');
|
||||
});
|
||||
|
||||
it('저장 버튼의 emitEvent가 apiCall 앞에 위치해야 합니다', () => {
|
||||
const allButtons = findComponentsByName(userPostForm, 'Button');
|
||||
const saveButton = allButtons.find((btn: any) => {
|
||||
if (!btn.actions) return false;
|
||||
return btn.actions.some((a: any) =>
|
||||
a.handler === 'sequence' &&
|
||||
a.actions?.some((sa: any) => sa.handler === 'apiCall')
|
||||
);
|
||||
});
|
||||
|
||||
const handlers = getSequenceHandlers(saveButton.actions);
|
||||
const emitIndex = handlers.indexOf('emitEvent');
|
||||
const apiCallIndex = handlers.indexOf('apiCall');
|
||||
|
||||
expect(emitIndex).toBeGreaterThan(-1);
|
||||
expect(apiCallIndex).toBeGreaterThan(-1);
|
||||
expect(emitIndex).toBeLessThan(apiCallIndex);
|
||||
});
|
||||
|
||||
it('저장 sequence 순서: setState → emitEvent → apiCall', () => {
|
||||
const allButtons = findComponentsByName(userPostForm, 'Button');
|
||||
const saveButton = allButtons.find((btn: any) => {
|
||||
if (!btn.actions) return false;
|
||||
return btn.actions.some((a: any) =>
|
||||
a.handler === 'sequence' &&
|
||||
a.actions?.some((sa: any) => sa.handler === 'apiCall')
|
||||
);
|
||||
});
|
||||
|
||||
const handlers = getSequenceHandlers(saveButton.actions);
|
||||
expect(handlers[0]).toBe('setState');
|
||||
expect(handlers[1]).toBe('emitEvent');
|
||||
expect(handlers[2]).toBe('apiCall');
|
||||
});
|
||||
});
|
||||
|
||||
describe('업로드 에러 시 isSaving 복구 (onUploadError)', () => {
|
||||
it('관리자 첨부파일 onUploadError에서 isSaving: false를 설정해야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(adminAttachments, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
const errorAction = uploader.actions.find((a: any) => a.event === 'onUploadError');
|
||||
|
||||
expect(errorAction).toBeDefined();
|
||||
// sequence 내 setState에서 isSaving: false 확인
|
||||
const setStateAction = errorAction.actions.find((a: any) => a.handler === 'setState');
|
||||
expect(setStateAction).toBeDefined();
|
||||
expect(setStateAction.params.isSaving).toBe(false);
|
||||
});
|
||||
|
||||
it('사용자 폼 onUploadError에서 isSaving: false를 설정해야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(userPostForm, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
const errorAction = uploader.actions.find((a: any) => a.event === 'onUploadError');
|
||||
|
||||
expect(errorAction).toBeDefined();
|
||||
const setStateAction = errorAction.actions.find((a: any) => a.handler === 'setState');
|
||||
expect(setStateAction).toBeDefined();
|
||||
expect(setStateAction.params.isSaving).toBe(false);
|
||||
});
|
||||
|
||||
it('관리자 첨부파일 uploadParams에 post_id가 없어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(adminAttachments, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.uploadParams.post_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('사용자 폼 uploadParams에 post_id가 없어야 합니다', () => {
|
||||
const fileUploaders = findComponentsByName(userPostForm, 'FileUploader');
|
||||
const uploader = fileUploaders[0];
|
||||
expect(uploader.props.uploadParams.post_id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+581
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* 게시판 설정 메일 템플릿 탭/모달 레이아웃 JSON 구조 검증 테스트
|
||||
*
|
||||
* @description
|
||||
* - 탭 Partial: _tab_mail_templates.json
|
||||
* - 모달 Partial: _modal_mail_template_edit.json, _modal_mail_template_preview.json
|
||||
*
|
||||
* 검증 항목:
|
||||
* - 탭 조건부 렌더링 (activeBoardSettingsTab === 'mail_templates')
|
||||
* - 검색/필터 바 (검색타입 Select + Input + 검색/초기화 버튼)
|
||||
* - 총 건수 + per_page 선택 + 페이지네이션
|
||||
* - iteration 기반 템플릿 카드 목록 렌더링 (페이지네이션 데이터 경로)
|
||||
* - 빈 상태 (검색결과 없음 / 데이터 없음 분기)
|
||||
* - 카드 내 토글/편집/리셋 버튼 구조 및 API 호출
|
||||
* - 변수 목록 조건부 표시 (variables.length > 0)
|
||||
* - 타입 표시 분기 (is_default 여부)
|
||||
* - navigate 기반 갱신 방식 (refetchDataSource 대신)
|
||||
* - 편집 모달: blur_until_loaded, sticky footer, IIFE body, is_active toggle
|
||||
* - 편집 버튼 상태 초기화 (templateErrors: null, isSaving: false)
|
||||
* - 미리보기 모달 구조
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// 레이아웃 JSON 임포트
|
||||
import mainLayout from '../../../layouts/admin/admin_board_settings.json';
|
||||
import tabMailTemplates from '../../../layouts/admin/partials/admin_board_settings/_tab_mail_templates.json';
|
||||
import modalEdit from '../../../layouts/admin/partials/admin_board_settings/_modal_mail_template_edit.json';
|
||||
import modalPreview from '../../../layouts/admin/partials/admin_board_settings/_modal_mail_template_preview.json';
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 ID를 가진 노드를 재귀적으로 찾습니다.
|
||||
*/
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node) return null;
|
||||
if (node.id === id) return node;
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
const found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
const found = findById(child as any, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 메인 레이아웃 (admin_board_settings.json) 내 메일 템플릿 연동 검증
|
||||
// ============================================================================
|
||||
|
||||
describe('admin_board_settings.json 메일 템플릿 연동', () => {
|
||||
it('data_sources에 boardMailTemplates가 정의되어 있다', () => {
|
||||
const dataSources = mainLayout.data_sources;
|
||||
const mailDs = dataSources.find((ds: any) => ds.id === 'boardMailTemplates');
|
||||
expect(mailDs).toBeDefined();
|
||||
expect(mailDs!.endpoint).toBe('/api/modules/sirsoft-board/admin/mail-templates');
|
||||
expect(mailDs!.method).toBe('GET');
|
||||
expect(mailDs!.auto_fetch).toBe(true);
|
||||
});
|
||||
|
||||
it('data_sources params에 페이지네이션/검색 파라미터가 있다', () => {
|
||||
const dataSources = mainLayout.data_sources;
|
||||
const mailDs = dataSources.find((ds: any) => ds.id === 'boardMailTemplates');
|
||||
expect(mailDs).toBeDefined();
|
||||
expect(mailDs!.params).toBeDefined();
|
||||
const paramsStr = JSON.stringify(mailDs!.params);
|
||||
expect(paramsStr).toContain('query.page');
|
||||
expect(paramsStr).toContain('query.per_page');
|
||||
expect(paramsStr).toContain('query.search');
|
||||
expect(paramsStr).toContain('query.search_type');
|
||||
});
|
||||
|
||||
it('탭 네비게이션에 mail_templates 탭이 포함되어 있다', () => {
|
||||
const slotContent = (mainLayout as any).slots?.content;
|
||||
|
||||
function findTabNav(node: any): any | null {
|
||||
if (!node) return null;
|
||||
if (node.name === 'TabNavigation') return node;
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
const found = findTabNav(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabNav = findTabNav({ children: slotContent });
|
||||
expect(tabNav).toBeDefined();
|
||||
|
||||
const tabIds = tabNav.props.tabs.map((t: any) => t.id);
|
||||
expect(tabIds).toContain('mail_templates');
|
||||
});
|
||||
|
||||
it('slots에 _tab_mail_templates.json partial이 참조되어 있다', () => {
|
||||
const slotContent = (mainLayout as any).slots;
|
||||
const partials = JSON.stringify(slotContent);
|
||||
expect(partials).toContain('_tab_mail_templates.json');
|
||||
});
|
||||
|
||||
it('modals에 편집 모달 partial이 참조되어 있다', () => {
|
||||
const modals = mainLayout.modals;
|
||||
const modalsStr = JSON.stringify(modals);
|
||||
expect(modalsStr).toContain('_modal_mail_template_edit.json');
|
||||
});
|
||||
|
||||
it('modals에 미리보기 모달 partial이 참조되어 있다', () => {
|
||||
const modals = mainLayout.modals;
|
||||
const modalsStr = JSON.stringify(modals);
|
||||
expect(modalsStr).toContain('_modal_mail_template_preview.json');
|
||||
});
|
||||
|
||||
it('initLocal에 mailTemplateFilter가 있다', () => {
|
||||
const initLocal = (mainLayout as any).initLocal;
|
||||
expect(initLocal).toBeDefined();
|
||||
expect(initLocal.mailTemplateFilter).toBeDefined();
|
||||
expect(initLocal.mailTemplateFilter.search).toBe('');
|
||||
expect(initLocal.mailTemplateFilter.searchType).toBe('all');
|
||||
});
|
||||
|
||||
it('initLocal에 expandedMailTemplateIds가 빈 배열로 있다', () => {
|
||||
const initLocal = (mainLayout as any).initLocal;
|
||||
expect(initLocal.expandedMailTemplateIds).toBeDefined();
|
||||
expect(initLocal.expandedMailTemplateIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 탭 Partial (_tab_mail_templates.json) 구조 검증
|
||||
// ============================================================================
|
||||
|
||||
describe('_tab_mail_templates.json 구조 검증', () => {
|
||||
it('partial 메타 정보가 올바르다', () => {
|
||||
expect(tabMailTemplates.meta.is_partial).toBe(true);
|
||||
});
|
||||
|
||||
it('mail_templates 탭일 때만 표시된다 (조건부 렌더링)', () => {
|
||||
expect(tabMailTemplates.if).toContain('mail_templates');
|
||||
expect(tabMailTemplates.if).toContain('_global.activeBoardSettingsTab');
|
||||
});
|
||||
|
||||
it('헤더에 제목과 설명이 있다', () => {
|
||||
const header = findById(tabMailTemplates, 'board_mail_templates_header');
|
||||
expect(header).toBeDefined();
|
||||
expect(header.type).toBe('basic');
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 검색/필터 바
|
||||
// ========================================================================
|
||||
|
||||
describe('검색/필터 바', () => {
|
||||
it('검색 바 영역이 존재한다', () => {
|
||||
const search = findById(tabMailTemplates, 'board_mail_templates_search');
|
||||
expect(search).toBeDefined();
|
||||
});
|
||||
|
||||
it('검색 타입 Select (all/subject/body)가 있다', () => {
|
||||
const search = findById(tabMailTemplates, 'board_mail_templates_search');
|
||||
const searchStr = JSON.stringify(search);
|
||||
expect(searchStr).toContain('search_type_all');
|
||||
expect(searchStr).toContain('search_type_subject');
|
||||
expect(searchStr).toContain('search_type_body');
|
||||
});
|
||||
|
||||
it('검색 Input이 있다', () => {
|
||||
const search = findById(tabMailTemplates, 'board_mail_templates_search');
|
||||
const searchStr = JSON.stringify(search);
|
||||
expect(searchStr).toContain('search_placeholder');
|
||||
expect(searchStr).toContain('mailTemplateFilter');
|
||||
});
|
||||
|
||||
it('검색 버튼이 navigate 핸들러를 사용한다', () => {
|
||||
const search = findById(tabMailTemplates, 'board_mail_templates_search');
|
||||
const searchStr = JSON.stringify(search);
|
||||
expect(searchStr).toContain('btn_search');
|
||||
expect(searchStr).toContain('"handler":"navigate"');
|
||||
});
|
||||
|
||||
it('초기화 버튼이 setState + navigate sequence를 실행한다', () => {
|
||||
const search = findById(tabMailTemplates, 'board_mail_templates_search');
|
||||
const searchStr = JSON.stringify(search);
|
||||
expect(searchStr).toContain('btn_reset_filter');
|
||||
expect(searchStr).toContain('"handler":"sequence"');
|
||||
expect(searchStr).toContain('"handler":"setState"');
|
||||
expect(searchStr).toContain('"handler":"navigate"');
|
||||
});
|
||||
|
||||
it('Enter 키로 검색이 가능하다', () => {
|
||||
const search = findById(tabMailTemplates, 'board_mail_templates_search');
|
||||
const searchStr = JSON.stringify(search);
|
||||
expect(searchStr).toContain('"type":"keydown"');
|
||||
expect(searchStr).toContain('"key":"Enter"');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 정보 표시 줄 (총 건수 + per_page)
|
||||
// ========================================================================
|
||||
|
||||
describe('정보 표시 줄', () => {
|
||||
it('총 건수를 표시한다', () => {
|
||||
const info = findById(tabMailTemplates, 'board_mail_templates_info');
|
||||
expect(info).toBeDefined();
|
||||
const infoStr = JSON.stringify(info);
|
||||
expect(infoStr).toContain('total_count');
|
||||
expect(infoStr).toContain('boardMailTemplates?.data?.pagination?.total');
|
||||
});
|
||||
|
||||
it('per_page Select (10/20/50/100)가 있다', () => {
|
||||
const info = findById(tabMailTemplates, 'board_mail_templates_info');
|
||||
const infoStr = JSON.stringify(info);
|
||||
expect(infoStr).toContain('per_page_label');
|
||||
expect(infoStr).toContain('"10"');
|
||||
expect(infoStr).toContain('"20"');
|
||||
expect(infoStr).toContain('"50"');
|
||||
expect(infoStr).toContain('"100"');
|
||||
});
|
||||
|
||||
it('per_page 변경 시 navigate를 사용한다', () => {
|
||||
const info = findById(tabMailTemplates, 'board_mail_templates_info');
|
||||
const infoStr = JSON.stringify(info);
|
||||
expect(infoStr).toContain('"handler":"navigate"');
|
||||
expect(infoStr).toContain('per_page');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 템플릿 카드 목록
|
||||
// ========================================================================
|
||||
|
||||
describe('템플릿 카드 목록', () => {
|
||||
it('iteration 소스가 페이지네이션 데이터 경로를 사용한다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
expect(card).toBeDefined();
|
||||
expect(card.iteration).toBeDefined();
|
||||
expect(card.iteration.source).toContain('boardMailTemplates?.data?.data');
|
||||
expect(card.iteration.item_var).toBe('tpl');
|
||||
expect(card.iteration.index_var).toBe('tplIdx');
|
||||
});
|
||||
|
||||
it('is_default 여부로 타입 표시를 분기한다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('tpl.is_default');
|
||||
// is_default인 경우 $t: 번역 키, 아닌 경우 raw type
|
||||
expect(cardStr).toContain('sirsoft-board.admin.settings.mail_templates.types');
|
||||
expect(cardStr).toContain('{{tpl.type}}');
|
||||
});
|
||||
|
||||
it('카드에 활성/비활성 배지가 있다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('tpl.is_active');
|
||||
});
|
||||
|
||||
it('카드에 로케일 제목 미리보기가 있다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('tpl.subject');
|
||||
});
|
||||
|
||||
it('카드의 변수가 조건부로 표시된다 (variables.length > 0)', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('(tpl.variables ?? []).length > 0');
|
||||
expect(cardStr).toContain('variable.key');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 토글 버튼
|
||||
// ========================================================================
|
||||
|
||||
describe('토글 버튼', () => {
|
||||
it('PATCH /toggle-active API를 호출한다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('/api/modules/sirsoft-board/admin/mail-templates/');
|
||||
expect(cardStr).toContain('toggle-active');
|
||||
expect(cardStr).toContain('"method":"PATCH"');
|
||||
});
|
||||
|
||||
it('성공 시 navigate를 사용한다 (refetchDataSource 대신)', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('"handler":"navigate"');
|
||||
expect(cardStr).toContain('"mergeQuery":true');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 편집 버튼
|
||||
// ========================================================================
|
||||
|
||||
describe('편집 버튼', () => {
|
||||
it('sequence 핸들러로 setState + openModal을 실행한다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('"handler":"setState"');
|
||||
expect(cardStr).toContain('"editingTemplate"');
|
||||
expect(cardStr).toContain('"handler":"openModal"');
|
||||
expect(cardStr).toContain('modal_board_mail_template_edit');
|
||||
});
|
||||
|
||||
it('편집 시 상태 초기화 (templateErrors: null, isSaving: false)', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('"templateErrors":null');
|
||||
expect(cardStr).toContain('"isSaving":false');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 리셋 버튼
|
||||
// ========================================================================
|
||||
|
||||
describe('리셋 버튼', () => {
|
||||
it('POST /reset API를 호출한다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('/reset');
|
||||
expect(cardStr).toContain('"method":"POST"');
|
||||
});
|
||||
|
||||
it('is_default가 아닐 때만 표시된다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
expect(cardStr).toContain('tpl.is_default');
|
||||
});
|
||||
|
||||
it('성공 시 navigate를 사용한다', () => {
|
||||
const card = findById(tabMailTemplates, 'board_template_card');
|
||||
const cardStr = JSON.stringify(card);
|
||||
// reset onSuccess에도 navigate가 사용됨
|
||||
expect(cardStr).toContain('"handler":"navigate"');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 본문 펼치기/접기 토글
|
||||
// ========================================================================
|
||||
|
||||
describe('본문 펼치기/접기 토글', () => {
|
||||
it('펼치기 버튼이 카드에 존재한다', () => {
|
||||
const cardStr = JSON.stringify(tabMailTemplates);
|
||||
expect(cardStr).toContain('board_btn_expand_');
|
||||
expect(cardStr).toContain('fa-chevron-down');
|
||||
expect(cardStr).toContain('fa-chevron-up');
|
||||
});
|
||||
|
||||
it('펼치기 버튼이 expandedMailTemplateIds 상태를 토글한다', () => {
|
||||
const cardStr = JSON.stringify(tabMailTemplates);
|
||||
expect(cardStr).toContain('expandedMailTemplateIds');
|
||||
expect(cardStr).toContain('.includes(tpl.id)');
|
||||
expect(cardStr).toContain('.filter(');
|
||||
});
|
||||
|
||||
it('본문 미리보기가 펼침 상태일 때만 표시된다', () => {
|
||||
const cardStr = JSON.stringify(tabMailTemplates);
|
||||
// body preview section with if condition
|
||||
expect(cardStr).toContain('expandedMailTemplateIds');
|
||||
expect(cardStr).toContain('tpl.body');
|
||||
expect(cardStr).toContain('edit_modal.body');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 빈 상태
|
||||
// ========================================================================
|
||||
|
||||
describe('빈 상태', () => {
|
||||
it('빈 상태 영역이 존재한다', () => {
|
||||
const empty = findById(tabMailTemplates, 'board_mail_templates_empty');
|
||||
expect(empty).toBeDefined();
|
||||
expect(empty.if).toContain('boardMailTemplates?.data?.data');
|
||||
expect(empty.if).toContain('length === 0');
|
||||
});
|
||||
|
||||
it('검색결과 없음과 데이터 없음을 분기 표시한다', () => {
|
||||
const empty = findById(tabMailTemplates, 'board_mail_templates_empty');
|
||||
const emptyStr = JSON.stringify(empty);
|
||||
expect(emptyStr).toContain('empty_no_results');
|
||||
expect(emptyStr).toContain('empty_message');
|
||||
expect(emptyStr).toContain('query.search');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 페이지네이션
|
||||
// ========================================================================
|
||||
|
||||
describe('페이지네이션', () => {
|
||||
it('Pagination 컴포넌트가 존재한다', () => {
|
||||
const pagination = findById(tabMailTemplates, 'board_mail_templates_pagination');
|
||||
expect(pagination).toBeDefined();
|
||||
const paginationStr = JSON.stringify(pagination);
|
||||
expect(paginationStr).toContain('"name":"Pagination"');
|
||||
});
|
||||
|
||||
it('currentPage와 totalPages가 올바른 데이터 경로를 사용한다', () => {
|
||||
const pagination = findById(tabMailTemplates, 'board_mail_templates_pagination');
|
||||
const paginationStr = JSON.stringify(pagination);
|
||||
expect(paginationStr).toContain('boardMailTemplates?.data?.pagination?.current_page');
|
||||
expect(paginationStr).toContain('boardMailTemplates?.data?.pagination?.last_page');
|
||||
});
|
||||
|
||||
it('페이지 변경 시 navigate를 사용한다', () => {
|
||||
const pagination = findById(tabMailTemplates, 'board_mail_templates_pagination');
|
||||
const paginationStr = JSON.stringify(pagination);
|
||||
expect(paginationStr).toContain('"handler":"navigate"');
|
||||
expect(paginationStr).toContain('mergeQuery');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 편집 모달 (_modal_mail_template_edit.json) 구조 검증
|
||||
// ============================================================================
|
||||
|
||||
describe('_modal_mail_template_edit.json 구조 검증', () => {
|
||||
it('모달 ID가 올바르다', () => {
|
||||
expect(modalEdit.id).toBe('modal_board_mail_template_edit');
|
||||
});
|
||||
|
||||
it('Modal 컴포넌트를 사용한다', () => {
|
||||
expect(modalEdit.name).toBe('Modal');
|
||||
expect(modalEdit.type).toBe('composite');
|
||||
});
|
||||
|
||||
it('모달 크기가 xl이다', () => {
|
||||
expect(modalEdit.props.size).toBe('xl');
|
||||
});
|
||||
|
||||
it('closeOnOverlayClick이 false이다', () => {
|
||||
expect(modalEdit.props.closeOnOverlayClick).toBe(false);
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// blur_until_loaded
|
||||
// ========================================================================
|
||||
|
||||
it('폼 컨테이너에 blur_until_loaded가 적용되어 있다', () => {
|
||||
const container = findById(modalEdit, 'board_edit_form_container');
|
||||
expect(container).toBeDefined();
|
||||
expect(container.blur_until_loaded).toBeDefined();
|
||||
expect(container.blur_until_loaded).toContain('isSaving');
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// sticky footer
|
||||
// ========================================================================
|
||||
|
||||
it('footer가 sticky 스타일을 가진다', () => {
|
||||
const footer = findById(modalEdit, 'board_edit_modal_footer');
|
||||
expect(footer).toBeDefined();
|
||||
expect(footer.props.className).toContain('sticky');
|
||||
expect(footer.props.className).toContain('bottom-0');
|
||||
expect(footer.props.className).toContain('bg-white');
|
||||
expect(footer.props.className).toContain('dark:bg-gray-800');
|
||||
});
|
||||
|
||||
it('알림 유형 라벨이 읽기 전용으로 표시된다', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
expect(editStr).toContain('_local.editingTemplate?.type');
|
||||
});
|
||||
|
||||
it('한국어/영어 제목 입력 필드가 있다', () => {
|
||||
const subjectKo = findById(modalEdit, 'board_edit_subject_ko');
|
||||
const subjectEn = findById(modalEdit, 'board_edit_subject_en');
|
||||
expect(subjectKo).toBeDefined();
|
||||
expect(subjectEn).toBeDefined();
|
||||
});
|
||||
|
||||
it('한국어/영어 본문 에디터가 있다', () => {
|
||||
const bodyKo = findById(modalEdit, 'board_edit_body_ko');
|
||||
const bodyEn = findById(modalEdit, 'board_edit_body_en');
|
||||
expect(bodyKo).toBeDefined();
|
||||
expect(bodyEn).toBeDefined();
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// is_active Toggle
|
||||
// ========================================================================
|
||||
|
||||
it('is_active Toggle이 있다', () => {
|
||||
const toggle = findById(modalEdit, 'board_edit_is_active_toggle');
|
||||
expect(toggle).toBeDefined();
|
||||
const toggleStr = JSON.stringify(toggle);
|
||||
expect(toggleStr).toContain('Toggle');
|
||||
expect(toggleStr).toContain('editingTemplate?.is_active');
|
||||
expect(toggleStr).toContain('is_active_label');
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 변수 정보 조건부 표시
|
||||
// ========================================================================
|
||||
|
||||
it('변수 정보가 variables.length > 0일 때만 표시된다', () => {
|
||||
const varInfo = findById(modalEdit, 'board_edit_variables_info');
|
||||
expect(varInfo).toBeDefined();
|
||||
expect(varInfo.if).toContain('editingTemplate?.variables');
|
||||
expect(varInfo.if).toContain('length > 0');
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 저장 버튼 (함수형 IIFE body)
|
||||
// ========================================================================
|
||||
|
||||
it('저장 버튼이 PUT API를 호출한다', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
expect(editStr).toContain('/api/modules/sirsoft-board/admin/mail-templates/');
|
||||
expect(editStr).toContain('"method":"PUT"');
|
||||
});
|
||||
|
||||
it('저장 body가 함수형 IIFE 패턴을 사용한다', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
expect(editStr).toContain('(function()');
|
||||
expect(editStr).toContain('tpl.subject');
|
||||
expect(editStr).toContain('tpl.body');
|
||||
expect(editStr).toContain('tpl.is_active');
|
||||
});
|
||||
|
||||
it('저장 성공 시 closeModal + navigate를 실행한다', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
expect(editStr).toContain('"handler":"closeModal"');
|
||||
expect(editStr).toContain('"handler":"navigate"');
|
||||
expect(editStr).toContain('"mergeQuery":true');
|
||||
});
|
||||
|
||||
it('저장 전 isSaving=true, templateErrors=null 설정', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
// sequence 첫 번째 액션에서 isSaving: true 설정
|
||||
expect(editStr).toContain('"isSaving":true');
|
||||
expect(editStr).toContain('"templateErrors":null');
|
||||
});
|
||||
|
||||
it('저장 실패 시 에러를 templateErrors에 저장한다', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
expect(editStr).toContain('error.errors');
|
||||
expect(editStr).toContain('"isSaving":false');
|
||||
});
|
||||
|
||||
it('취소 버튼이 closeModal을 실행한다', () => {
|
||||
const editStr = JSON.stringify(modalEdit);
|
||||
expect(editStr).toContain('"handler":"closeModal"');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 미리보기 모달 (_modal_mail_template_preview.json) 구조 검증
|
||||
// ============================================================================
|
||||
|
||||
describe('_modal_mail_template_preview.json 구조 검증', () => {
|
||||
it('모달 ID가 올바르다', () => {
|
||||
expect(modalPreview.id).toBe('modal_board_mail_template_preview');
|
||||
});
|
||||
|
||||
it('Modal 컴포넌트를 사용한다', () => {
|
||||
expect(modalPreview.name).toBe('Modal');
|
||||
expect(modalPreview.type).toBe('composite');
|
||||
});
|
||||
|
||||
it('미리보기 제목이 표시된다', () => {
|
||||
const previewStr = JSON.stringify(modalPreview);
|
||||
expect(previewStr).toContain('preview');
|
||||
});
|
||||
});
|
||||
+1221
File diff suppressed because it is too large.
Load diff
+851
@@ -0,0 +1,851 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* @file board-index-category-filter.test.tsx
|
||||
* @description 게시판 게시글 목록 - 카테고리 필터 검색 기능 테스트
|
||||
*
|
||||
* 검증 항목:
|
||||
* 1. 카테고리 필터 표시/미표시 조건
|
||||
* 2. API params에 category 포함
|
||||
* 3. 페이지네이션에 category query 유지
|
||||
* 4. 빈 결과 조건 분기 (카테고리 필터 반영)
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { createLayoutTest, screen } from '@core/template-engine/__tests__/utils/layoutTestUtils';
|
||||
import { ComponentRegistry } from '@core/template-engine/ComponentRegistry';
|
||||
|
||||
// G7Core 전역 모킹
|
||||
(globalThis as any).window = globalThis.window || globalThis;
|
||||
(window as any).G7Core = (window as any).G7Core || {
|
||||
createChangeEvent: vi.fn((data: { checked?: boolean; name?: string; value?: any }) => ({
|
||||
target: {
|
||||
checked: data.checked ?? false,
|
||||
name: data.name ?? '',
|
||||
value: data.value ?? (data.checked ? 'on' : 'off'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
currentTarget: {
|
||||
checked: data.checked ?? false,
|
||||
name: data.name ?? '',
|
||||
value: data.value ?? (data.checked ? 'on' : 'off'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
})),
|
||||
state: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
update: vi.fn(),
|
||||
subscribe: vi.fn(() => vi.fn()),
|
||||
},
|
||||
toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
||||
modal: { open: vi.fn(), close: vi.fn() },
|
||||
events: { emit: vi.fn(), on: vi.fn(() => vi.fn()), off: vi.fn() },
|
||||
t: (key: string) => key,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 테스트용 컴포넌트 정의
|
||||
// ============================================================
|
||||
|
||||
const TestDiv: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
}> = ({ className, children, 'data-testid': testId }) => (
|
||||
<div className={className} data-testid={testId}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TestButton: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
'data-testid'?: string;
|
||||
}> = ({ className, children, text, onClick, 'data-testid': testId }) => (
|
||||
<button className={className} onClick={onClick} data-testid={testId}>
|
||||
{children || text}
|
||||
</button>
|
||||
);
|
||||
|
||||
const TestSelect: React.FC<{
|
||||
value?: string | number;
|
||||
className?: string;
|
||||
options?: Array<{ value: string | number; label: string }>;
|
||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
'data-testid'?: string;
|
||||
}> = ({ value, className, options, onChange, 'data-testid': testId }) => (
|
||||
<select
|
||||
value={value}
|
||||
className={className}
|
||||
onChange={onChange}
|
||||
data-testid={testId}
|
||||
>
|
||||
{options?.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const TestSpan: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<span className={className}>{children || text}</span>
|
||||
);
|
||||
|
||||
const TestH1: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<h1 className={className}>{children || text}</h1>
|
||||
);
|
||||
|
||||
const TestH2: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<h2 className={className}>{children || text}</h2>
|
||||
);
|
||||
|
||||
const TestP: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<p className={className}>{children || text}</p>
|
||||
);
|
||||
|
||||
const TestIcon: React.FC<{
|
||||
name?: string;
|
||||
size?: string;
|
||||
className?: string;
|
||||
}> = ({ name, size, className }) => (
|
||||
<i className={className} data-icon={name} data-size={size} />
|
||||
);
|
||||
|
||||
const TestContainer: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ className, children }) => (
|
||||
<div className={className} data-testid="container">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TestSearchBar: React.FC<{
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
showButton?: boolean;
|
||||
className?: string;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
'data-testid'?: string;
|
||||
}> = ({ name, placeholder, value, className, onSubmit, 'data-testid': testId }) => (
|
||||
<form onSubmit={onSubmit} data-testid={testId || 'search-bar'}>
|
||||
<input name={name} placeholder={placeholder} defaultValue={value} />
|
||||
</form>
|
||||
);
|
||||
|
||||
const TestPagination: React.FC<{
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
className?: string;
|
||||
onPageChange?: (page: number) => void;
|
||||
}> = ({ currentPage, totalPages, className, onPageChange }) => (
|
||||
<nav className={className} data-testid="pagination">
|
||||
<span>
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
{onPageChange && (
|
||||
<button onClick={() => onPageChange((currentPage ?? 1) + 1)}>Next</button>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// 컴포넌트 레지스트리 설정
|
||||
// ============================================================
|
||||
|
||||
function setupTestRegistry(): ComponentRegistry {
|
||||
const registry = ComponentRegistry.getInstance();
|
||||
|
||||
(registry as any).registry = {
|
||||
Fragment: { component: ({ children }: any) => <>{children}</>, metadata: { name: 'Fragment', type: 'layout' } },
|
||||
Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } },
|
||||
Button: {
|
||||
component: TestButton,
|
||||
metadata: { name: 'Button', type: 'basic' },
|
||||
},
|
||||
Select: {
|
||||
component: TestSelect,
|
||||
metadata: { name: 'Select', type: 'basic' },
|
||||
},
|
||||
Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } },
|
||||
H1: { component: TestH1, metadata: { name: 'H1', type: 'basic' } },
|
||||
H2: { component: TestH2, metadata: { name: 'H2', type: 'basic' } },
|
||||
P: { component: TestP, metadata: { name: 'P', type: 'basic' } },
|
||||
Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } },
|
||||
Container: {
|
||||
component: TestContainer,
|
||||
metadata: { name: 'Container', type: 'layout' },
|
||||
},
|
||||
SearchBar: {
|
||||
component: TestSearchBar,
|
||||
metadata: { name: 'SearchBar', type: 'composite' },
|
||||
},
|
||||
Pagination: {
|
||||
component: TestPagination,
|
||||
metadata: { name: 'Pagination', type: 'composite' },
|
||||
},
|
||||
};
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 레이아웃 JSON (카테고리 필터 관련 부분만 발췌)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 카테고리가 있는 게시판의 게시글 목록 레이아웃 (간략화)
|
||||
* 실제 index.json에서 카테고리 필터 관련 기능만 추출
|
||||
*/
|
||||
const categoryFilterLayoutJson = {
|
||||
version: '1.0.0',
|
||||
layout_name: 'board_index_category_test',
|
||||
data_sources: [
|
||||
{
|
||||
id: 'posts',
|
||||
type: 'api',
|
||||
endpoint: '/api/modules/sirsoft-board/boards/test-board/posts',
|
||||
method: 'GET',
|
||||
params: {
|
||||
page: '{{query.page ?? 1}}',
|
||||
search: "{{query.search ?? ''}}",
|
||||
category: "{{query.category ?? ''}}",
|
||||
},
|
||||
auto_fetch: true,
|
||||
auth_required: true,
|
||||
refetchOnMount: true,
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
comment: '메인 컨테이너',
|
||||
type: 'layout',
|
||||
name: 'Container',
|
||||
if: '{{posts?.data?.board}}',
|
||||
children: [
|
||||
{
|
||||
comment: '헤더',
|
||||
type: 'basic',
|
||||
name: 'H1',
|
||||
props: {
|
||||
'data-testid': 'board-title',
|
||||
},
|
||||
text: "{{posts?.data?.board?.name ?? ''}}",
|
||||
},
|
||||
{
|
||||
comment: '필터 바 (카테고리 필터 + 검색)',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
props: {
|
||||
className:
|
||||
'flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 mb-4',
|
||||
'data-testid': 'filter-bar',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
comment: '카테고리 필터 (카테고리가 있을 때만 표시)',
|
||||
type: 'basic',
|
||||
name: 'Select',
|
||||
if: '{{posts?.data?.board?.categories && posts?.data?.board?.categories.length > 0}}',
|
||||
props: {
|
||||
value: "{{query.category || 'all'}}",
|
||||
className:
|
||||
'w-full sm:w-48 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white text-sm',
|
||||
options:
|
||||
"{{[{value:'all', label:'전체 카테고리'}].concat((posts?.data?.board?.categories ?? []).map(function(c){return{value:c,label:c};}))}}",
|
||||
'data-testid': 'category-filter',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: 'change',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/{{route.slug}}',
|
||||
mergeQuery: true,
|
||||
query: {
|
||||
category:
|
||||
"{{$event.target.value === 'all' ? '' : $event.target.value}}",
|
||||
page: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '검색 바',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
props: { className: 'flex justify-end' },
|
||||
children: [
|
||||
{
|
||||
type: 'composite',
|
||||
name: 'SearchBar',
|
||||
props: {
|
||||
name: 'search',
|
||||
placeholder: '검색',
|
||||
value: "{{query.search || ''}}",
|
||||
showButton: false,
|
||||
className: 'w-full sm:w-64',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: 'submit',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/{{route.slug}}',
|
||||
mergeQuery: true,
|
||||
query: {
|
||||
search: '{{$event.target.search.value}}',
|
||||
page: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '게시글 없음 (검색/필터 아닐 때)',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
if: "{{posts?.data?.board?.slug && !query.search && !query.category && (!query.page || Number(query.page) <= 1) && posts?.data?.pagination?.total === 0}}",
|
||||
props: { 'data-testid': 'empty-no-filter' },
|
||||
children: [
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'P',
|
||||
text: '등록된 게시글이 없습니다',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '검색/카테고리 결과 없음',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
if: '{{posts?.data?.board?.slug && (query.search || query.category) && posts?.data?.pagination?.total === 0}}',
|
||||
props: { 'data-testid': 'empty-with-filter' },
|
||||
children: [
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'P',
|
||||
text: '검색 결과가 없습니다',
|
||||
},
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'Button',
|
||||
text: '검색 초기화',
|
||||
props: { 'data-testid': 'clear-filter-btn' },
|
||||
actions: [
|
||||
{
|
||||
type: 'click',
|
||||
handler: 'navigate',
|
||||
params: { path: '/board/{{route.slug}}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '페이지네이션',
|
||||
type: 'composite',
|
||||
name: 'Pagination',
|
||||
props: {
|
||||
currentPage: '{{posts?.data?.pagination?.current_page ?? 1}}',
|
||||
totalPages: '{{posts?.data?.pagination?.last_page ?? 1}}',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
event: 'onPageChange',
|
||||
type: 'change',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/{{route?.slug}}',
|
||||
mergeQuery: true,
|
||||
query: {
|
||||
page: '{{$args[0]}}',
|
||||
category: '{{query.category}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 테스트 데이터
|
||||
// ============================================================
|
||||
|
||||
/** 카테고리가 있는 게시판 + 게시글이 있는 응답 */
|
||||
const boardWithCategoriesResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 1,
|
||||
slug: 'test-board',
|
||||
name: '테스트 게시판',
|
||||
type: 'basic',
|
||||
categories: ['공지사항', '자유게시판', '질문답변'],
|
||||
show_category: true,
|
||||
},
|
||||
data: [
|
||||
{ id: 1, title: '첫 번째 글', category: '공지사항' },
|
||||
{ id: 2, title: '두 번째 글', category: '자유게시판' },
|
||||
],
|
||||
pagination: { current_page: 1, last_page: 3, total: 25 },
|
||||
permissions: { posts_write: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** 카테고리가 없는 게시판 응답 */
|
||||
const boardWithoutCategoriesResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 2,
|
||||
slug: 'no-cat-board',
|
||||
name: '카테고리 없는 게시판',
|
||||
type: 'basic',
|
||||
categories: [],
|
||||
show_category: false,
|
||||
},
|
||||
data: [{ id: 1, title: '글1' }],
|
||||
pagination: { current_page: 1, last_page: 1, total: 1 },
|
||||
permissions: { posts_write: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** 빈 결과 (필터 없이) */
|
||||
const emptyBoardResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 1,
|
||||
slug: 'test-board',
|
||||
name: '테스트 게시판',
|
||||
type: 'basic',
|
||||
categories: ['공지사항'],
|
||||
show_category: true,
|
||||
},
|
||||
data: [],
|
||||
pagination: { current_page: 1, last_page: 1, total: 0 },
|
||||
permissions: { posts_write: true },
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 테스트
|
||||
// ============================================================
|
||||
|
||||
describe('게시판 목록 - 카테고리 필터', () => {
|
||||
let registry: ComponentRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = setupTestRegistry();
|
||||
});
|
||||
|
||||
describe('카테고리 필터 표시/미표시 조건', () => {
|
||||
it('카테고리가 있는 게시판에서 카테고리 Select가 표시된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: boardWithCategoriesResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// 카테고리 필터 Select가 존재해야 함
|
||||
const categorySelect = screen.queryByTestId('category-filter');
|
||||
expect(categorySelect).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('카테고리가 없는 게시판에서 카테고리 Select가 표시되지 않는다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'no-cat-board' },
|
||||
initialData: { posts: boardWithoutCategoriesResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// 카테고리 필터 Select가 없어야 함
|
||||
const categorySelect = screen.queryByTestId('category-filter');
|
||||
expect(categorySelect).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('categories가 빈 배열인 게시판에서 카테고리 Select가 표시되지 않는다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'no-cat-board' },
|
||||
initialData: {
|
||||
posts: {
|
||||
data: {
|
||||
...boardWithoutCategoriesResponse.data,
|
||||
board: {
|
||||
...boardWithoutCategoriesResponse.data.board,
|
||||
categories: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
const categorySelect = screen.queryByTestId('category-filter');
|
||||
expect(categorySelect).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API params에 category 포함', () => {
|
||||
it('data_sources params에 category 파라미터가 포함된다', () => {
|
||||
// 레이아웃 JSON 구조 검증 (정적 분석)
|
||||
const dataSource = categoryFilterLayoutJson.data_sources[0];
|
||||
expect(dataSource.params).toHaveProperty('category');
|
||||
expect(dataSource.params.category).toBe("{{query.category ?? ''}}");
|
||||
});
|
||||
|
||||
it('query.category가 있을 때 API 요청에 category가 포함된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '공지사항' },
|
||||
initialData: { posts: boardWithCategoriesResponse },
|
||||
});
|
||||
|
||||
testUtils.mockApi('posts', {
|
||||
response: boardWithCategoriesResponse.data,
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// category Select의 value가 query.category와 일치해야 함
|
||||
const categorySelect = screen.queryByTestId('category-filter');
|
||||
expect(categorySelect).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('카테고리 필터 변경 시 네비게이션', () => {
|
||||
it('카테고리 선택 시 navigate 핸들러가 호출된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: boardWithCategoriesResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// 카테고리 변경 액션 트리거
|
||||
await testUtils.triggerAction({
|
||||
type: 'change',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/test-board',
|
||||
mergeQuery: true,
|
||||
query: {
|
||||
category: '공지사항',
|
||||
page: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// navigate가 호출되었는지 확인
|
||||
expect(testUtils.mockNavigate).toHaveBeenCalled();
|
||||
expect(testUtils.getNavigationHistory()).toContain('/board/test-board');
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('"전체 카테고리" 선택 시 category 값이 빈 문자열이어야 한다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '공지사항' },
|
||||
initialData: { posts: boardWithCategoriesResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// "전체" 선택 시 category='' 로 네비게이션
|
||||
await testUtils.triggerAction({
|
||||
type: 'change',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/test-board',
|
||||
mergeQuery: true,
|
||||
query: {
|
||||
category: '', // 'all' 선택 시 빈 문자열
|
||||
page: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(testUtils.mockNavigate).toHaveBeenCalled();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('페이지네이션에 category query 유지', () => {
|
||||
it('페이지네이션 액션 query에 category가 포함된다', () => {
|
||||
// 레이아웃 JSON 구조 검증 (정적 분석)
|
||||
const container = categoryFilterLayoutJson.components[0];
|
||||
const pagination = container.children?.find(
|
||||
(c: any) => c.name === 'Pagination'
|
||||
);
|
||||
|
||||
expect(pagination).toBeDefined();
|
||||
expect(pagination?.actions?.[0]?.params?.query).toHaveProperty(
|
||||
'category'
|
||||
);
|
||||
expect(pagination?.actions?.[0]?.params?.query?.category).toBe(
|
||||
'{{query.category}}'
|
||||
);
|
||||
});
|
||||
|
||||
it('페이지 변경 시 카테고리 필터가 유지된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '공지사항', page: '1' },
|
||||
initialData: { posts: boardWithCategoriesResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// 페이지 변경 트리거
|
||||
await testUtils.triggerAction({
|
||||
type: 'change',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/test-board',
|
||||
mergeQuery: true,
|
||||
query: {
|
||||
page: 2,
|
||||
category: '공지사항', // 카테고리 유지
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(testUtils.mockNavigate).toHaveBeenCalled();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('빈 결과 조건 분기', () => {
|
||||
it('필터 없이 게시글이 0건일 때 "게시글 없음" 메시지가 표시된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// "게시글 없음" (필터 없을 때)
|
||||
const emptyNoFilter = screen.queryByTestId('empty-no-filter');
|
||||
expect(emptyNoFilter).toBeInTheDocument();
|
||||
|
||||
// "검색 결과 없음"은 표시되지 않아야 함
|
||||
const emptyWithFilter = screen.queryByTestId('empty-with-filter');
|
||||
expect(emptyWithFilter).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('카테고리 필터 결과가 0건일 때 "검색 결과 없음" 메시지가 표시된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '공지사항' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// "검색 결과 없음" (카테고리 필터 있을 때)
|
||||
const emptyWithFilter = screen.queryByTestId('empty-with-filter');
|
||||
expect(emptyWithFilter).toBeInTheDocument();
|
||||
|
||||
// "게시글 없음"은 표시되지 않아야 함 (category 필터가 있으므로)
|
||||
const emptyNoFilter = screen.queryByTestId('empty-no-filter');
|
||||
expect(emptyNoFilter).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('검색어가 있고 결과가 0건일 때 "검색 결과 없음" 메시지가 표시된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { search: '없는글' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
const emptyWithFilter = screen.queryByTestId('empty-with-filter');
|
||||
expect(emptyWithFilter).toBeInTheDocument();
|
||||
|
||||
const emptyNoFilter = screen.queryByTestId('empty-no-filter');
|
||||
expect(emptyNoFilter).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('카테고리 + 검색 동시 필터 결과가 0건일 때 "검색 결과 없음" 메시지가 표시된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '공지사항', search: '없는글' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
const emptyWithFilter = screen.queryByTestId('empty-with-filter');
|
||||
expect(emptyWithFilter).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('초기화 버튼 클릭 시 모든 필터가 해제된다', async () => {
|
||||
const testUtils = createLayoutTest(categoryFilterLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
translations: { board: { filter: { category_all: '전체 카테고리' } } },
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '공지사항' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
// 초기화 버튼 트리거 (path만 지정 → 쿼리 초기화)
|
||||
await testUtils.triggerAction({
|
||||
type: 'click',
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/board/test-board',
|
||||
},
|
||||
});
|
||||
|
||||
expect(testUtils.mockNavigate).toHaveBeenCalled();
|
||||
// path에 query가 없으므로 필터 해제
|
||||
expect(testUtils.getNavigationHistory()).toContain('/board/test-board');
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('레이아웃 JSON 구조 정적 검증', () => {
|
||||
it('data_sources에 category 파라미터가 올바르게 정의되어 있다', () => {
|
||||
const ds = categoryFilterLayoutJson.data_sources[0];
|
||||
expect(ds.params.category).toBe("{{query.category ?? ''}}");
|
||||
});
|
||||
|
||||
it('Select 컴포넌트 타입이 basic이다', () => {
|
||||
const container = categoryFilterLayoutJson.components[0];
|
||||
const filterBar = container.children?.find(
|
||||
(c: any) => c.comment === '필터 바 (카테고리 필터 + 검색)'
|
||||
);
|
||||
const select = filterBar?.children?.find(
|
||||
(c: any) => c.name === 'Select'
|
||||
);
|
||||
|
||||
expect(select).toBeDefined();
|
||||
expect(select?.type).toBe('basic');
|
||||
});
|
||||
|
||||
it('Select의 change 액션이 navigate 핸들러를 사용한다', () => {
|
||||
const container = categoryFilterLayoutJson.components[0];
|
||||
const filterBar = container.children?.find(
|
||||
(c: any) => c.comment === '필터 바 (카테고리 필터 + 검색)'
|
||||
);
|
||||
const select = filterBar?.children?.find(
|
||||
(c: any) => c.name === 'Select'
|
||||
);
|
||||
const action = select?.actions?.[0];
|
||||
|
||||
expect(action?.type).toBe('change');
|
||||
expect(action?.handler).toBe('navigate');
|
||||
expect(action?.params?.mergeQuery).toBe(true);
|
||||
expect(action?.params?.query?.page).toBe(1);
|
||||
});
|
||||
|
||||
it('Select의 if 조건이 categories 존재 여부를 확인한다', () => {
|
||||
const container = categoryFilterLayoutJson.components[0];
|
||||
const filterBar = container.children?.find(
|
||||
(c: any) => c.comment === '필터 바 (카테고리 필터 + 검색)'
|
||||
);
|
||||
const select = filterBar?.children?.find(
|
||||
(c: any) => c.name === 'Select'
|
||||
);
|
||||
|
||||
expect(select?.if).toContain('categories');
|
||||
expect(select?.if).toContain('length > 0');
|
||||
});
|
||||
|
||||
it('빈 결과 조건에 !query.category가 포함되어 있다', () => {
|
||||
const container = categoryFilterLayoutJson.components[0];
|
||||
const emptyNoFilter = container.children?.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'empty-no-filter'
|
||||
);
|
||||
|
||||
expect(emptyNoFilter?.if).toContain('!query.category');
|
||||
});
|
||||
|
||||
it('검색 결과 없음 조건에 query.category가 포함되어 있다', () => {
|
||||
const container = categoryFilterLayoutJson.components[0];
|
||||
const emptyWithFilter = container.children?.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'empty-with-filter'
|
||||
);
|
||||
|
||||
expect(emptyWithFilter?.if).toContain('query.category');
|
||||
});
|
||||
});
|
||||
});
|
||||
+733
@@ -0,0 +1,733 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* @file board-index-empty-states.test.tsx
|
||||
* @description 게시판 목록 - 빈 상태, 로딩/오류, 글쓰기 버튼 렌더링 테스트
|
||||
*
|
||||
* 검증 항목:
|
||||
* 1. 빈 상태 3종 (빈 페이지 / 게시글 없음 / 검색 결과 없음) 조건부 렌더링
|
||||
* 2. 로딩 스켈레톤 / 오류 페이지 조건부 렌더링
|
||||
* 3. 글쓰기 버튼 권한별 분기 (allowed / no_permission / not_logged_in)
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { createLayoutTest, screen } from '@core/template-engine/__tests__/utils/layoutTestUtils';
|
||||
import { ComponentRegistry } from '@core/template-engine/ComponentRegistry';
|
||||
|
||||
// G7Core 전역 모킹
|
||||
(globalThis as any).window = globalThis.window || globalThis;
|
||||
(window as any).G7Core = (window as any).G7Core || {
|
||||
createChangeEvent: vi.fn((data: { checked?: boolean; name?: string; value?: any }) => ({
|
||||
target: {
|
||||
checked: data.checked ?? false,
|
||||
name: data.name ?? '',
|
||||
value: data.value ?? (data.checked ? 'on' : 'off'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
currentTarget: {
|
||||
checked: data.checked ?? false,
|
||||
name: data.name ?? '',
|
||||
value: data.value ?? (data.checked ? 'on' : 'off'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
})),
|
||||
state: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
update: vi.fn(),
|
||||
subscribe: vi.fn(() => vi.fn()),
|
||||
},
|
||||
toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
||||
modal: { open: vi.fn(), close: vi.fn() },
|
||||
events: { emit: vi.fn(), on: vi.fn(() => vi.fn()), off: vi.fn() },
|
||||
t: (key: string) => key,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 테스트용 컴포넌트 정의
|
||||
// ============================================================
|
||||
|
||||
const TestDiv: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
}> = ({ className, children, 'data-testid': testId }) => (
|
||||
<div className={className} data-testid={testId}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TestButton: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
type?: string;
|
||||
onClick?: () => void;
|
||||
'data-testid'?: string;
|
||||
}> = ({ className, children, text, onClick, 'data-testid': testId }) => (
|
||||
<button className={className} onClick={onClick} data-testid={testId}>
|
||||
{children || text}
|
||||
</button>
|
||||
);
|
||||
|
||||
const TestSpan: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<span className={className}>{children || text}</span>
|
||||
);
|
||||
|
||||
const TestH1: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<h1 className={className}>{children || text}</h1>
|
||||
);
|
||||
|
||||
const TestH2: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<h2 className={className}>{children || text}</h2>
|
||||
);
|
||||
|
||||
const TestP: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
text?: string;
|
||||
}> = ({ className, children, text }) => (
|
||||
<p className={className}>{children || text}</p>
|
||||
);
|
||||
|
||||
const TestIcon: React.FC<{
|
||||
name?: string;
|
||||
size?: string;
|
||||
className?: string;
|
||||
}> = ({ name, size, className }) => (
|
||||
<i className={className} data-icon={name} data-size={size} />
|
||||
);
|
||||
|
||||
const TestContainer: React.FC<{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ className, children }) => (
|
||||
<div className={className} data-testid="container">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TestSearchBar: React.FC<{
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
showButton?: boolean;
|
||||
className?: string;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
}> = ({ name, placeholder, value, className, onSubmit }) => (
|
||||
<form onSubmit={onSubmit} data-testid="search-bar">
|
||||
<input name={name} placeholder={placeholder} defaultValue={value} />
|
||||
</form>
|
||||
);
|
||||
|
||||
const TestPagination: React.FC<{
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
className?: string;
|
||||
onPageChange?: (page: number) => void;
|
||||
}> = ({ currentPage, totalPages, className }) => (
|
||||
<nav className={className} data-testid="pagination">
|
||||
<span>
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
</nav>
|
||||
);
|
||||
|
||||
const TestSelect: React.FC<{
|
||||
value?: string | number;
|
||||
className?: string;
|
||||
options?: Array<{ value: string | number; label: string }>;
|
||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
}> = ({ value, className, options, onChange }) => (
|
||||
<select value={value} className={className} onChange={onChange}>
|
||||
{options?.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// 컴포넌트 레지스트리 설정
|
||||
// ============================================================
|
||||
|
||||
function setupTestRegistry(): ComponentRegistry {
|
||||
const registry = ComponentRegistry.getInstance();
|
||||
|
||||
(registry as any).registry = {
|
||||
Fragment: { component: ({ children }: any) => <>{children}</>, metadata: { name: 'Fragment', type: 'layout' } },
|
||||
Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } },
|
||||
Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } },
|
||||
Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } },
|
||||
H1: { component: TestH1, metadata: { name: 'H1', type: 'basic' } },
|
||||
H2: { component: TestH2, metadata: { name: 'H2', type: 'basic' } },
|
||||
P: { component: TestP, metadata: { name: 'P', type: 'basic' } },
|
||||
Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } },
|
||||
Select: { component: TestSelect, metadata: { name: 'Select', type: 'basic' } },
|
||||
Container: { component: TestContainer, metadata: { name: 'Container', type: 'layout' } },
|
||||
SearchBar: { component: TestSearchBar, metadata: { name: 'SearchBar', type: 'composite' } },
|
||||
Pagination: { component: TestPagination, metadata: { name: 'Pagination', type: 'composite' } },
|
||||
};
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 레이아웃 JSON (빈 상태 + 로딩/오류 + 글쓰기 버튼 검증용)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 리팩토링된 index.json 구조의 핵심 부분을 재현
|
||||
* partial 분리된 내용을 인라인으로 포함 (테스트 편의)
|
||||
*/
|
||||
const boardIndexLayoutJson = {
|
||||
version: '1.0.0',
|
||||
layout_name: 'board_index_empty_states_test',
|
||||
data_sources: [
|
||||
{
|
||||
id: 'posts',
|
||||
type: 'api',
|
||||
endpoint: '/api/modules/sirsoft-board/boards/test-board/posts',
|
||||
method: 'GET',
|
||||
params: {
|
||||
page: '{{query.page ?? 1}}',
|
||||
search: "{{query.search ?? ''}}",
|
||||
category: "{{query.category ?? ''}}",
|
||||
},
|
||||
auto_fetch: true,
|
||||
auth_required: true,
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
comment: '로딩 스켈레톤 (오류 없을 때만)',
|
||||
type: 'layout',
|
||||
name: 'Container',
|
||||
if: '{{!posts?.data?.board && !_global.hasError}}',
|
||||
props: { 'data-testid': 'loading-skeleton' },
|
||||
children: [
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'P',
|
||||
text: '게시글을 불러오는 중입니다...',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '오류 페이지',
|
||||
type: 'layout',
|
||||
name: 'Container',
|
||||
if: '{{_global.hasError}}',
|
||||
props: { 'data-testid': 'error-page' },
|
||||
children: [
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'H2',
|
||||
props: { 'data-testid': 'error-title' },
|
||||
text: '일시적인 오류가 발생했습니다',
|
||||
},
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'Button',
|
||||
props: { 'data-testid': 'reload-btn' },
|
||||
text: '새로고침',
|
||||
actions: [{ type: 'click', handler: 'refresh' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '메인 컨테이너 (데이터 로드 완료 시)',
|
||||
type: 'layout',
|
||||
name: 'Container',
|
||||
if: '{{posts?.data?.board}}',
|
||||
children: [
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'H1',
|
||||
props: { 'data-testid': 'board-title' },
|
||||
text: "{{posts?.data?.board?.name ?? ''}}",
|
||||
},
|
||||
{
|
||||
comment: '글쓰기 버튼',
|
||||
type: 'basic',
|
||||
name: 'Button',
|
||||
props: { 'data-testid': 'write-btn' },
|
||||
actions: [
|
||||
{
|
||||
type: 'click',
|
||||
handler: 'switch',
|
||||
params: {
|
||||
value:
|
||||
"{{posts?.data?.permissions?.posts_write ? 'allowed' : (_global.currentUser?.uuid ? 'no_permission' : 'not_logged_in')}}",
|
||||
},
|
||||
cases: {
|
||||
allowed: {
|
||||
handler: 'navigate',
|
||||
params: { path: '/board/{{route.slug}}/write' },
|
||||
},
|
||||
no_permission: {
|
||||
handler: 'toast',
|
||||
params: { type: 'warning', message: '글쓰기 권한이 없습니다' },
|
||||
},
|
||||
not_logged_in: {
|
||||
handler: 'sequence',
|
||||
actions: [
|
||||
{
|
||||
handler: 'toast',
|
||||
params: { type: 'info', message: '글을 작성하려면 로그인이 필요합니다' },
|
||||
},
|
||||
{
|
||||
handler: 'navigate',
|
||||
params: {
|
||||
path: '/login',
|
||||
query: { redirect: '/board/{{route.slug}}/write' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{ type: 'basic', name: 'Icon', props: { name: 'pencil', size: 'sm' } },
|
||||
{ type: 'basic', name: 'Span', text: '글쓰기' },
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '빈 페이지 (page > 1 + 데이터 0건)',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
if: '{{posts?.data?.board?.slug && query.page && Number(query.page) > 1 && posts?.data?.data?.length === 0}}',
|
||||
props: {
|
||||
className: 'flex flex-col items-center justify-center py-16 text-center bg-gray-100/70 dark:bg-gray-800/50 rounded-xl',
|
||||
'data-testid': 'empty-page',
|
||||
},
|
||||
children: [
|
||||
{ type: 'basic', name: 'P', text: '해당 페이지에 게시글이 없습니다' },
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'Button',
|
||||
text: '첫 페이지로 이동',
|
||||
props: { 'data-testid': 'go-first-btn' },
|
||||
actions: [
|
||||
{
|
||||
type: 'click',
|
||||
handler: 'navigate',
|
||||
params: { path: '/board/{{route.slug}}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '게시글 없음 (검색/카테고리 아닐 때)',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
if: "{{posts?.data?.board?.slug && !query.search && !query.category && (!query.page || Number(query.page) <= 1) && posts?.data?.pagination?.total === 0}}",
|
||||
props: {
|
||||
className: 'flex flex-col items-center justify-center py-16 text-center bg-gray-100/70 dark:bg-gray-800/50 rounded-xl',
|
||||
'data-testid': 'empty-no-posts',
|
||||
},
|
||||
children: [
|
||||
{ type: 'basic', name: 'P', text: '등록된 게시글이 없습니다' },
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '검색 결과 없음',
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
if: '{{posts?.data?.board?.slug && (query.search || query.category) && posts?.data?.pagination?.total === 0}}',
|
||||
props: {
|
||||
className: 'flex flex-col items-center justify-center py-16 text-center bg-gray-100/70 dark:bg-gray-800/50 rounded-xl',
|
||||
'data-testid': 'empty-search',
|
||||
},
|
||||
children: [
|
||||
{ type: 'basic', name: 'P', text: '검색 결과가 없습니다' },
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'Button',
|
||||
text: '검색 초기화',
|
||||
props: { 'data-testid': 'clear-search-btn' },
|
||||
actions: [
|
||||
{
|
||||
type: 'click',
|
||||
handler: 'navigate',
|
||||
params: { path: '/board/{{route.slug}}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
comment: '페이지네이션',
|
||||
type: 'composite',
|
||||
name: 'Pagination',
|
||||
props: {
|
||||
currentPage: '{{posts?.data?.pagination?.current_page ?? 1}}',
|
||||
totalPages: '{{posts?.data?.pagination?.last_page ?? 1}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 테스트 데이터
|
||||
// ============================================================
|
||||
|
||||
/** 게시글이 있는 기본 응답 */
|
||||
const boardWithPostsResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 1,
|
||||
slug: 'test-board',
|
||||
name: '테스트 게시판',
|
||||
type: 'basic',
|
||||
categories: [],
|
||||
},
|
||||
data: [
|
||||
{ id: 1, title: '첫 번째 글' },
|
||||
{ id: 2, title: '두 번째 글' },
|
||||
],
|
||||
pagination: { current_page: 1, last_page: 2, total: 15 },
|
||||
permissions: { posts_write: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** 게시글이 없는 응답 (total=0) */
|
||||
const emptyBoardResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 1,
|
||||
slug: 'test-board',
|
||||
name: '테스트 게시판',
|
||||
type: 'basic',
|
||||
categories: [],
|
||||
},
|
||||
data: [],
|
||||
pagination: { current_page: 1, last_page: 1, total: 0 },
|
||||
permissions: { posts_write: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** page > 1이면서 데이터 0건인 응답 */
|
||||
const emptyPageResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 1,
|
||||
slug: 'test-board',
|
||||
name: '테스트 게시판',
|
||||
type: 'basic',
|
||||
categories: [],
|
||||
},
|
||||
data: [],
|
||||
pagination: { current_page: 5, last_page: 3, total: 25 },
|
||||
permissions: { posts_write: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** 글쓰기 권한 없는 응답 (로그인 상태) */
|
||||
const boardNoWritePermResponse = {
|
||||
data: {
|
||||
board: {
|
||||
id: 1,
|
||||
slug: 'test-board',
|
||||
name: '테스트 게시판',
|
||||
type: 'basic',
|
||||
categories: [],
|
||||
},
|
||||
data: [{ id: 1, title: '글1' }],
|
||||
pagination: { current_page: 1, last_page: 1, total: 1 },
|
||||
permissions: { posts_write: false },
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 테스트
|
||||
// ============================================================
|
||||
|
||||
describe('게시판 목록 - 빈 상태/로딩/오류/글쓰기 버튼', () => {
|
||||
let registry: ComponentRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = setupTestRegistry();
|
||||
});
|
||||
|
||||
describe('로딩 스켈레톤 표시 조건', () => {
|
||||
it('데이터 로드 전 + 오류 없음: 로딩 스켈레톤 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: {},
|
||||
initGlobal: { hasError: false },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('loading-skeleton')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('error-page')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('board-title')).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('데이터 로드 완료: 로딩 스켈레톤 미표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: boardWithPostsResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('loading-skeleton')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('board-title')).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('오류 페이지 표시 조건', () => {
|
||||
it('_global.hasError가 true: 오류 페이지 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: {},
|
||||
initGlobal: { hasError: true },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('error-page')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('error-title')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('reload-btn')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('loading-skeleton')).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('빈 상태 3종 조건부 렌더링', () => {
|
||||
it('게시글 없음 (검색/카테고리 없이 total=0): empty-no-posts 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('empty-no-posts')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-search')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-page')).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('검색 결과 없음 (query.search 있고 total=0): empty-search 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { search: '존재하지않는검색어' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('empty-search')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-no-posts')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('clear-search-btn')).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('카테고리 필터 결과 없음 (query.category 있고 total=0): empty-search 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { category: '존재하지않는카테고리' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('empty-search')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-no-posts')).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('빈 페이지 (page > 1 + data 0건): empty-page 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
queryParams: { page: '5' },
|
||||
initialData: { posts: emptyPageResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('empty-page')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('go-first-btn')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-no-posts')).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('게시글이 있을 때 빈 상태 모두 미표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: boardWithPostsResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('empty-no-posts')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-search')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('empty-page')).not.toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('빈 상태 배경 스타일 일관성', () => {
|
||||
it('모든 빈 상태 컴포넌트가 동일한 배경 클래스를 가진다', () => {
|
||||
const expectedBgClass = 'bg-gray-100/70 dark:bg-gray-800/50 rounded-xl';
|
||||
const mainContainer = boardIndexLayoutJson.components[2];
|
||||
const children = mainContainer.children;
|
||||
|
||||
// 빈 페이지, 게시글 없음, 검색 결과 없음 3개 컴포넌트 확인
|
||||
const emptyPageComp = children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'empty-page'
|
||||
);
|
||||
const emptyNoPostsComp = children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'empty-no-posts'
|
||||
);
|
||||
const emptySearchComp = children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'empty-search'
|
||||
);
|
||||
|
||||
expect(emptyPageComp?.props?.className).toContain(expectedBgClass);
|
||||
expect(emptyNoPostsComp?.props?.className).toContain(expectedBgClass);
|
||||
expect(emptySearchComp?.props?.className).toContain(expectedBgClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('글쓰기 버튼 switch 핸들러', () => {
|
||||
it('글쓰기 권한 있을 때 switch value가 "allowed"', () => {
|
||||
// 정적 구조 검증: switch value 표현식 확인
|
||||
const mainContainer = boardIndexLayoutJson.components[2];
|
||||
const writeBtn = mainContainer.children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'write-btn'
|
||||
);
|
||||
|
||||
expect(writeBtn).toBeDefined();
|
||||
expect(writeBtn.actions[0].handler).toBe('switch');
|
||||
expect(writeBtn.actions[0].cases).toHaveProperty('allowed');
|
||||
expect(writeBtn.actions[0].cases).toHaveProperty('no_permission');
|
||||
expect(writeBtn.actions[0].cases).toHaveProperty('not_logged_in');
|
||||
});
|
||||
|
||||
it('allowed 케이스: navigate 핸들러로 글쓰기 페이지 이동', () => {
|
||||
const mainContainer = boardIndexLayoutJson.components[2];
|
||||
const writeBtn = mainContainer.children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'write-btn'
|
||||
);
|
||||
|
||||
const allowedCase = writeBtn.actions[0].cases.allowed;
|
||||
expect(allowedCase.handler).toBe('navigate');
|
||||
expect(allowedCase.params.path).toBe('/board/{{route.slug}}/write');
|
||||
});
|
||||
|
||||
it('no_permission 케이스: toast warning 표시', () => {
|
||||
const mainContainer = boardIndexLayoutJson.components[2];
|
||||
const writeBtn = mainContainer.children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'write-btn'
|
||||
);
|
||||
|
||||
const noPermCase = writeBtn.actions[0].cases.no_permission;
|
||||
expect(noPermCase.handler).toBe('toast');
|
||||
expect(noPermCase.params.type).toBe('warning');
|
||||
});
|
||||
|
||||
it('not_logged_in 케이스: sequence로 toast + navigate (로그인 페이지)', () => {
|
||||
const mainContainer = boardIndexLayoutJson.components[2];
|
||||
const writeBtn = mainContainer.children.find(
|
||||
(c: any) => c.props?.['data-testid'] === 'write-btn'
|
||||
);
|
||||
|
||||
const notLoggedInCase = writeBtn.actions[0].cases.not_logged_in;
|
||||
expect(notLoggedInCase.handler).toBe('sequence');
|
||||
expect(notLoggedInCase.actions).toHaveLength(2);
|
||||
expect(notLoggedInCase.actions[0].handler).toBe('toast');
|
||||
expect(notLoggedInCase.actions[1].handler).toBe('navigate');
|
||||
expect(notLoggedInCase.actions[1].params.path).toBe('/login');
|
||||
expect(notLoggedInCase.actions[1].params.query.redirect).toBe(
|
||||
'/board/{{route.slug}}/write'
|
||||
);
|
||||
});
|
||||
|
||||
it('글쓰기 버튼에 pencil 아이콘과 텍스트가 있다', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: boardWithPostsResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
const writeBtn = screen.queryByTestId('write-btn');
|
||||
expect(writeBtn).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('페이지네이션 항상 표시', () => {
|
||||
it('게시글이 있을 때 페이지네이션 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: boardWithPostsResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('pagination')).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
|
||||
it('게시글이 없을 때도 페이지네이션 표시', async () => {
|
||||
const testUtils = createLayoutTest(boardIndexLayoutJson, {
|
||||
componentRegistry: registry,
|
||||
routeParams: { slug: 'test-board' },
|
||||
initialData: { posts: emptyBoardResponse },
|
||||
});
|
||||
|
||||
await testUtils.render();
|
||||
|
||||
expect(screen.queryByTestId('pagination')).toBeInTheDocument();
|
||||
|
||||
testUtils.cleanup();
|
||||
});
|
||||
});
|
||||
});
|
||||
+629
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 게시판 유형 관리 모달 레이아웃 JSON 구조 검증 테스트
|
||||
*
|
||||
* @description
|
||||
* - Partial 모달: 게시판 유형 CRUD (목록 테이블 + 추가/수정 폼)
|
||||
* - 상태 관리: _global.boardTypesList, btFormMode, btFormSlug, btFormName
|
||||
* - API: GET/POST/PUT/DELETE board-types 엔드포인트
|
||||
* - 핸들러: sequence, apiCall, setState, refetchDataSource, toast
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import modal from '../../../layouts/admin/partials/admin_board_form/_board_type_manage_modal.json';
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 ID를 가진 노드를 재귀적으로 찾습니다.
|
||||
*/
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node) return null;
|
||||
if (node.id === id) return node;
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
const found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
const found = findById(child as any, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 name을 가진 컴포넌트를 모두 찾습니다.
|
||||
*/
|
||||
function findByName(node: any, name: string): any[] {
|
||||
const results: any[] = [];
|
||||
if (!node) return results;
|
||||
|
||||
if (node.name === name) {
|
||||
results.push(node);
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findByName(child, name));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots) {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
results.push(...findByName(child as any, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 트리에서 특정 handler를 가진 액션을 모두 재귀적으로 찾습니다.
|
||||
*/
|
||||
function findActions(node: any, handlerName: string): any[] {
|
||||
const results: any[] = [];
|
||||
if (!node) return results;
|
||||
|
||||
if (node.actions && Array.isArray(node.actions)) {
|
||||
for (const action of node.actions) {
|
||||
if (action.handler === handlerName) {
|
||||
results.push(action);
|
||||
}
|
||||
if (action.actions && Array.isArray(action.actions)) {
|
||||
for (const subAction of action.actions) {
|
||||
if (subAction.handler === handlerName) {
|
||||
results.push(subAction);
|
||||
}
|
||||
if (subAction.onSuccess && Array.isArray(subAction.onSuccess)) {
|
||||
for (const cb of subAction.onSuccess) {
|
||||
if (cb.handler === handlerName) results.push(cb);
|
||||
if (cb.onSuccess && Array.isArray(cb.onSuccess)) {
|
||||
for (const nested of cb.onSuccess) {
|
||||
if (nested.handler === handlerName) results.push(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (subAction.onError && Array.isArray(subAction.onError)) {
|
||||
for (const cb of subAction.onError) {
|
||||
if (cb.handler === handlerName) results.push(cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findActions(child, handlerName));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('게시판 유형 관리 모달 레이아웃', () => {
|
||||
|
||||
describe('메타 정보', () => {
|
||||
it('Partial 메타 정보가 올바르다', () => {
|
||||
expect(modal.meta).toBeDefined();
|
||||
expect(modal.meta.is_partial).toBe(true);
|
||||
});
|
||||
|
||||
it('모달 기본 속성이 올바르다', () => {
|
||||
expect(modal.id).toBe('board_type_manage_modal');
|
||||
expect(modal.type).toBe('composite');
|
||||
expect(modal.name).toBe('Modal');
|
||||
expect(modal.props.width).toBe('640px');
|
||||
});
|
||||
|
||||
it('모달 제목이 다국어 키로 설정되어 있다', () => {
|
||||
expect(modal.props.title).toBe('$t:sirsoft-board.admin.board_types.modal_title');
|
||||
});
|
||||
|
||||
it('슬러그 사용법 안내 텍스트가 모달 상단에 있다', () => {
|
||||
const guide = findById(modal, 'bt_usage_guide');
|
||||
expect(guide).not.toBeNull();
|
||||
expect(guide.name).toBe('Div');
|
||||
// 아이콘 + 텍스트 children 구조
|
||||
const icon = findById(guide, 'bt_usage_guide_icon');
|
||||
expect(icon).not.toBeNull();
|
||||
expect(icon.name).toBe('Icon');
|
||||
expect(icon.props.name).toBe('fa-solid fa-circle-info');
|
||||
const text = findById(guide, 'bt_usage_guide_text');
|
||||
expect(text).not.toBeNull();
|
||||
expect(text.text).toBe('$t:sirsoft-board.admin.board_types.usage_guide');
|
||||
});
|
||||
});
|
||||
|
||||
describe('테이블 구조', () => {
|
||||
it('테이블 헤더에 3개 컬럼이 있다 (slug, name, actions)', () => {
|
||||
const theadRow = findById(modal, 'bt_thead_row');
|
||||
expect(theadRow).not.toBeNull();
|
||||
expect(theadRow.children).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('각 헤더 컬럼이 올바른 다국어 키를 사용한다', () => {
|
||||
const expectedColumns = [
|
||||
{ id: 'bt_th_slug', text: '$t:sirsoft-board.admin.board_types.slug' },
|
||||
{ id: 'bt_th_name', text: '$t:sirsoft-board.admin.board_types.name' },
|
||||
{ id: 'bt_th_actions', text: '$t:sirsoft-board.admin.board_types.actions' },
|
||||
];
|
||||
|
||||
for (const col of expectedColumns) {
|
||||
const th = findById(modal, col.id);
|
||||
expect(th).not.toBeNull();
|
||||
expect(th.text).toBe(col.text);
|
||||
}
|
||||
});
|
||||
|
||||
it('빈 상태 행이 존재하고 조건부 렌더링된다', () => {
|
||||
const emptyRow = findById(modal, 'bt_empty_row');
|
||||
expect(emptyRow).not.toBeNull();
|
||||
expect(emptyRow.if).toBe('{{!_global.boardTypesList || _global.boardTypesList.length === 0}}');
|
||||
|
||||
const emptyCell = findById(modal, 'bt_empty_cell');
|
||||
expect(emptyCell.props.colSpan).toBe(3);
|
||||
});
|
||||
|
||||
it('데이터 행에 iteration이 올바르게 설정되어 있다', () => {
|
||||
const dataRow = findById(modal, 'bt_row');
|
||||
expect(dataRow).not.toBeNull();
|
||||
expect(dataRow.if).toBe('{{_global.boardTypesList && _global.boardTypesList.length > 0}}');
|
||||
expect(dataRow.iteration).toEqual({
|
||||
source: '{{_global.boardTypesList ?? []}}',
|
||||
item_var: 'btItem',
|
||||
index_var: 'btIdx',
|
||||
});
|
||||
});
|
||||
|
||||
it('데이터 행에 3개 셀이 있다 (slug, name, actions)', () => {
|
||||
const dataRow = findById(modal, 'bt_row');
|
||||
expect(dataRow.children).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('테이블 행 데이터 바인딩', () => {
|
||||
it('slug 셀이 btItem.slug을 표시한다', () => {
|
||||
const slugCell = findById(modal, 'bt_td_slug');
|
||||
expect(slugCell.text).toBe('{{btItem.slug}}');
|
||||
});
|
||||
|
||||
it('name 셀이 다국어 fallback 체인으로 표시한다', () => {
|
||||
const nameCell = findById(modal, 'bt_td_name');
|
||||
expect(nameCell.text).toContain('btItem.name');
|
||||
expect(nameCell.text).toContain('_global.locale');
|
||||
});
|
||||
});
|
||||
|
||||
describe('액션 버튼', () => {
|
||||
it('수정 버튼이 전역 상태를 폼에 로드한다', () => {
|
||||
const editBtn = findById(modal, 'bt_edit_btn');
|
||||
expect(editBtn).not.toBeNull();
|
||||
|
||||
const editAction = editBtn.actions[0];
|
||||
expect(editAction.handler).toBe('setState');
|
||||
expect(editAction.params.target).toBe('global');
|
||||
expect(editAction.params.btFormMode).toBe('edit');
|
||||
expect(editAction.params.btFormSlug).toBe('{{btItem.slug}}');
|
||||
// btFormName은 객체로 설정
|
||||
expect(editAction.params.btFormName).toContain('btItem.name');
|
||||
});
|
||||
|
||||
it('삭제 버튼이 sequence로 API 호출 후 목록 갱신한다', () => {
|
||||
const deleteBtn = findById(modal, 'bt_delete_btn');
|
||||
expect(deleteBtn).not.toBeNull();
|
||||
|
||||
const clickAction = deleteBtn.actions[0];
|
||||
expect(clickAction.handler).toBe('sequence');
|
||||
expect(clickAction.actions).toHaveLength(2);
|
||||
|
||||
expect(clickAction.actions[0].handler).toBe('setState');
|
||||
expect(clickAction.actions[0].params.btDeletingId).toBe('{{btItem.id}}');
|
||||
|
||||
const apiCall = clickAction.actions[1];
|
||||
expect(apiCall.handler).toBe('apiCall');
|
||||
expect(apiCall.params.method).toBe('DELETE');
|
||||
expect(apiCall.target).toContain('/api/modules/sirsoft-board/admin/board-types/');
|
||||
});
|
||||
|
||||
it('삭제 API 호출에 confirm 확인 메시지가 설정되어 있다', () => {
|
||||
const deleteBtn = findById(modal, 'bt_delete_btn');
|
||||
const apiCall = deleteBtn.actions[0].actions[1];
|
||||
expect(apiCall.confirm).toBe('$t:sirsoft-board.admin.board_types.delete_confirm');
|
||||
});
|
||||
|
||||
it('삭제 아이콘과 스피너가 조건부 렌더링된다', () => {
|
||||
const deleteIcon = findById(modal, 'bt_delete_icon');
|
||||
const deleteSpinner = findById(modal, 'bt_delete_spinner');
|
||||
|
||||
expect(deleteIcon.if).toBe('{{_global.btDeletingId !== btItem.id}}');
|
||||
expect(deleteSpinner.if).toBe('{{_global.btDeletingId === btItem.id}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('폼 섹션', () => {
|
||||
it('폼에 2개 필드가 있다 (slug, name)', () => {
|
||||
const formFields = findById(modal, 'bt_form_fields');
|
||||
expect(formFields).not.toBeNull();
|
||||
expect(formFields.children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('slug Input이 올바른 전역 상태 바인딩을 사용한다', () => {
|
||||
const slugInput = findById(modal, 'bt_input_slug');
|
||||
expect(slugInput).not.toBeNull();
|
||||
expect(slugInput.props.value).toBe("{{_global.btFormSlug ?? ''}}");
|
||||
});
|
||||
|
||||
it('name 필드가 MultilingualInput 컴포넌트를 사용한다', () => {
|
||||
const nameInput = findById(modal, 'bt_input_name');
|
||||
expect(nameInput).not.toBeNull();
|
||||
expect(nameInput.name).toBe('MultilingualInput');
|
||||
expect(nameInput.type).toBe('composite');
|
||||
expect(nameInput.props.layout).toBe('tabs');
|
||||
expect(nameInput.props.required).toBe(true);
|
||||
});
|
||||
|
||||
it('MultilingualInput이 btFormName 상태에 바인딩된다', () => {
|
||||
const nameInput = findById(modal, 'bt_input_name');
|
||||
expect(nameInput.props.value).toContain('_global.btFormName');
|
||||
|
||||
const onChangeAction = nameInput.actions[0];
|
||||
expect(onChangeAction.event).toBe('onChange');
|
||||
expect(onChangeAction.handler).toBe('setState');
|
||||
expect(onChangeAction.params.target).toBe('global');
|
||||
expect(onChangeAction.params.btFormName).toContain('$args[0]?.target?.value');
|
||||
});
|
||||
|
||||
it('slug Input의 setState 핸들러가 btFormSlug를 설정한다', () => {
|
||||
const slugInput = findById(modal, 'bt_input_slug');
|
||||
const setStateAction = slugInput.actions[0];
|
||||
expect(setStateAction.handler).toBe('setState');
|
||||
expect(setStateAction.params.target).toBe('global');
|
||||
expect(setStateAction.params.btFormSlug).toBe('{{$event.target.value}}');
|
||||
});
|
||||
|
||||
it('slug 필드가 수정 모드에서 비활성화된다', () => {
|
||||
const slugInput = findById(modal, 'bt_input_slug');
|
||||
expect(slugInput.props.disabled).toBe("{{_computed.isReadOnly || _global.btFormMode === 'edit'}}");
|
||||
});
|
||||
});
|
||||
|
||||
describe('폼 헤더 및 모드 전환', () => {
|
||||
it('폼 제목이 add/edit 모드에 따라 변경된다', () => {
|
||||
const title = findById(modal, 'bt_form_title');
|
||||
expect(title.text).toContain('btFormMode');
|
||||
expect(title.text).toContain('edit_title');
|
||||
expect(title.text).toContain('add_title');
|
||||
});
|
||||
|
||||
it('취소 버튼이 수정 모드에서만 표시된다', () => {
|
||||
const cancelBtn = findById(modal, 'bt_form_cancel_btn');
|
||||
expect(cancelBtn.if).toBe("{{_global.btFormMode === 'edit'}}");
|
||||
});
|
||||
|
||||
it('취소 버튼에 다크모드 포함 스타일이 적용된다', () => {
|
||||
const cancelBtn = findById(modal, 'bt_form_cancel_btn');
|
||||
expect(cancelBtn.props.className).toContain('text-gray-600');
|
||||
expect(cancelBtn.props.className).toContain('dark:text-gray-300');
|
||||
expect(cancelBtn.props.className).toContain('dark:bg-gray-700');
|
||||
expect(cancelBtn.props.className).toContain('text-xs');
|
||||
});
|
||||
|
||||
it('취소 버튼 클릭 시 폼을 초기화한다', () => {
|
||||
const cancelBtn = findById(modal, 'bt_form_cancel_btn');
|
||||
const action = cancelBtn.actions[0];
|
||||
expect(action.handler).toBe('setState');
|
||||
expect(action.params.btFormMode).toBe('add');
|
||||
expect(action.params.btEditingItem).toBeNull();
|
||||
expect(action.params.btFormSlug).toBe('');
|
||||
expect(action.params.btFormName).toEqual({ ko: '', en: '' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('제출 버튼', () => {
|
||||
it('제출 버튼이 필수 필드 검증으로 비활성화 조건을 가진다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
expect(submitBtn.props.disabled).toContain('_global.btSaving');
|
||||
expect(submitBtn.props.disabled).toContain('_global.btFormSlug');
|
||||
expect(submitBtn.props.disabled).toContain('_global.btFormName');
|
||||
});
|
||||
|
||||
it('제출 시 모드에 따라 POST 또는 PUT을 호출한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const clickAction = submitBtn.actions[0];
|
||||
expect(clickAction.handler).toBe('sequence');
|
||||
|
||||
const apiCall = clickAction.actions[1];
|
||||
expect(apiCall.handler).toBe('apiCall');
|
||||
expect(apiCall.params.method).toContain('btFormMode');
|
||||
expect(apiCall.params.method).toContain('PUT');
|
||||
expect(apiCall.params.method).toContain('POST');
|
||||
});
|
||||
|
||||
it('제출 요청 body에 slug과 name 필드가 포함된다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const apiCall = submitBtn.actions[0].actions[1];
|
||||
const body = apiCall.params.body;
|
||||
|
||||
expect(body.slug).toBe('{{_global.btFormSlug}}');
|
||||
expect(body.name).toContain('_global.btFormName');
|
||||
});
|
||||
|
||||
it('제출 성공 시 폼을 초기화하고 목록을 갱신한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const apiCall = submitBtn.actions[0].actions[1];
|
||||
const onSuccess = apiCall.onSuccess;
|
||||
|
||||
const resetAction = onSuccess[0];
|
||||
expect(resetAction.handler).toBe('setState');
|
||||
expect(resetAction.params.btSaving).toBe(false);
|
||||
expect(resetAction.params.btFormMode).toBe('add');
|
||||
expect(resetAction.params.btFormSlug).toBe('');
|
||||
expect(resetAction.params.btFormName).toEqual({ ko: '', en: '' });
|
||||
|
||||
const refetchCall = onSuccess[1];
|
||||
expect(refetchCall.handler).toBe('apiCall');
|
||||
expect(refetchCall.target).toBe('/api/modules/sirsoft-board/admin/board-types');
|
||||
expect(refetchCall.params.method).toBe('GET');
|
||||
});
|
||||
|
||||
it('제출 스피너가 저장 중에만 표시된다', () => {
|
||||
const spinner = findById(modal, 'bt_submit_spinner');
|
||||
expect(spinner.if).toBe('{{_global.btSaving}}');
|
||||
});
|
||||
|
||||
it('제출 버튼 텍스트가 모드와 저장 상태에 따라 변경된다', () => {
|
||||
const text = findById(modal, 'bt_submit_text');
|
||||
expect(text.text).toContain('btFormMode');
|
||||
expect(text.text).toContain('btSaving');
|
||||
});
|
||||
});
|
||||
|
||||
describe('API 엔드포인트', () => {
|
||||
it('모든 API 호출이 올바른 base URL을 사용한다', () => {
|
||||
const allApiCalls = findActions(modal, 'apiCall');
|
||||
const basePath = '/api/modules/sirsoft-board/admin/board-types';
|
||||
|
||||
for (const apiCall of allApiCalls) {
|
||||
expect(apiCall.target).toContain(basePath);
|
||||
}
|
||||
});
|
||||
|
||||
it('모든 API 호출에 auth_required가 설정되어 있다', () => {
|
||||
const allApiCalls = findActions(modal, 'apiCall');
|
||||
for (const apiCall of allApiCalls) {
|
||||
expect(apiCall.auth_required).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('데이터소스 갱신', () => {
|
||||
it('성공 콜백에서 boards와 board_types 데이터소스를 갱신한다', () => {
|
||||
const refetchActions = findActions(modal, 'refetchDataSource');
|
||||
const dataSourceIds = refetchActions.map((a: any) => a.params.dataSourceId);
|
||||
|
||||
expect(dataSourceIds).toContain('boards');
|
||||
expect(dataSourceIds).toContain('board_types');
|
||||
});
|
||||
});
|
||||
|
||||
describe('에러 처리', () => {
|
||||
it('제출 API 호출의 onError에서 btSaving을 false로 설정한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const seq = submitBtn.actions[0];
|
||||
const apiCall = seq.actions.find((a: any) => a.handler === 'apiCall');
|
||||
expect(apiCall).not.toBeNull();
|
||||
expect(apiCall.onError).toBeDefined();
|
||||
|
||||
const resetState = apiCall.onError.find((a: any) => a.handler === 'setState');
|
||||
expect(resetState.params.btSaving).toBe(false);
|
||||
});
|
||||
|
||||
it('삭제 API 호출의 onError에서 btDeletingId를 null로 설정한다', () => {
|
||||
const deleteBtn = findById(modal, 'bt_delete_btn');
|
||||
const seq = deleteBtn.actions[0];
|
||||
const apiCall = seq.actions.find((a: any) => a.handler === 'apiCall');
|
||||
expect(apiCall).not.toBeNull();
|
||||
expect(apiCall.onError).toBeDefined();
|
||||
|
||||
const resetState = apiCall.onError.find((a: any) => a.handler === 'setState');
|
||||
expect(resetState.params.btDeletingId).toBeNull();
|
||||
// 삭제는 btSaving을 사용하지 않음 (저장 버튼 로딩과 분리)
|
||||
expect(resetState.params.btSaving).toBeUndefined();
|
||||
});
|
||||
|
||||
it('에러 시 toast로 오류 메시지를 표시한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const apiCall = submitBtn.actions[0].actions[1];
|
||||
const errorToast = apiCall.onError.find((a: any) => a.handler === 'toast');
|
||||
|
||||
expect(errorToast.params.type).toBe('error');
|
||||
expect(errorToast.params.message).toBe('{{error.errors ? Object.values(error.errors).flat()[0] : error.message}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('모달 닫기', () => {
|
||||
it('닫기 버튼이 존재하고 sequence로 폼 초기화 후 모달을 닫는다', () => {
|
||||
const closeBtn = findById(modal, 'bt_modal_close_btn');
|
||||
expect(closeBtn).not.toBeNull();
|
||||
|
||||
const seq = closeBtn.actions[0];
|
||||
expect(seq.handler).toBe('sequence');
|
||||
expect(seq.actions).toHaveLength(2);
|
||||
|
||||
expect(seq.actions[0].handler).toBe('setState');
|
||||
expect(seq.actions[0].params.btFormMode).toBe('add');
|
||||
expect(seq.actions[0].params.btFormName).toEqual({ ko: '', en: '' });
|
||||
|
||||
expect(seq.actions[1].handler).toBe('closeModal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('다크 모드', () => {
|
||||
it('테이블 본문에 다크 모드 클래스가 있다', () => {
|
||||
const tbody = findById(modal, 'bt_tbody');
|
||||
expect(tbody.props.className).toContain('dark:bg-gray-800');
|
||||
});
|
||||
|
||||
it('헤더 셀에 다크 모드 텍스트 색상이 있다', () => {
|
||||
const thSlug = findById(modal, 'bt_th_slug');
|
||||
expect(thSlug.props.className).toContain('dark:text-gray-400');
|
||||
});
|
||||
|
||||
it('데이터 행에 다크 모드 호버 효과가 있다', () => {
|
||||
const row = findById(modal, 'bt_row');
|
||||
expect(row.props.className).toContain('dark:hover:bg-gray-700');
|
||||
});
|
||||
|
||||
it('폼 구분선에 다크 모드 색상이 있다', () => {
|
||||
const formSection = findById(modal, 'bt_form_section');
|
||||
expect(formSection.props.className).toContain('dark:border-gray-700');
|
||||
});
|
||||
});
|
||||
|
||||
describe('모달 UI/UX 개선', () => {
|
||||
it('수정 중인 행에 classMap으로 하이라이트 배경이 적용된다', () => {
|
||||
const row = findById(modal, 'bt_row');
|
||||
expect(row.classMap).toBeDefined();
|
||||
expect(row.classMap.key).toContain('btEditingItem');
|
||||
expect(row.classMap.variants.true).toContain('bg-blue-50');
|
||||
expect(row.classMap.variants.true).toContain('dark:bg-blue-900');
|
||||
});
|
||||
|
||||
it('수정 모드 진입 시 폼 영역에 ring 효과가 적용된다', () => {
|
||||
const formSection = findById(modal, 'bt_form_section');
|
||||
expect(formSection.classMap).toBeDefined();
|
||||
expect(formSection.classMap.key).toBe('{{_global.btFormMode}}');
|
||||
expect(formSection.classMap.variants.edit).toContain('ring-2');
|
||||
expect(formSection.classMap.variants.edit).toContain('ring-blue-300');
|
||||
expect(formSection.classMap.variants.add).toBe('');
|
||||
});
|
||||
|
||||
it('slug 필드에 허용 형식 힌트가 있다 (에러 없을 때만 표시)', () => {
|
||||
const hint = findById(modal, 'bt_slug_hint');
|
||||
expect(hint).not.toBeNull();
|
||||
expect(hint.name).toBe('P');
|
||||
expect(hint.text).toBe('$t:sirsoft-board.admin.board_types.slug_hint');
|
||||
expect(hint.if).toContain("btFormMode !== 'edit'");
|
||||
expect(hint.if).toContain('btFormErrors');
|
||||
});
|
||||
|
||||
it('Modal에 width prop이 설정되어 있다', () => {
|
||||
expect(modal.props.width).toBe('640px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('폼 유효성 에러 표시', () => {
|
||||
it('slug Input에 에러 시 빨간 테두리를 위한 classMap이 있다', () => {
|
||||
const slugInput = findById(modal, 'bt_input_slug');
|
||||
expect(slugInput.classMap).toBeDefined();
|
||||
expect(slugInput.classMap.key).toContain('btFormErrors?.slug');
|
||||
expect(slugInput.classMap.variants.error).toContain('border-red-500');
|
||||
expect(slugInput.classMap.variants.error).toContain('dark:border-red-400');
|
||||
expect(slugInput.classMap.variants.normal).toContain('border-gray-300');
|
||||
});
|
||||
|
||||
it('slug 에러 메시지 요소가 존재하고 조건부 렌더링된다', () => {
|
||||
const slugError = findById(modal, 'bt_slug_error');
|
||||
expect(slugError).not.toBeNull();
|
||||
expect(slugError.name).toBe('P');
|
||||
expect(slugError.if).toContain('btFormErrors?.slug');
|
||||
expect(slugError.text).toContain('btFormErrors?.slug?.[0]');
|
||||
expect(slugError.props.className).toContain('text-red-500');
|
||||
expect(slugError.props.className).toContain('dark:text-red-400');
|
||||
});
|
||||
|
||||
it('name 에러 메시지 요소가 존재하고 조건부 렌더링된다', () => {
|
||||
const nameError = findById(modal, 'bt_name_error');
|
||||
expect(nameError).not.toBeNull();
|
||||
expect(nameError.name).toBe('P');
|
||||
expect(nameError.if).toContain("btFormErrors?.['name.ko']");
|
||||
expect(nameError.text).toContain("btFormErrors?.['name.ko']?.[0]");
|
||||
expect(nameError.props.className).toContain('text-red-500');
|
||||
});
|
||||
|
||||
it('MultilingualInput에 error prop이 전달된다', () => {
|
||||
const nameInput = findById(modal, 'bt_input_name');
|
||||
expect(nameInput.props.error).toBeDefined();
|
||||
expect(nameInput.props.error).toContain("btFormErrors?.['name.ko']");
|
||||
});
|
||||
|
||||
it('slug Input 변경 시 slug 에러를 클리어한다', () => {
|
||||
const slugInput = findById(modal, 'bt_input_slug');
|
||||
const inputAction = slugInput.actions[0];
|
||||
expect(inputAction.params.btFormErrors).toBeDefined();
|
||||
expect(inputAction.params.btFormErrors).toContain('delete e.slug');
|
||||
});
|
||||
|
||||
it('name Input 변경 시 name 에러를 클리어한다', () => {
|
||||
const nameInput = findById(modal, 'bt_input_name');
|
||||
const onChangeAction = nameInput.actions[0];
|
||||
expect(onChangeAction.params.btFormErrors).toBeDefined();
|
||||
expect(onChangeAction.params.btFormErrors).toContain("delete e['name.ko']");
|
||||
});
|
||||
|
||||
it('제출 시 에러 상태를 초기화한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const seq = submitBtn.actions[0];
|
||||
const initState = seq.actions[0];
|
||||
expect(initState.params.btFormErrors).toBeNull();
|
||||
});
|
||||
|
||||
it('제출 onError에서 btFormErrors에 에러를 저장한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const apiCall = submitBtn.actions[0].actions[1];
|
||||
const errorState = apiCall.onError.find((a: any) => a.handler === 'setState');
|
||||
expect(errorState.params.btFormErrors).toBe('{{error.errors ?? null}}');
|
||||
});
|
||||
|
||||
it('취소 버튼 클릭 시 에러를 초기화한다', () => {
|
||||
const cancelBtn = findById(modal, 'bt_form_cancel_btn');
|
||||
const action = cancelBtn.actions[0];
|
||||
expect(action.params.btFormErrors).toBeNull();
|
||||
});
|
||||
|
||||
it('모달 닫기 시 에러를 초기화한다', () => {
|
||||
const closeBtn = findById(modal, 'bt_modal_close_btn');
|
||||
const seq = closeBtn.actions[0];
|
||||
const resetAction = seq.actions[0];
|
||||
expect(resetAction.params.btFormErrors).toBeNull();
|
||||
});
|
||||
|
||||
it('수정 버튼 클릭 시 에러를 초기화한다', () => {
|
||||
const editBtn = findById(modal, 'bt_edit_btn');
|
||||
const action = editBtn.actions[0];
|
||||
expect(action.params.btFormErrors).toBeNull();
|
||||
});
|
||||
|
||||
it('제출 성공 시 에러를 초기화한다', () => {
|
||||
const submitBtn = findById(modal, 'bt_submit_btn');
|
||||
const apiCall = submitBtn.actions[0].actions[1];
|
||||
const successState = apiCall.onSuccess[0];
|
||||
expect(successState.params.btFormErrors).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button type 속성', () => {
|
||||
it('모든 Button에 type="button"이 명시되어 있다 (submit 방지)', () => {
|
||||
const buttons = findByName(modal, 'Button');
|
||||
for (const btn of buttons) {
|
||||
expect(btn.props.type).toBe('button');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 게시판 사용자 레이아웃 abilities 키 마이그레이션 검증 테스트
|
||||
*
|
||||
* @description
|
||||
* - permissions -> abilities 키 마이그레이션 완료 검증
|
||||
* - user_permissions -> user_abilities 키 마이그레이션 완료 검증
|
||||
* - 모든 board 관련 레이아웃 JSON에서 구 키(permissions, user_permissions) 잔존 여부 확인
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// === Board Index 관련 ===
|
||||
import writeButton from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/index/_write_button.json';
|
||||
|
||||
// === Board Show 관련 ===
|
||||
import commentInput from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/show/_comment_input.json';
|
||||
import commentSection from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/show/_comment_section.json';
|
||||
import commentItem from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/show/_comment_item.json';
|
||||
import postAttachments from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/show/_post_attachments.json';
|
||||
import basicShow from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/types/basic/show.json';
|
||||
|
||||
// === Board Form 관련 ===
|
||||
import postForm from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/form/_post_form.json';
|
||||
import typeRenderer from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/form/_type_renderer.json';
|
||||
import passwordVerifyModal from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/form/_password_verify_modal.json';
|
||||
|
||||
// === Shop QnA (게시판 abilities 사용) ===
|
||||
import tabQna from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_tab_qna.json';
|
||||
|
||||
// === Board Index 타입별 ===
|
||||
import galleryIndex from '../../../../../../../templates/_bundled/sirsoft-basic/layouts/partials/board/types/gallery/index.json';
|
||||
|
||||
/**
|
||||
* JSON 노드를 재귀적으로 순회하며 모든 문자열 값에서 {{...}} 표현식을 추출합니다.
|
||||
*
|
||||
* @param node JSON 노드 (객체, 배열, 또는 프리미티브)
|
||||
* @param expressions 수집된 표현식 배열 (참조로 전달)
|
||||
* @param path 현재 노드 경로 (디버깅용)
|
||||
*/
|
||||
function findExpressionsInJson(
|
||||
node: unknown,
|
||||
expressions: Array<{ expression: string; path: string }> = [],
|
||||
path: string = '$'
|
||||
): Array<{ expression: string; path: string }> {
|
||||
if (node === null || node === undefined) return expressions;
|
||||
|
||||
if (typeof node === 'string') {
|
||||
// {{...}} 패턴 매칭 (중첩 가능하므로 전체 문자열에서 모두 추출)
|
||||
const matches = node.match(/\{\{[^}]*\}\}/g);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
expressions.push({ expression: match, path });
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((item, index) => {
|
||||
findExpressionsInJson(item, expressions, `${path}[${index}]`);
|
||||
});
|
||||
return expressions;
|
||||
}
|
||||
|
||||
if (typeof node === 'object') {
|
||||
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
||||
findExpressionsInJson(value, expressions, `${path}.${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
return expressions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 표현식 목록에서 특정 패턴을 포함하는 표현식을 필터링합니다.
|
||||
*/
|
||||
function findExpressionsContaining(
|
||||
expressions: Array<{ expression: string; path: string }>,
|
||||
pattern: string | RegExp
|
||||
): Array<{ expression: string; path: string }> {
|
||||
return expressions.filter(({ expression }) => {
|
||||
if (typeof pattern === 'string') {
|
||||
return expression.includes(pattern);
|
||||
}
|
||||
return pattern.test(expression);
|
||||
});
|
||||
}
|
||||
|
||||
// 모든 레이아웃 파일 목록 (파일명과 JSON 데이터 매핑)
|
||||
const allBoardLayouts: Array<{ name: string; json: unknown }> = [
|
||||
{ name: 'index/_write_button.json', json: writeButton },
|
||||
{ name: 'show/_comment_input.json', json: commentInput },
|
||||
{ name: 'show/_comment_section.json', json: commentSection },
|
||||
{ name: 'show/_comment_item.json', json: commentItem },
|
||||
{ name: 'show/_post_attachments.json', json: postAttachments },
|
||||
{ name: 'types/basic/show.json', json: basicShow },
|
||||
{ name: 'form/_post_form.json', json: postForm },
|
||||
{ name: 'form/_type_renderer.json', json: typeRenderer },
|
||||
{ name: 'form/_password_verify_modal.json', json: passwordVerifyModal },
|
||||
{ name: 'shop/detail/_tab_qna.json', json: tabQna },
|
||||
{ name: 'types/gallery/index.json', json: galleryIndex },
|
||||
];
|
||||
|
||||
// 폼 관련 레이아웃 (user_permissions -> user_abilities 확인 대상)
|
||||
const formRelatedLayouts: Array<{ name: string; json: unknown }> = [
|
||||
{ name: 'form/_post_form.json', json: postForm },
|
||||
{ name: 'form/_type_renderer.json', json: typeRenderer },
|
||||
{ name: 'form/_password_verify_modal.json', json: passwordVerifyModal },
|
||||
];
|
||||
|
||||
describe('게시판 사용자 레이아웃 abilities 키 마이그레이션 검증', () => {
|
||||
describe('permissions -> abilities 키 잔존 검사 (전체 레이아웃)', () => {
|
||||
it.each(allBoardLayouts)(
|
||||
'$name: permissions 키가 남아있지 않아야 합니다',
|
||||
({ json }) => {
|
||||
const expressions = findExpressionsInJson(json);
|
||||
// "permissions" 단어를 포함하지만 "abilities"의 일부가 아닌 표현식 검색
|
||||
// 예: "permissions?.can_write" (구 키) vs "abilities?.can_write" (신 키)
|
||||
const oldKeyExpressions = findExpressionsContaining(
|
||||
expressions,
|
||||
/(?<!\w)permissions(?!\w)/
|
||||
);
|
||||
|
||||
expect(
|
||||
oldKeyExpressions,
|
||||
`구 키 "permissions"가 다음 위치에 남아있습니다:\n${oldKeyExpressions.map((e) => ` - ${e.path}: ${e.expression}`).join('\n')}`
|
||||
).toHaveLength(0);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('user_permissions -> user_abilities 키 잔존 검사 (폼 관련 레이아웃)', () => {
|
||||
it.each(formRelatedLayouts)(
|
||||
'$name: user_permissions 키가 남아있지 않아야 합니다',
|
||||
({ json }) => {
|
||||
const expressions = findExpressionsInJson(json);
|
||||
const oldKeyExpressions = findExpressionsContaining(
|
||||
expressions,
|
||||
'user_permissions'
|
||||
);
|
||||
|
||||
expect(
|
||||
oldKeyExpressions,
|
||||
`구 키 "user_permissions"가 다음 위치에 남아있습니다:\n${oldKeyExpressions.map((e) => ` - ${e.path}: ${e.expression}`).join('\n')}`
|
||||
).toHaveLength(0);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('abilities 키 사용 확인 (양성 검증)', () => {
|
||||
it('_write_button.json: posts?.data?.abilities?.can_write 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(writeButton);
|
||||
const abilitiesExpressions = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_write'
|
||||
);
|
||||
expect(abilitiesExpressions.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('_write_button.json: disabled prop에 abilities?.can_write 체크 포함', () => {
|
||||
const button = writeButton as Record<string, unknown>;
|
||||
const props = button.props as Record<string, unknown>;
|
||||
expect(props.disabled).toBeDefined();
|
||||
expect(String(props.disabled)).toContain('abilities?.can_write');
|
||||
});
|
||||
|
||||
it('_comment_input.json: post?.data?.abilities?.can_write_comments 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(commentInput);
|
||||
const abilitiesExpressions = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_write_comments'
|
||||
);
|
||||
expect(abilitiesExpressions.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('_comment_section.json: abilities?.can_read_comments 및 abilities?.can_write_comments 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(commentSection);
|
||||
|
||||
const canReadComments = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_read_comments'
|
||||
);
|
||||
const canWriteComments = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_write_comments'
|
||||
);
|
||||
|
||||
expect(canReadComments.length).toBeGreaterThanOrEqual(1);
|
||||
expect(canWriteComments.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('_comment_item.json: abilities?.can_write_comments 및 abilities?.can_manage 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(commentItem);
|
||||
|
||||
const canWriteComments = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_write_comments'
|
||||
);
|
||||
const canManage = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_manage'
|
||||
);
|
||||
|
||||
expect(canWriteComments.length).toBeGreaterThanOrEqual(1);
|
||||
expect(canManage.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('types/basic/show.json: abilities?.can_write 및 abilities?.can_manage 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(basicShow);
|
||||
|
||||
const canWrite = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_write'
|
||||
);
|
||||
const canManage = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_manage'
|
||||
);
|
||||
|
||||
expect(canWrite.length).toBeGreaterThanOrEqual(1);
|
||||
expect(canManage.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('types/basic/show.json: abilities?.can_read_comments 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(basicShow);
|
||||
const canReadComments = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_read_comments'
|
||||
);
|
||||
expect(canReadComments.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('form/_post_form.json: user_abilities?.can_manage 및 user_abilities?.can_upload 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(postForm);
|
||||
|
||||
const canManage = findExpressionsContaining(
|
||||
expressions,
|
||||
'user_abilities?.can_manage'
|
||||
);
|
||||
const canUpload = findExpressionsContaining(
|
||||
expressions,
|
||||
'user_abilities?.can_upload'
|
||||
);
|
||||
|
||||
expect(canManage.length).toBeGreaterThanOrEqual(1);
|
||||
expect(canUpload.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('form/_type_renderer.json: user_abilities?.can_manage 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(typeRenderer);
|
||||
const canManage = findExpressionsContaining(
|
||||
expressions,
|
||||
'user_abilities?.can_manage'
|
||||
);
|
||||
expect(canManage.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('form/_password_verify_modal.json: user_abilities?.can_manage 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(passwordVerifyModal);
|
||||
const canManage = findExpressionsContaining(
|
||||
expressions,
|
||||
'user_abilities?.can_manage'
|
||||
);
|
||||
expect(canManage.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('shop/detail/_tab_qna.json: abilities?.can_write 표현식 포함', () => {
|
||||
const expressions = findExpressionsInJson(tabQna);
|
||||
const canWrite = findExpressionsContaining(
|
||||
expressions,
|
||||
'abilities?.can_write'
|
||||
);
|
||||
expect(canWrite.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findExpressionsInJson 헬퍼 정합성 검증', () => {
|
||||
it('중첩 JSON에서 모든 표현식을 추출합니다', () => {
|
||||
const testNode = {
|
||||
if: '{{a.permissions.x}}',
|
||||
props: {
|
||||
className: 'static-class',
|
||||
disabled: '{{b.abilities.y}}',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
text: '{{c.value}}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const expressions = findExpressionsInJson(testNode);
|
||||
expect(expressions).toHaveLength(3);
|
||||
expect(expressions[0].expression).toBe('{{a.permissions.x}}');
|
||||
expect(expressions[1].expression).toBe('{{b.abilities.y}}');
|
||||
expect(expressions[2].expression).toBe('{{c.value}}');
|
||||
});
|
||||
|
||||
it('표현식이 없는 정적 문자열은 무시합니다', () => {
|
||||
const testNode = {
|
||||
type: 'basic',
|
||||
name: 'Div',
|
||||
text: 'no expressions here',
|
||||
};
|
||||
|
||||
const expressions = findExpressionsInJson(testNode);
|
||||
expect(expressions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('한 문자열에 여러 표현식이 있으면 모두 추출합니다', () => {
|
||||
const testNode = {
|
||||
text: '{{a.permissions}} and {{b.abilities}}',
|
||||
};
|
||||
|
||||
const expressions = findExpressionsInJson(testNode);
|
||||
expect(expressions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 게시판 신고현황 named_actions 검증 테스트
|
||||
*
|
||||
* @description
|
||||
* - named_actions.searchBoardReports 정의 검증
|
||||
* - Desktop/Mobile Enter keypress + 검색 버튼 click이 actionRef 참조하는지 검증
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import boardReports from '../../../layouts/admin/admin_board_reports_index.json';
|
||||
|
||||
/** JSON 전체에서 actionRef를 가진 액션을 찾는 유틸리티 */
|
||||
function findActionRefs(obj: any, refName: string, results: any[] = []): any[] {
|
||||
if (!obj) return results;
|
||||
if (typeof obj === 'object') {
|
||||
if (obj.actionRef === refName) {
|
||||
results.push(obj);
|
||||
}
|
||||
for (const key of Object.keys(obj)) {
|
||||
findActionRefs(obj[key], refName, results);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** JSON 전체에서 특정 id를 가진 컴포넌트를 찾는 유틸리티 */
|
||||
function findComponentById(obj: any, id: string): any | null {
|
||||
if (!obj) return null;
|
||||
if (typeof obj === 'object') {
|
||||
if (obj.id === id) return obj;
|
||||
for (const key of Object.keys(obj)) {
|
||||
const found = findComponentById(obj[key], id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('게시판 신고현황 named_actions 검증', () => {
|
||||
it('레이아웃에 named_actions.searchBoardReports가 정의되어 있어야 함', () => {
|
||||
const namedActions = (boardReports as any).named_actions;
|
||||
expect(namedActions).toBeDefined();
|
||||
expect(namedActions.searchBoardReports).toBeDefined();
|
||||
});
|
||||
|
||||
it('searchBoardReports가 올바른 navigate 핸들러를 가져야 함', () => {
|
||||
const action = (boardReports as any).named_actions.searchBoardReports;
|
||||
expect(action.handler).toBe('navigate');
|
||||
expect(action.params.path).toBe('/admin/boards/reports');
|
||||
expect(action.params.query['filters[0][operator]']).toBe('like');
|
||||
expect(action.params.query.status).toBeDefined();
|
||||
expect(action.params.query.type).toBeDefined();
|
||||
});
|
||||
|
||||
it('Desktop + Mobile Enter keypress가 actionRef로 searchBoardReports를 참조해야 함', () => {
|
||||
const refs = findActionRefs(boardReports, 'searchBoardReports');
|
||||
const enterRefs = refs.filter((r: any) => r.type === 'keypress' && r.key === 'Enter');
|
||||
expect(enterRefs.length).toBe(2); // desktop + mobile
|
||||
});
|
||||
|
||||
it('검색 버튼 click이 actionRef로 searchBoardReports를 참조해야 함', () => {
|
||||
const refs = findActionRefs(boardReports, 'searchBoardReports');
|
||||
const clickRef = refs.find((r: any) => r.type === 'click');
|
||||
expect(clickRef).toBeDefined();
|
||||
});
|
||||
|
||||
it('총 3개의 actionRef가 searchBoardReports를 참조해야 함', () => {
|
||||
const refs = findActionRefs(boardReports, 'searchBoardReports');
|
||||
expect(refs.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('모바일 Dropdown 일괄 처리 검증', () => {
|
||||
it('mobile_bulk_action_dropdown의 items가 value 필드를 가져야 함 (key 금지)', () => {
|
||||
const dropdown = findComponentById(boardReports, 'mobile_bulk_action_dropdown');
|
||||
expect(dropdown).toBeDefined();
|
||||
|
||||
const items = dropdown.props.items as Array<{ value?: string; key?: string; label: string }>;
|
||||
expect(items).toBeDefined();
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
|
||||
for (const item of items) {
|
||||
expect(item.key, `items에 "key" 필드 사용 금지 (value: ${item.key})`).toBeUndefined();
|
||||
expect(item.value, `items의 "value" 필드가 정의되어야 함`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('mobile_bulk_action_dropdown의 switch cases가 items의 value와 일치해야 함', () => {
|
||||
const dropdown = findComponentById(boardReports, 'mobile_bulk_action_dropdown');
|
||||
const items = dropdown.props.items as Array<{ value: string }>;
|
||||
const switchAction = dropdown.actions?.find((a: any) => a.handler === 'switch');
|
||||
|
||||
expect(switchAction).toBeDefined();
|
||||
expect(switchAction.cases).toBeDefined();
|
||||
|
||||
for (const item of items) {
|
||||
expect(
|
||||
switchAction.cases[item.value],
|
||||
`switch cases에 "${item.value}" 케이스가 존재해야 함`,
|
||||
).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('switch cases의 각 케이스가 sequence 객체여야 함 (배열 직접 사용 금지)', () => {
|
||||
const dropdown = findComponentById(boardReports, 'mobile_bulk_action_dropdown');
|
||||
const switchAction = dropdown.actions?.find((a: any) => a.handler === 'switch');
|
||||
|
||||
for (const [key, caseValue] of Object.entries(switchAction.cases)) {
|
||||
expect(
|
||||
Array.isArray(caseValue),
|
||||
`"${key}" 케이스가 배열이면 안 됨 — sequence로 감싸야 함`,
|
||||
).toBe(false);
|
||||
expect(
|
||||
(caseValue as any).handler,
|
||||
`"${key}" 케이스의 handler가 "sequence"여야 함`,
|
||||
).toBe('sequence');
|
||||
expect(
|
||||
Array.isArray((caseValue as any).actions),
|
||||
`"${key}" 케이스의 actions가 배열이어야 함`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"common": {
|
||||
"$partial": "partial/en/common.json"
|
||||
},
|
||||
"admin": {
|
||||
"$partial": "partial/en/admin.json"
|
||||
},
|
||||
"board": {
|
||||
"$partial": "partial/en/board.json"
|
||||
},
|
||||
"messages": {
|
||||
"$partial": "partial/en/messages.json"
|
||||
},
|
||||
"validation": {
|
||||
"$partial": "partial/en/validation.json"
|
||||
},
|
||||
"attributes": {
|
||||
"$partial": "partial/en/attributes.json"
|
||||
},
|
||||
"enums": {
|
||||
"$partial": "partial/en/enums.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'description' => [
|
||||
// Board management (Admin)
|
||||
'board_index' => 'Board list viewed',
|
||||
'board_show' => 'Board details viewed (:board_name)',
|
||||
'board_create' => 'Board created (:board_name)',
|
||||
'board_update' => 'Board updated (:board_name)',
|
||||
'board_delete' => 'Board deleted (:board_name)',
|
||||
'board_copy' => 'Board copied (:board_name)',
|
||||
'board_add_to_menu' => 'Board added to menu (:board_name)',
|
||||
|
||||
// Board type (Admin)
|
||||
'board_type_index' => 'Board type list viewed',
|
||||
'board_type_show' => 'Board type details viewed (:type_name)',
|
||||
'board_type_create' => 'Board type created (:type_name)',
|
||||
'board_type_update' => 'Board type updated (:type_name)',
|
||||
'board_type_delete' => 'Board type deleted (:type_name)',
|
||||
|
||||
// Posts
|
||||
'post_create' => 'Post created (Board: :board_name, Title: :title)',
|
||||
'post_update' => 'Post updated (Board: :board_name, Title: :title)',
|
||||
'post_delete' => 'Post deleted (Board: :board_name, ID: :post_id)',
|
||||
'post_blind' => 'Post blinded (Board: :board_name, ID: :post_id)',
|
||||
'post_restore' => 'Post restored (Board: :board_name, ID: :post_id)',
|
||||
|
||||
// Comments
|
||||
'comment_create' => 'Comment created (Board: :board_name, Post: :post_id)',
|
||||
'comment_update' => 'Comment updated (ID: :comment_id)',
|
||||
'comment_delete' => 'Comment deleted (ID: :comment_id)',
|
||||
'comment_blind' => 'Comment blinded (ID: :comment_id)',
|
||||
'comment_restore' => 'Comment restored (ID: :comment_id)',
|
||||
|
||||
// Attachments
|
||||
'board_attachment_upload' => 'Attachment uploaded (Post: :post_id)',
|
||||
'board_attachment_delete' => 'Attachment deleted (Post: :post_id)',
|
||||
'board_attachment_reorder' => 'Attachment order changed (Post: :post_id)',
|
||||
|
||||
// Report management (Admin)
|
||||
'report_create' => 'Report created (ID: :report_id)',
|
||||
'report_update_status' => 'Report status changed (ID: :report_id)',
|
||||
'report_bulk_update_status' => 'Report statuses bulk changed (:count items)',
|
||||
'report_delete' => 'Report deleted (ID: :report_id)',
|
||||
'report_restore_content' => 'Report content restored (ID: :report_id)',
|
||||
'report_blind_content' => 'Report content blinded (ID: :report_id)',
|
||||
'report_delete_content' => 'Report content deleted (ID: :report_id)',
|
||||
|
||||
// Board settings
|
||||
'board_settings_index' => 'Board settings viewed',
|
||||
'board_settings_bulk_apply' => 'Board settings bulk applied',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"common": {
|
||||
"$partial": "partial/ko/common.json"
|
||||
},
|
||||
"admin": {
|
||||
"$partial": "partial/ko/admin.json"
|
||||
},
|
||||
"board": {
|
||||
"$partial": "partial/ko/board.json"
|
||||
},
|
||||
"messages": {
|
||||
"$partial": "partial/ko/messages.json"
|
||||
},
|
||||
"validation": {
|
||||
"$partial": "partial/ko/validation.json"
|
||||
},
|
||||
"attributes": {
|
||||
"$partial": "partial/ko/attributes.json"
|
||||
},
|
||||
"enums": {
|
||||
"$partial": "partial/ko/enums.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'description' => [
|
||||
// 게시판 관리 (Admin)
|
||||
'board_index' => '게시판 목록 조회',
|
||||
'board_show' => '게시판 상세 조회 (:board_name)',
|
||||
'board_create' => '게시판 생성 (:board_name)',
|
||||
'board_update' => '게시판 수정 (:board_name)',
|
||||
'board_delete' => '게시판 삭제 (:board_name)',
|
||||
'board_copy' => '게시판 복사 (:board_name)',
|
||||
'board_add_to_menu' => '게시판 메뉴 추가 (:board_name)',
|
||||
|
||||
// 게시판 유형 (Admin)
|
||||
'board_type_index' => '게시판 유형 목록 조회',
|
||||
'board_type_show' => '게시판 유형 상세 조회 (:type_name)',
|
||||
'board_type_create' => '게시판 유형 생성 (:type_name)',
|
||||
'board_type_update' => '게시판 유형 수정 (:type_name)',
|
||||
'board_type_delete' => '게시판 유형 삭제 (:type_name)',
|
||||
|
||||
// 게시물
|
||||
'post_create' => '게시물 작성 (게시판: :board_name, 제목: :title)',
|
||||
'post_update' => '게시물 수정 (게시판: :board_name, 제목: :title)',
|
||||
'post_delete' => '게시물 삭제 (게시판: :board_name, ID: :post_id)',
|
||||
'post_blind' => '게시물 블라인드 (게시판: :board_name, ID: :post_id)',
|
||||
'post_restore' => '게시물 복원 (게시판: :board_name, ID: :post_id)',
|
||||
|
||||
// 댓글
|
||||
'comment_create' => '댓글 작성 (게시판: :board_name, 게시물: :post_id)',
|
||||
'comment_update' => '댓글 수정 (ID: :comment_id)',
|
||||
'comment_delete' => '댓글 삭제 (ID: :comment_id)',
|
||||
'comment_blind' => '댓글 블라인드 (ID: :comment_id)',
|
||||
'comment_restore' => '댓글 복원 (ID: :comment_id)',
|
||||
|
||||
// 첨부파일
|
||||
'board_attachment_upload' => '첨부파일 업로드 (게시물: :post_id)',
|
||||
'board_attachment_delete' => '첨부파일 삭제 (게시물: :post_id)',
|
||||
'board_attachment_reorder' => '첨부파일 순서 변경 (게시물: :post_id)',
|
||||
|
||||
// 신고 관리 (Admin)
|
||||
'report_create' => '신고 접수 (ID: :report_id)',
|
||||
'report_update_status' => '신고 상태 변경 (ID: :report_id)',
|
||||
'report_bulk_update_status' => '신고 일괄 상태 변경 (:count건)',
|
||||
'report_delete' => '신고 삭제 (ID: :report_id)',
|
||||
'report_restore_content' => '신고 콘텐츠 복원 (ID: :report_id)',
|
||||
'report_blind_content' => '신고 콘텐츠 블라인드 (ID: :report_id)',
|
||||
'report_delete_content' => '신고 콘텐츠 삭제 (ID: :report_id)',
|
||||
|
||||
// 게시판 설정
|
||||
'board_settings_index' => '게시판 설정 조회',
|
||||
'board_settings_bulk_apply' => '게시판 설정 일괄 적용',
|
||||
],
|
||||
];
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"post": {
|
||||
"title": "Title",
|
||||
"content": "Content",
|
||||
"category": "Category",
|
||||
"is_notice": "Notice",
|
||||
"is_secret": "Secret",
|
||||
"content_mode": "Content Mode",
|
||||
"status": "Status",
|
||||
"author_name": "Author Name",
|
||||
"password": "Password"
|
||||
},
|
||||
"report": {
|
||||
"status": "Status",
|
||||
"process_note": "Reason"
|
||||
},
|
||||
"comment": {
|
||||
"content": "Comment Content",
|
||||
"parent_id": "Parent Comment",
|
||||
"is_secret": "Secret Comment",
|
||||
"status": "Status",
|
||||
"user_id": "Author",
|
||||
"author_name": "Author Name",
|
||||
"password": "Password",
|
||||
"ip_address": "IP Address"
|
||||
},
|
||||
"settings": {
|
||||
"basic_defaults.type": "Board Type",
|
||||
"basic_defaults.per_page": "Posts Per Page",
|
||||
"basic_defaults.per_page_mobile": "Posts Per Page (Mobile)",
|
||||
"basic_defaults.order_by": "Sort By",
|
||||
"basic_defaults.order_direction": "Sort Direction",
|
||||
"basic_defaults.secret_mode": "Secret Mode",
|
||||
"basic_defaults.use_comment": "Enable Comments",
|
||||
"basic_defaults.use_reply": "Enable Replies",
|
||||
"basic_defaults.max_reply_depth": "Max Reply Depth",
|
||||
"basic_defaults.max_comment_depth": "Max Comment Depth",
|
||||
"basic_defaults.comment_order": "Comment Order",
|
||||
"basic_defaults.show_view_count": "Show View Count",
|
||||
"basic_defaults.use_report": "Enable Reports",
|
||||
"basic_defaults.min_title_length": "Min Title Length",
|
||||
"basic_defaults.max_title_length": "Max Title Length",
|
||||
"basic_defaults.min_content_length": "Min Content Length",
|
||||
"basic_defaults.max_content_length": "Max Content Length",
|
||||
"basic_defaults.min_comment_length": "Min Comment Length",
|
||||
"basic_defaults.max_comment_length": "Max Comment Length",
|
||||
"basic_defaults.use_file_upload": "Enable File Upload",
|
||||
"basic_defaults.max_file_size": "Max File Size",
|
||||
"basic_defaults.max_file_count": "Max File Count",
|
||||
"basic_defaults.allowed_extensions": "Allowed Extensions",
|
||||
"basic_defaults.notify_admin_on_post": "Notify Admin on Post",
|
||||
"basic_defaults.notify_author": "Notify Author",
|
||||
"basic_defaults.new_display_hours": "New Post Display Hours",
|
||||
"basic_defaults.default_board_permissions": "Default Board Permissions",
|
||||
"basic_defaults.default_board_permissions.admin": "Admin Permissions",
|
||||
"basic_defaults.default_board_permissions.admin.*": "Admin Permission Role",
|
||||
"basic_defaults.default_board_permissions.posts": "Post Permissions",
|
||||
"basic_defaults.default_board_permissions.posts.*": "Post Permission Role",
|
||||
"basic_defaults.default_board_permissions.comments": "Comment Permissions",
|
||||
"basic_defaults.default_board_permissions.comments.*": "Comment Permission Role",
|
||||
"basic_defaults.default_board_permissions.attachments": "Attachment Permissions",
|
||||
"basic_defaults.default_board_permissions.attachments.*": "Attachment Permission Role",
|
||||
"basic_defaults.default_board_permissions.manager": "Board Manager Permissions",
|
||||
"basic_defaults.default_board_permissions.manager.*": "Board Manager Permission Role",
|
||||
"display.date_display_format": "Date Display Format",
|
||||
"report_policy.auto_hide_threshold": "Auto Hide Threshold",
|
||||
"report_policy.auto_hide_target": "Auto Hide Target",
|
||||
"report_policy.daily_report_limit": "Daily Report Limit",
|
||||
"report_policy.rejection_limit_count": "Rejection Limit Count",
|
||||
"report_policy.rejection_limit_days": "Rejection Limit (Days)",
|
||||
"spam_security.blocked_keywords": "Blocked Keywords",
|
||||
"spam_security.post_cooldown_seconds": "Post Cooldown (Seconds)",
|
||||
"spam_security.comment_cooldown_seconds": "Comment Cooldown (Seconds)",
|
||||
"spam_security.view_count_method": "View Count Method",
|
||||
"spam_security.view_count_cache_ttl": "View Count Cache TTL (Seconds)"
|
||||
},
|
||||
"bulk_apply": {
|
||||
"fields": "Fields",
|
||||
"fields.*": "Field Item",
|
||||
"apply_all": "Apply All",
|
||||
"board_ids": "Board List",
|
||||
"board_ids.*": "Board ID"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"parent_post": "Original Post",
|
||||
"view_parent_post": "View Original Post",
|
||||
"view_detail": "View Detail",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"permissions": {
|
||||
"admin": {
|
||||
"posts": {
|
||||
"read": "View Posts (Admin)",
|
||||
"read_desc": "Can view post list and details in admin page.",
|
||||
"write": "Create/Edit/Delete Posts (Admin)",
|
||||
"write_desc": "Can create posts and edit/delete all posts in admin page.",
|
||||
"read-secret": "View Secret Posts (Admin)",
|
||||
"read-secret_desc": "Can view secret posts without entering password."
|
||||
},
|
||||
"comments": {
|
||||
"read": "View Comments (Admin)",
|
||||
"read_desc": "Can view all comments in admin page.",
|
||||
"write": "Create/Edit/Delete Comments (Admin)",
|
||||
"write_desc": "Can create comments and edit/delete all comments in admin page."
|
||||
},
|
||||
"attachments": {
|
||||
"upload": "Upload/Delete Files (Admin)",
|
||||
"upload_desc": "Can attach files to posts and delete all attachments.",
|
||||
"download": "Download Files (Admin)",
|
||||
"download_desc": "Can download all attachments."
|
||||
},
|
||||
"manage": "Manage Board (Admin)",
|
||||
"manage_desc": "Can manage others' posts/comments, blind, restore, and set notices."
|
||||
},
|
||||
"posts": {
|
||||
"read": "View Posts",
|
||||
"read_desc": "Can view post list and details.",
|
||||
"write": "Create/Edit/Delete Posts",
|
||||
"write_desc": "Can create posts and edit/delete own posts.",
|
||||
"read-secret": "View Secret Posts",
|
||||
"read-secret_desc": "Can view secret posts without entering password."
|
||||
},
|
||||
"comments": {
|
||||
"read": "View Comments",
|
||||
"read_desc": "Can view comment list.",
|
||||
"write": "Create/Edit/Delete Comments",
|
||||
"write_desc": "Can create comments and edit/delete own comments."
|
||||
},
|
||||
"attachments": {
|
||||
"upload": "Upload/Delete Files",
|
||||
"upload_desc": "Can attach files to posts and delete own attachments.",
|
||||
"download": "Download Files",
|
||||
"download_desc": "Can download attached files."
|
||||
},
|
||||
"manager": "Manage Board (User)",
|
||||
"manager_desc": "Can blind, restore, and delete others' posts/comments in user page."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"loading": "Loading...",
|
||||
"cancel": "Cancel",
|
||||
"refresh": "Refresh",
|
||||
"search": "Search",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"deleting": "Deleting...",
|
||||
"view": "View",
|
||||
"copy": "Copy",
|
||||
"confirm": "Confirm",
|
||||
"close": "Close",
|
||||
"continue": "Continue",
|
||||
"blind": "Blind",
|
||||
"restore": "Restore",
|
||||
"back": "Back",
|
||||
"new": "New",
|
||||
"pagination_info": "Showing {{from}}-{{to}} of {{total}} users",
|
||||
"error_occurred": "An error has occurred",
|
||||
"optional": "Optional"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"report_status": {
|
||||
"pending": "Pending",
|
||||
"review": "Under Review",
|
||||
"rejected": "Rejected",
|
||||
"suspended": "Suspended",
|
||||
"deleted": "Permanently Deleted"
|
||||
},
|
||||
"trigger_type": {
|
||||
"report": "Report",
|
||||
"admin": "Admin",
|
||||
"system": "System"
|
||||
},
|
||||
"report_type": {
|
||||
"post": "Post",
|
||||
"comment": "Comment"
|
||||
},
|
||||
"report_reason_type": {
|
||||
"abuse": "Abuse/Insult",
|
||||
"hate_speech": "Hate Speech",
|
||||
"spam": "Spam/Advertisement",
|
||||
"copyright": "Copyright Violation",
|
||||
"privacy": "Privacy Violation",
|
||||
"misinformation": "Misinformation/Fake News",
|
||||
"sexual": "Sexual Content",
|
||||
"violence": "Violent Content",
|
||||
"other": "Other"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"common": {
|
||||
"system": "System"
|
||||
},
|
||||
"boards": {
|
||||
"menu_added_success": "Successfully added to admin menu.",
|
||||
"menu_add_failed": "Failed to add menu.",
|
||||
"menu_already_exists": "Menu already added.",
|
||||
"module_not_found": "Board module not found."
|
||||
},
|
||||
"post": {
|
||||
"notice": "Notice",
|
||||
"reply": "Reply",
|
||||
"action": {
|
||||
"blind": "Blinded",
|
||||
"delete": "Deleted",
|
||||
"restore": "Restored"
|
||||
},
|
||||
"deleted_post_title": "This post has been deleted",
|
||||
"deleted_post_content": "This post has been deleted.",
|
||||
"blinded_post_title": "This post has been blinded",
|
||||
"blinded_post_content": "This post has been blinded."
|
||||
},
|
||||
"comment": {
|
||||
"create_success": "Comment has been created.",
|
||||
"update_success": "Comment has been updated.",
|
||||
"delete_success": "Comment has been deleted.",
|
||||
"blind_success": "Comment has been blinded.",
|
||||
"restore_success": "Comment has been restored.",
|
||||
"not_found": "Comment not found.",
|
||||
"not_guest_comment": "This is not a guest comment.",
|
||||
"invalid_password": "Password does not match.",
|
||||
"password_verified": "Password has been verified.",
|
||||
"verify_password_failed": "Password verification failed.",
|
||||
"deleted_comment_content": "This comment has been deleted.",
|
||||
"blinded_comment_content": "This comment has been blinded.",
|
||||
"view_original": "View Original",
|
||||
"hide_original": "Hide Original",
|
||||
"post_blinded": "Comments cannot be posted on a blinded post.",
|
||||
"post_deleted": "Comments cannot be posted on a deleted post."
|
||||
},
|
||||
"reports": {
|
||||
"create_success": "Report has been submitted.",
|
||||
"create_failed": "Failed to submit report.",
|
||||
"duplicate_report": "You have already reported this.",
|
||||
"report_disabled": "Reporting is disabled for this board.",
|
||||
"target_not_reportable": "This target cannot be reported."
|
||||
},
|
||||
"board_type": {
|
||||
"list": "Board types retrieved successfully.",
|
||||
"created": "Board type has been created.",
|
||||
"updated": "Board type has been updated.",
|
||||
"deleted": "Board type has been deleted.",
|
||||
"default_set": "Default board type has been set.",
|
||||
"not_found": "Board type not found.",
|
||||
"in_use": "Cannot delete a board type that is in use.",
|
||||
"delete_in_use": "Cannot delete this board type. It is used by :count board(s)."
|
||||
},
|
||||
"settings": {
|
||||
"fetch_success": "Settings loaded successfully.",
|
||||
"fetch_failed": "Failed to load settings.",
|
||||
"save_success": "Settings saved successfully.",
|
||||
"save_failed": "Failed to save settings.",
|
||||
"save_error": "An error occurred while saving settings.",
|
||||
"bulk_apply_success": "Settings applied to :count boards.",
|
||||
"bulk_apply_failed": "Failed to bulk apply settings.",
|
||||
"clear_cache_success": "Settings cache cleared successfully.",
|
||||
"clear_cache_error": "An error occurred while clearing settings cache."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"spam": "Spam/Ads",
|
||||
"abuse": "Abuse/Insult",
|
||||
"obscenity": "Obscenity",
|
||||
"illegal": "Illegal Content",
|
||||
"hate_speech": "Hate Speech",
|
||||
"privacy": "Privacy Violation",
|
||||
"copyright": "Copyright Infringement",
|
||||
"other": "Other"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"post": {
|
||||
"title": {
|
||||
"required": "Title is required.",
|
||||
"min": "Title must be at least :min characters.",
|
||||
"max": "Title can be up to :max characters."
|
||||
},
|
||||
"content": {
|
||||
"required": "Content is required.",
|
||||
"min": "Content must be at least :min characters.",
|
||||
"max": "Content can be up to :max characters."
|
||||
},
|
||||
"category": {
|
||||
"max": "Category can be up to :max characters."
|
||||
},
|
||||
"status": {
|
||||
"in": "Invalid status."
|
||||
},
|
||||
"author_name": {
|
||||
"max": "Author name can be up to :max characters."
|
||||
},
|
||||
"password": {
|
||||
"min": "Password must be at least :min characters."
|
||||
},
|
||||
"parent_id": {
|
||||
"not_found": "Original post not found.",
|
||||
"blinded": "You cannot reply to a blinded post.",
|
||||
"deleted": "You cannot reply to a deleted post.",
|
||||
"exists": "The specified post does not exist."
|
||||
},
|
||||
"files": {
|
||||
"array": "Files must be an array.",
|
||||
"max": "You can upload up to :max files.",
|
||||
"file": "Must be a valid file.",
|
||||
"file_max": "File size cannot exceed :maxKB.",
|
||||
"mimes": "File extension not allowed."
|
||||
}
|
||||
},
|
||||
"comment": {
|
||||
"content": {
|
||||
"required": "Comment content is required.",
|
||||
"min": "Comment must be at least :min characters.",
|
||||
"max": "Comment can be up to :max characters."
|
||||
},
|
||||
"post_id": {
|
||||
"not_found": "Post not found.",
|
||||
"blinded": "You cannot comment on a blinded post.",
|
||||
"deleted": "You cannot comment on a deleted post."
|
||||
},
|
||||
"parent_id": {
|
||||
"integer": "Parent comment ID must be an integer.",
|
||||
"not_found": "Parent comment not found.",
|
||||
"blinded": "You cannot reply to a blinded comment.",
|
||||
"deleted": "You cannot reply to a deleted comment."
|
||||
},
|
||||
"user_id": {
|
||||
"exists": "User does not exist."
|
||||
},
|
||||
"author_name": {
|
||||
"max": "Author name can be up to :max characters."
|
||||
},
|
||||
"password": {
|
||||
"min": "Password must be at least :min characters."
|
||||
},
|
||||
"ip_address": {
|
||||
"required": "IP address is required."
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"not_found": "Board not found."
|
||||
},
|
||||
"board_type": {
|
||||
"slug_required": "Slug is required.",
|
||||
"slug_format": "Slug must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.",
|
||||
"slug_unique": "This slug is already in use.",
|
||||
"name_required": "Type name is required.",
|
||||
"name_ko_required": "Korean type name is required."
|
||||
},
|
||||
"report": {
|
||||
"status": {
|
||||
"required": "Status is required.",
|
||||
"in": "Invalid status."
|
||||
},
|
||||
"process_note": {
|
||||
"max": "Reason can be up to 1000 characters."
|
||||
},
|
||||
"invalid_status_transition": "Cannot change to this status from current status."
|
||||
},
|
||||
"max_reply_depth": {
|
||||
"min": "Max reply depth must be at least :min.",
|
||||
"max": "Max reply depth can be up to :max."
|
||||
},
|
||||
"max_comment_depth": {
|
||||
"min": "Max comment depth must be at least :min.",
|
||||
"max": "Max comment depth can be up to :max."
|
||||
},
|
||||
"notify_author_channels": {
|
||||
"array": "Author notification channels must be an array.",
|
||||
"item_max": "Channel name can be up to :max characters."
|
||||
},
|
||||
"notify_admin_on_post_channels": {
|
||||
"array": "Admin notification channels must be an array.",
|
||||
"item_max": "Channel name can be up to :max characters."
|
||||
},
|
||||
"settings": {
|
||||
"category": {
|
||||
"required": "Category is required.",
|
||||
"in": "Invalid category."
|
||||
}
|
||||
},
|
||||
"bulk_apply": {
|
||||
"fields": {
|
||||
"required": "Please select fields to apply.",
|
||||
"min": "Please select at least one field."
|
||||
},
|
||||
"fields_item": {
|
||||
"in": "Invalid field."
|
||||
},
|
||||
"apply_all": {
|
||||
"required": "Apply all option is required.",
|
||||
"boolean": "Apply all must be true or false."
|
||||
},
|
||||
"board_ids": {
|
||||
"required_if": "Please select boards when not applying to all.",
|
||||
"array": "Board IDs must be an array."
|
||||
},
|
||||
"board_ids_item": {
|
||||
"integer": "Board ID must be an integer.",
|
||||
"exists": "Board does not exist."
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"post": {
|
||||
"title": "제목",
|
||||
"content": "내용",
|
||||
"category": "분류",
|
||||
"is_notice": "공지글",
|
||||
"is_secret": "비밀글",
|
||||
"content_mode": "내용 모드",
|
||||
"status": "상태",
|
||||
"author_name": "작성자명",
|
||||
"password": "비밀번호"
|
||||
},
|
||||
"report": {
|
||||
"status": "상태",
|
||||
"process_note": "처리 사유"
|
||||
},
|
||||
"comment": {
|
||||
"content": "댓글 내용",
|
||||
"parent_id": "부모 댓글",
|
||||
"is_secret": "비밀 댓글",
|
||||
"status": "상태",
|
||||
"user_id": "작성자",
|
||||
"author_name": "작성자명",
|
||||
"password": "비밀번호",
|
||||
"ip_address": "IP 주소"
|
||||
},
|
||||
"settings": {
|
||||
"basic_defaults.type": "게시판 유형",
|
||||
"basic_defaults.per_page": "페이지당 게시글 수",
|
||||
"basic_defaults.per_page_mobile": "모바일 페이지당 게시글 수",
|
||||
"basic_defaults.order_by": "정렬 기준",
|
||||
"basic_defaults.order_direction": "정렬 방향",
|
||||
"basic_defaults.secret_mode": "비밀글 모드",
|
||||
"basic_defaults.use_comment": "댓글 사용",
|
||||
"basic_defaults.use_reply": "답글 사용",
|
||||
"basic_defaults.max_reply_depth": "최대 답글 깊이",
|
||||
"basic_defaults.max_comment_depth": "최대 댓글 깊이",
|
||||
"basic_defaults.comment_order": "댓글 정렬",
|
||||
"basic_defaults.show_view_count": "조회수 표시",
|
||||
"basic_defaults.use_report": "신고 기능",
|
||||
"basic_defaults.min_title_length": "제목 최소 길이",
|
||||
"basic_defaults.max_title_length": "제목 최대 길이",
|
||||
"basic_defaults.min_content_length": "내용 최소 길이",
|
||||
"basic_defaults.max_content_length": "내용 최대 길이",
|
||||
"basic_defaults.min_comment_length": "댓글 최소 길이",
|
||||
"basic_defaults.max_comment_length": "댓글 최대 길이",
|
||||
"basic_defaults.use_file_upload": "파일 업로드 사용",
|
||||
"basic_defaults.max_file_size": "최대 파일 크기",
|
||||
"basic_defaults.max_file_count": "최대 파일 수",
|
||||
"basic_defaults.allowed_extensions": "허용 확장자",
|
||||
"basic_defaults.notify_admin_on_post": "게시글 작성 시 관리자 알림",
|
||||
"basic_defaults.notify_author": "작성자 알림",
|
||||
"basic_defaults.new_display_hours": "새 글 표시 시간",
|
||||
"basic_defaults.default_board_permissions": "기본 게시판 권한",
|
||||
"basic_defaults.default_board_permissions.admin": "관리자 권한",
|
||||
"basic_defaults.default_board_permissions.admin.*": "관리자 권한 역할",
|
||||
"basic_defaults.default_board_permissions.posts": "게시글 권한",
|
||||
"basic_defaults.default_board_permissions.posts.*": "게시글 권한 역할",
|
||||
"basic_defaults.default_board_permissions.comments": "댓글 권한",
|
||||
"basic_defaults.default_board_permissions.comments.*": "댓글 권한 역할",
|
||||
"basic_defaults.default_board_permissions.attachments": "첨부파일 권한",
|
||||
"basic_defaults.default_board_permissions.attachments.*": "첨부파일 권한 역할",
|
||||
"basic_defaults.default_board_permissions.manager": "게시판 관리 권한",
|
||||
"basic_defaults.default_board_permissions.manager.*": "게시판 관리 권한 역할",
|
||||
"display.date_display_format": "날짜 표시 방식",
|
||||
"report_policy.auto_hide_threshold": "자동 숨김 임계값",
|
||||
"report_policy.auto_hide_target": "자동 숨김 대상",
|
||||
"report_policy.daily_report_limit": "일일 신고 제한",
|
||||
"report_policy.rejection_limit_count": "반려 제한 횟수",
|
||||
"report_policy.rejection_limit_days": "반려 제한 기간(일)",
|
||||
"spam_security.blocked_keywords": "차단 키워드",
|
||||
"spam_security.post_cooldown_seconds": "게시글 작성 간격(초)",
|
||||
"spam_security.comment_cooldown_seconds": "댓글 작성 간격(초)",
|
||||
"spam_security.view_count_method": "조회수 카운트 방식",
|
||||
"spam_security.view_count_cache_ttl": "조회수 캐시 유효시간(초)"
|
||||
},
|
||||
"bulk_apply": {
|
||||
"fields": "적용 필드",
|
||||
"fields.*": "적용 필드 항목",
|
||||
"apply_all": "전체 적용",
|
||||
"board_ids": "게시판 목록",
|
||||
"board_ids.*": "게시판 ID"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"parent_post": "원글",
|
||||
"view_parent_post": "원글 보기",
|
||||
"view_detail": "상세 보기",
|
||||
"show_more": "더보기",
|
||||
"show_less": "접기",
|
||||
"permissions": {
|
||||
"admin": {
|
||||
"posts": {
|
||||
"read": "게시글 조회 (관리자)",
|
||||
"read_desc": "관리자 페이지에서 게시글 목록 및 상세 내용을 조회할 수 있습니다.",
|
||||
"write": "게시글 작성/수정/삭제 (관리자)",
|
||||
"write_desc": "관리자 페이지에서 게시글을 작성하고, 모든 게시글을 수정/삭제할 수 있습니다.",
|
||||
"read-secret": "비밀글 조회 (관리자)",
|
||||
"read-secret_desc": "비밀글을 비밀번호 입력 없이 열람할 수 있습니다."
|
||||
},
|
||||
"comments": {
|
||||
"read": "댓글 조회 (관리자)",
|
||||
"read_desc": "관리자 페이지에서 모든 댓글을 조회할 수 있습니다.",
|
||||
"write": "댓글 작성/수정/삭제 (관리자)",
|
||||
"write_desc": "관리자 페이지에서 댓글을 작성하고, 모든 댓글을 수정/삭제할 수 있습니다."
|
||||
},
|
||||
"attachments": {
|
||||
"upload": "파일 업로드/삭제 (관리자)",
|
||||
"upload_desc": "게시글에 파일을 첨부하고, 모든 첨부파일을 삭제할 수 있습니다.",
|
||||
"download": "파일 다운로드 (관리자)",
|
||||
"download_desc": "모든 첨부파일을 다운로드할 수 있습니다."
|
||||
},
|
||||
"manage": "게시판 관리 (관리자)",
|
||||
"manage_desc": "타인 게시글/댓글 관리, 블라인드, 복원, 공지 설정을 할 수 있습니다."
|
||||
},
|
||||
"posts": {
|
||||
"read": "게시글 조회",
|
||||
"read_desc": "게시글 목록 및 상세 내용을 조회할 수 있습니다.",
|
||||
"write": "게시글 작성/수정/삭제",
|
||||
"write_desc": "게시글을 작성하고 본인의 글을 수정/삭제할 수 있습니다.",
|
||||
"read-secret": "비밀글 조회",
|
||||
"read-secret_desc": "비밀글을 비밀번호 입력 없이 열람할 수 있습니다."
|
||||
},
|
||||
"comments": {
|
||||
"read": "댓글 조회",
|
||||
"read_desc": "댓글 목록을 조회할 수 있습니다.",
|
||||
"write": "댓글 작성/수정/삭제",
|
||||
"write_desc": "댓글을 작성하고 본인의 댓글을 수정/삭제할 수 있습니다."
|
||||
},
|
||||
"attachments": {
|
||||
"upload": "파일 업로드/삭제",
|
||||
"upload_desc": "게시글에 파일을 첨부하고, 본인이 첨부한 파일을 삭제할 수 있습니다.",
|
||||
"download": "파일 다운로드",
|
||||
"download_desc": "첨부파일을 다운로드할 수 있습니다."
|
||||
},
|
||||
"manager": "게시판 관리 (사용자)",
|
||||
"manager_desc": "사용자 페이지에서 타인의 게시글/댓글을 블라인드, 복원, 삭제할 수 있습니다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"save": "저장",
|
||||
"saving": "저장 중...",
|
||||
"loading": "로딩 중...",
|
||||
"cancel": "취소",
|
||||
"refresh": "새로고침",
|
||||
"search": "검색",
|
||||
"add": "추가",
|
||||
"edit": "수정",
|
||||
"delete": "삭제",
|
||||
"deleting": "삭제 중...",
|
||||
"view": "보기",
|
||||
"copy": "복사",
|
||||
"confirm": "확인",
|
||||
"close": "닫기",
|
||||
"continue": "계속하기",
|
||||
"blind": "블라인드",
|
||||
"restore": "복원",
|
||||
"back": "뒤로가기",
|
||||
"new": "New",
|
||||
"pagination_info": "총 {{total}}명 중 {{from}}-{{to}}명 표시",
|
||||
"error_occurred": "오류가 발생했습니다",
|
||||
"optional": "선택"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"report_status": {
|
||||
"pending": "접수",
|
||||
"review": "검토",
|
||||
"rejected": "반려",
|
||||
"suspended": "게시중단",
|
||||
"deleted": "영구 삭제"
|
||||
},
|
||||
"trigger_type": {
|
||||
"report": "신고",
|
||||
"admin": "관리자",
|
||||
"system": "시스템"
|
||||
},
|
||||
"report_type": {
|
||||
"post": "게시글",
|
||||
"comment": "댓글"
|
||||
},
|
||||
"report_reason_type": {
|
||||
"abuse": "욕설/비방",
|
||||
"hate_speech": "혐오 발언",
|
||||
"spam": "스팸/광고",
|
||||
"copyright": "저작권 침해",
|
||||
"privacy": "개인정보 노출",
|
||||
"misinformation": "허위정보/가짜뉴스",
|
||||
"sexual": "성적인 콘텐츠",
|
||||
"violence": "폭력적인 콘텐츠",
|
||||
"other": "기타"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"common": {
|
||||
"system": "시스템"
|
||||
},
|
||||
"boards": {
|
||||
"menu_added_success": "관리자 메뉴에 추가되었습니다.",
|
||||
"menu_add_failed": "메뉴 추가에 실패했습니다.",
|
||||
"menu_already_exists": "이미 추가된 메뉴입니다.",
|
||||
"module_not_found": "게시판 모듈을 찾을 수 없습니다."
|
||||
},
|
||||
"post": {
|
||||
"notice": "공지",
|
||||
"reply": "답변",
|
||||
"action": {
|
||||
"blind": "블라인드 처리",
|
||||
"delete": "삭제 처리",
|
||||
"restore": "복원 처리"
|
||||
},
|
||||
"deleted_post_title": "삭제된 게시글입니다",
|
||||
"deleted_post_content": "이 게시글은 삭제되었습니다.",
|
||||
"blinded_post_title": "블라인드 처리된 게시글입니다",
|
||||
"blinded_post_content": "이 게시글은 블라인드 처리되었습니다."
|
||||
},
|
||||
"comment": {
|
||||
"create_success": "댓글이 등록되었습니다.",
|
||||
"update_success": "댓글이 수정되었습니다.",
|
||||
"delete_success": "댓글이 삭제되었습니다.",
|
||||
"blind_success": "댓글이 블라인드 처리되었습니다.",
|
||||
"restore_success": "댓글이 복원되었습니다.",
|
||||
"not_found": "댓글을 찾을 수 없습니다.",
|
||||
"not_guest_comment": "비회원 댓글이 아닙니다.",
|
||||
"invalid_password": "비밀번호가 일치하지 않습니다.",
|
||||
"password_verified": "비밀번호가 확인되었습니다.",
|
||||
"verify_password_failed": "비밀번호 확인에 실패했습니다.",
|
||||
"deleted_comment_content": "삭제된 댓글입니다",
|
||||
"blinded_comment_content": "블라인드 처리된 댓글입니다",
|
||||
"view_original": "원문 보기",
|
||||
"hide_original": "원문 숨기기",
|
||||
"post_blinded": "블라인드 처리된 게시글에는 댓글을 작성할 수 없습니다.",
|
||||
"post_deleted": "삭제된 게시글에는 댓글을 작성할 수 없습니다."
|
||||
},
|
||||
"reports": {
|
||||
"create_success": "신고가 접수되었습니다.",
|
||||
"create_failed": "신고 접수에 실패했습니다.",
|
||||
"duplicate_report": "이미 신고한 대상입니다.",
|
||||
"report_disabled": "이 게시판에서는 신고 기능이 비활성화되어 있습니다.",
|
||||
"target_not_reportable": "신고할 수 없는 대상입니다."
|
||||
},
|
||||
"board_type": {
|
||||
"list": "게시판 유형 목록을 조회했습니다.",
|
||||
"created": "게시판 유형이 생성되었습니다.",
|
||||
"updated": "게시판 유형이 수정되었습니다.",
|
||||
"deleted": "게시판 유형이 삭제되었습니다.",
|
||||
"default_set": "기본 게시판 유형이 설정되었습니다.",
|
||||
"not_found": "게시판 유형을 찾을 수 없습니다.",
|
||||
"in_use": "사용 중인 게시판 유형은 삭제할 수 없습니다.",
|
||||
"delete_in_use": ":count개 게시판에서 사용 중인 유형은 삭제할 수 없습니다."
|
||||
},
|
||||
"settings": {
|
||||
"fetch_success": "설정을 조회했습니다.",
|
||||
"fetch_failed": "설정 조회에 실패했습니다.",
|
||||
"save_success": "설정이 저장되었습니다.",
|
||||
"save_failed": "설정 저장에 실패했습니다.",
|
||||
"save_error": "설정 저장 중 오류가 발생했습니다.",
|
||||
"bulk_apply_success": ":count개 게시판에 설정이 일괄 적용되었습니다.",
|
||||
"bulk_apply_failed": "설정 일괄 적용에 실패했습니다.",
|
||||
"clear_cache_success": "설정 캐시가 초기화되었습니다.",
|
||||
"clear_cache_error": "설정 캐시 초기화 중 오류가 발생했습니다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"spam": "스팸/광고",
|
||||
"abuse": "욕설/비방",
|
||||
"obscenity": "음란물",
|
||||
"illegal": "불법정보",
|
||||
"hate_speech": "혐오 발언",
|
||||
"privacy": "개인정보 노출",
|
||||
"copyright": "저작권 침해",
|
||||
"other": "기타"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"post": {
|
||||
"title": {
|
||||
"required": "제목은 필수입니다.",
|
||||
"min": "제목은 최소 :min자 이상이어야 합니다.",
|
||||
"max": "제목은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"content": {
|
||||
"required": "내용은 필수입니다.",
|
||||
"min": "내용은 최소 :min자 이상이어야 합니다.",
|
||||
"max": "내용은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"category": {
|
||||
"max": "분류는 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"status": {
|
||||
"in": "유효하지 않은 상태입니다."
|
||||
},
|
||||
"author_name": {
|
||||
"max": "작성자명은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"password": {
|
||||
"min": "비밀번호는 최소 :min자 이상이어야 합니다."
|
||||
},
|
||||
"parent_id": {
|
||||
"not_found": "원글을 찾을 수 없습니다.",
|
||||
"blinded": "블라인드 처리된 게시글에는 답글을 작성할 수 없습니다.",
|
||||
"deleted": "삭제된 게시글에는 답글을 작성할 수 없습니다.",
|
||||
"exists": "존재하지 않는 게시글입니다."
|
||||
},
|
||||
"files": {
|
||||
"array": "파일은 배열이어야 합니다.",
|
||||
"max": "최대 :max개의 파일만 업로드 가능합니다.",
|
||||
"file": "유효한 파일이어야 합니다.",
|
||||
"file_max": "파일 용량은 :maxKB를 초과할 수 없습니다.",
|
||||
"mimes": "허용된 확장자가 아닙니다."
|
||||
}
|
||||
},
|
||||
"comment": {
|
||||
"content": {
|
||||
"required": "댓글 내용은 필수입니다.",
|
||||
"min": "댓글은 최소 :min자 이상이어야 합니다.",
|
||||
"max": "댓글은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"post_id": {
|
||||
"not_found": "게시글을 찾을 수 없습니다.",
|
||||
"blinded": "블라인드 처리된 게시글에는 댓글을 작성할 수 없습니다.",
|
||||
"deleted": "삭제된 게시글에는 댓글을 작성할 수 없습니다."
|
||||
},
|
||||
"parent_id": {
|
||||
"integer": "부모 댓글 ID는 정수여야 합니다.",
|
||||
"not_found": "부모 댓글을 찾을 수 없습니다.",
|
||||
"blinded": "블라인드 처리된 댓글에는 답글을 작성할 수 없습니다.",
|
||||
"deleted": "삭제된 댓글에는 답글을 작성할 수 없습니다."
|
||||
},
|
||||
"user_id": {
|
||||
"exists": "존재하지 않는 사용자입니다."
|
||||
},
|
||||
"author_name": {
|
||||
"max": "작성자명은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"password": {
|
||||
"min": "비밀번호는 최소 :min자 이상이어야 합니다."
|
||||
},
|
||||
"ip_address": {
|
||||
"required": "IP 주소는 필수입니다."
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"not_found": "게시판을 찾을 수 없습니다."
|
||||
},
|
||||
"board_type": {
|
||||
"slug_required": "슬러그는 필수입니다.",
|
||||
"slug_format": "슬러그는 소문자, 숫자, 하이픈만 사용할 수 있으며 소문자로 시작해야 합니다.",
|
||||
"slug_unique": "이미 사용 중인 슬러그입니다.",
|
||||
"name_required": "유형명은 필수입니다.",
|
||||
"name_ko_required": "한국어 유형명은 필수입니다."
|
||||
},
|
||||
"report": {
|
||||
"status": {
|
||||
"required": "상태는 필수입니다.",
|
||||
"in": "유효하지 않은 상태입니다."
|
||||
},
|
||||
"process_note": {
|
||||
"max": "처리 사유는 최대 1000자까지 입력 가능합니다."
|
||||
},
|
||||
"invalid_status_transition": "현재 상태에서 해당 상태로 변경할 수 없습니다."
|
||||
},
|
||||
"max_reply_depth": {
|
||||
"min": "답변글 최대 깊이는 최소 :min 이상이어야 합니다.",
|
||||
"max": "답변글 최대 깊이는 최대 :max까지 설정 가능합니다."
|
||||
},
|
||||
"max_comment_depth": {
|
||||
"min": "대댓글 최대 깊이는 최소 :min 이상이어야 합니다.",
|
||||
"max": "대댓글 최대 깊이는 최대 :max까지 설정 가능합니다."
|
||||
},
|
||||
"notify_author_channels": {
|
||||
"array": "작성자 알림 채널은 배열이어야 합니다.",
|
||||
"item_max": "알림 채널명은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"notify_admin_on_post_channels": {
|
||||
"array": "관리자 알림 채널은 배열이어야 합니다.",
|
||||
"item_max": "알림 채널명은 최대 :max자까지 입력 가능합니다."
|
||||
},
|
||||
"settings": {
|
||||
"category": {
|
||||
"required": "카테고리는 필수입니다.",
|
||||
"in": "유효하지 않은 카테고리입니다."
|
||||
}
|
||||
},
|
||||
"bulk_apply": {
|
||||
"fields": {
|
||||
"required": "적용할 필드를 선택해주세요.",
|
||||
"min": "최소 1개 이상의 필드를 선택해주세요."
|
||||
},
|
||||
"fields_item": {
|
||||
"in": "유효하지 않은 필드입니다."
|
||||
},
|
||||
"apply_all": {
|
||||
"required": "전체 적용 여부는 필수입니다.",
|
||||
"boolean": "전체 적용 여부는 true/false여야 합니다."
|
||||
},
|
||||
"board_ids": {
|
||||
"required_if": "특정 게시판 적용 시 게시판을 선택해주세요.",
|
||||
"array": "게시판 ID는 배열이어야 합니다."
|
||||
},
|
||||
"board_ids_item": {
|
||||
"integer": "게시판 ID는 정수여야 합니다.",
|
||||
"exists": "존재하지 않는 게시판입니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"layout_name": "admin_board_form",
|
||||
"permissions": [
|
||||
"sirsoft-board.boards.read"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "{{route?.id ? '$t:sirsoft-board.admin.form.title_edit' : '$t:sirsoft-board.admin.form.title_create'}}",
|
||||
"description": "$t:sirsoft-board.admin.form.description"
|
||||
},
|
||||
"state": {
|
||||
"activeTab": "basic",
|
||||
"showCategoryRemoveModal": false,
|
||||
"categoryToRemove": null,
|
||||
"isSaving": false,
|
||||
"hasChanges": false,
|
||||
"showCancelConfirmModal": false,
|
||||
"form": [],
|
||||
"isDataLoaded": false,
|
||||
"managerSearchResults": [],
|
||||
"stepSearchResults": []
|
||||
},
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "boards",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/boards/form-data",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"cache": false,
|
||||
"loading_strategy": "blocking",
|
||||
"params": {
|
||||
"board_id": "{{route?.id}}",
|
||||
"copy_id": "{{query?.copy_id}}"
|
||||
},
|
||||
"initLocal": "form",
|
||||
"refetchOnMount": true,
|
||||
"errorHandling": {
|
||||
"403": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roles",
|
||||
"type": "api",
|
||||
"endpoint": "/api/admin/roles/active",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true
|
||||
},
|
||||
{
|
||||
"id": "notification_channels",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/notification-channels",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true
|
||||
}
|
||||
],
|
||||
"computed": {
|
||||
"isReadOnly": "{{!!route?.id && !!boards?.data && boards?.data?.abilities?.can_update !== true}}"
|
||||
},
|
||||
"slots": {
|
||||
"content": [
|
||||
{
|
||||
"id": "board_form_wrapper",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": true,
|
||||
"dataKey": "form",
|
||||
"trackChanges": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "read_only_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_computed.isReadOnly}}",
|
||||
"props": {
|
||||
"className": "mx-6 mt-4 flex items-center gap-3 rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 px-4 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "lock",
|
||||
"className": "w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-800 dark:text-amber-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.read_only_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sticky_header_container",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "sticky top-0 z-40 bg-gray-50 dark:bg-gray-900"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "form_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border-b border-gray-200 dark:border-gray-700 py-3 px-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header_flex_container",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-between items-center"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header_title",
|
||||
"type": "basic",
|
||||
"name": "H1",
|
||||
"props": {
|
||||
"className": "text-xl font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{route?.id ? '$t:sirsoft-board.admin.form.title_edit' : '$t:sirsoft-board.admin.form.title_create'}}"
|
||||
},
|
||||
{
|
||||
"id": "header_actions",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "cancel_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 font-medium transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"target": "/admin/boards"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "save_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"disabled": "{{_computed.isReadOnly || _local.isSaving || (!!route?.id && !_local.hasChanges)}}",
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "{{route?.id ? '/api/modules/sirsoft-board/admin/boards/' + route.id : '/api/modules/sirsoft-board/admin/boards'}}",
|
||||
"params": {
|
||||
"method": "{{route?.id ? 'PUT' : 'POST'}}",
|
||||
"body": "{{_local.form}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"hasChanges": false,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "{{route?.id ? '$t:sirsoft-board.admin.board.messages.update_success' : '$t:sirsoft-board.admin.board.messages.create_success'}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "navigate",
|
||||
"condition": "{{!route?.id}}",
|
||||
"params": {
|
||||
"path": "{{'/admin/boards/' + result.data.id + '/edit'}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"errors": "{{error.errors}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}",
|
||||
"icon": "triangle-exclamation"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{!_local.isSaving}}",
|
||||
"props": {
|
||||
"name": "floppy-disk",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_local.isSaving}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.isSaving ? '$t:sirsoft-board.common.saving' : '$t:sirsoft-board.common.save'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tab_navigation",
|
||||
"type": "composite",
|
||||
"name": "TabNavigationScroll",
|
||||
"props": {
|
||||
"activeTabId": "{{_local.activeTab ?? 'basic'}}",
|
||||
"enableScrollSpy": true,
|
||||
"scrollSpyOffset": 120,
|
||||
"tabs": [
|
||||
{
|
||||
"id": "basic",
|
||||
"label": "$t:sirsoft-board.admin.form.tabs.basic"
|
||||
},
|
||||
{
|
||||
"id": "permissions",
|
||||
"label": "$t:sirsoft-board.admin.form.tabs.permissions"
|
||||
},
|
||||
{
|
||||
"id": "list",
|
||||
"label": "$t:sirsoft-board.admin.form.tabs.list"
|
||||
},
|
||||
{
|
||||
"id": "post",
|
||||
"label": "$t:sirsoft-board.admin.form.tabs.post"
|
||||
},
|
||||
{
|
||||
"id": "notification",
|
||||
"label": "$t:sirsoft-board.admin.form.tabs.notification"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "validation_error",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.errors}}",
|
||||
"props": {
|
||||
"className": "px-6 pt-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"className": "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.validation_error"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{typeof _local.errors === 'string'}}",
|
||||
"props": {
|
||||
"className": "mt-2 text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{_local.errors}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Ul",
|
||||
"if": "{{typeof _local.errors === 'object' && Object.keys(_local.errors).length > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 list-disc list-inside space-y-1"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.errors ?? {}).flatMap(([field, messages]) => Array.isArray(messages) ? messages : [messages])",
|
||||
"item_var": "message"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Li",
|
||||
"props": {
|
||||
"className": "text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{message}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_form",
|
||||
"type": "basic",
|
||||
"name": "Form",
|
||||
"props": {
|
||||
"id": "board_form",
|
||||
"className": "p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"partial": "partials/admin_board_form/_tab_basic.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_form/_tab_permissions.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_form/_tab_list.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_form/_tab_post.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_form/_tab_notification.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"modals": [
|
||||
{
|
||||
"id": "category_remove_modal",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"condition": "{{_local.showCategoryRemoveModal === true}}",
|
||||
"props": {
|
||||
"isOpen": "{{_local.showCategoryRemoveModal}}",
|
||||
"title": "$t:sirsoft-board.admin.form.modals.category_remove.title",
|
||||
"size": "sm"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "close",
|
||||
"handler": "setState",
|
||||
"target": "_local.showCategoryRemoveModal",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": "category_remove_modal_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "category_remove_modal_message",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.modals.category_remove.message"
|
||||
},
|
||||
{
|
||||
"id": "category_remove_modal_warning",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.modals.category_remove.warning"
|
||||
},
|
||||
{
|
||||
"id": "category_remove_modal_actions",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex mt-6 justify-end"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "category_remove_modal_confirm",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 font-medium transition-colors"
|
||||
},
|
||||
"text": "$t:common.confirm",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"target": "_local.showCategoryRemoveModal",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"layout_name": "admin_board_index",
|
||||
"permissions": [
|
||||
"sirsoft-board.boards.read"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "$t:sirsoft-board.admin.board.index.title",
|
||||
"description": "$t:sirsoft-board.admin.board.index.description"
|
||||
},
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "boards",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/boards",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"refetchOnMount": true,
|
||||
"loading_strategy": "progressive",
|
||||
"params": {
|
||||
"page": "{{query.page}}",
|
||||
"per_page": "{{query.per_page ?? 10}}",
|
||||
"search": "{{query.search}}"
|
||||
},
|
||||
"errorHandling": {
|
||||
"403": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"slots": {
|
||||
"content": [
|
||||
{
|
||||
"id": "board_list_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-4 sm:p-6 lg:p-8 bg-gray-50 dark:bg-gray-900"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "page_header",
|
||||
"type": "composite",
|
||||
"name": "PageHeader",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.board.index.title",
|
||||
"description": "$t:sirsoft-board.admin.board.index.description"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "refresh_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800 transition-colors",
|
||||
"aria-label": "$t:common.refresh"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards",
|
||||
"mergeQuery": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": "refresh_icon",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "refresh",
|
||||
"className": "w-5 h-5"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "add_board_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"disabled": "{{boards?.data?.abilities?.can_create !== true}}",
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"target": "/admin/boards/create"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "plus",
|
||||
"className": "w-5 h-5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.board.index.add_board"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "content_area",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "search_area",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "search_bar",
|
||||
"type": "composite",
|
||||
"name": "SearchBar",
|
||||
"props": {
|
||||
"name": "search",
|
||||
"placeholder": "$t:sirsoft-board.admin.board.index.search_placeholder",
|
||||
"value": "{{query.search || ''}}",
|
||||
"className": "w-64"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "submit",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards",
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"page": 1,
|
||||
"search": "{{$event.target.search.value}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boards_card_grid",
|
||||
"type": "composite",
|
||||
"name": "CardGrid",
|
||||
"blur_until_loaded": true,
|
||||
"props": {
|
||||
"data": "{{boards.data.data}}",
|
||||
"gridColumns": 1,
|
||||
"gap": 4,
|
||||
"pagination": true,
|
||||
"serverSidePagination": true,
|
||||
"serverCurrentPage": "{{boards?.data?.pagination?.current_page ?? 1}}",
|
||||
"serverTotalPages": "{{boards?.data?.pagination?.last_page ?? 1}}",
|
||||
"alwaysShowPagination": true,
|
||||
"emptyMessage": "$t:sirsoft-board.admin.board.index.empty_description",
|
||||
"cardClassName": "rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700 p-6 space-y-4",
|
||||
"cardColumns": [
|
||||
{
|
||||
"id": "board_card",
|
||||
"cellChildren": [
|
||||
{
|
||||
"id": "card_header_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start justify-between"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header_left",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_slug_badge",
|
||||
"type": "composite",
|
||||
"name": "StatusBadge",
|
||||
"props": {
|
||||
"status": "info",
|
||||
"label": "{{row.slug}}",
|
||||
"showIcon": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "board_name",
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{row.name}}"
|
||||
},
|
||||
{
|
||||
"id": "board_type",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "({{row.type}})"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "header_right",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "active_toggle",
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"checked": "{{row.is_active}}",
|
||||
"name": "is_active",
|
||||
"size": "sm",
|
||||
"disabled": "{{row.abilities?.can_update !== true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/boards/{{row.id}}",
|
||||
"params": {
|
||||
"method": "PUT",
|
||||
"body": {
|
||||
"is_active": "{{!row.is_active}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "boards",
|
||||
"sync": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "{{row.is_active ? '$t:sirsoft-board.admin.board.messages.deactivated' : '$t:sirsoft-board.admin.board.messages.activated'}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "action_menu",
|
||||
"type": "composite",
|
||||
"name": "ActionMenu",
|
||||
"props": {
|
||||
"triggerLabel": "",
|
||||
"triggerIconName": "ellipsis-h",
|
||||
"position": "right",
|
||||
"items": [
|
||||
{ "id": "edit", "label": "$t:sirsoft-board.admin.board.actions.edit", "iconName": "pencil", "disabled": "{{row.abilities?.can_update !== true}}" },
|
||||
{ "id": "manage_posts", "label": "$t:sirsoft-board.admin.board.actions.manage_posts", "iconName": "list" },
|
||||
{ "id": "copy", "label": "$t:sirsoft-board.admin.board.actions.copy", "iconName": "copy", "disabled": "{{row.abilities?.can_create !== true}}" },
|
||||
{ "id": "delete", "label": "$t:sirsoft-board.admin.board.actions.delete", "iconName": "trash", "variant": "danger", "disabled": "{{row.abilities?.can_delete !== true}}" }
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onItemClick",
|
||||
"handler": "switch",
|
||||
"value": "{{$args[0]}}",
|
||||
"cases": {
|
||||
"edit": {
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/{{row.id}}/edit",
|
||||
"mergeQuery": false,
|
||||
"query": []
|
||||
}
|
||||
},
|
||||
"manage_posts": {
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{row.slug}}"
|
||||
}
|
||||
},
|
||||
"copy": {
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"modal_data": "{{row}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "copy_confirm_modal"
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"modal_data": "{{row}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "delete_confirm_modal"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_meta_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "post_count_label",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-semibold"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board.index.post_count"
|
||||
},
|
||||
{
|
||||
"id": "post_count_value",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{row?.posts_count ?? 0}}"
|
||||
},
|
||||
{
|
||||
"id": "separator",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "mx-1"
|
||||
},
|
||||
"text": "·"
|
||||
},
|
||||
{
|
||||
"id": "created_date_label",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-semibold"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board.index.created_at"
|
||||
},
|
||||
{
|
||||
"id": "created_date_value",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{row?.created_at}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "categories_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{row.categories && row.categories.length > 0}}",
|
||||
"props": {
|
||||
"className": "flex flex-wrap gap-2 items-center"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "category_label",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board.index.categories_label"
|
||||
},
|
||||
{
|
||||
"id": "category_tag",
|
||||
"type": "composite",
|
||||
"name": "StatusBadge",
|
||||
"iteration": {
|
||||
"source": "row.categories",
|
||||
"item_var": "category"
|
||||
},
|
||||
"props": {
|
||||
"status": "default",
|
||||
"label": "{{category}}",
|
||||
"showIcon": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onPageChange",
|
||||
"type": "change",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards",
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"page": "{{$args[0]}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"modals": [
|
||||
{
|
||||
"id": "copy_confirm_modal",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.modals.copy.title",
|
||||
"size": "small"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "copy_modal_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "copy_board_name",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-gray-900 dark:text-white font-medium"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.modals.copy.message|name={{_global.modal_data.name}}"
|
||||
},
|
||||
{
|
||||
"id": "copy_info",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.modals.copy.info"
|
||||
},
|
||||
{
|
||||
"id": "copy_button_group",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "copy_cancel_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 dark:focus:ring-gray-400"
|
||||
},
|
||||
"text": "$t:common.cancel",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal",
|
||||
"target": "copy_confirm_modal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "copy_confirm_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 text-sm font-medium text-white bg-blue-600 dark:bg-blue-500 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board.actions.copy",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal",
|
||||
"target": "copy_confirm_modal"
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"target": "/admin/boards/create?copy_id={{_global.modal_data.id}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "delete_confirm_modal",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.modals.delete.title",
|
||||
"size": "small"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "delete_warning",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "delete_board_name",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-gray-900 dark:text-white font-semibold text-lg"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.modals.delete.confirm|name={{_global.modal_data.name}}"
|
||||
},
|
||||
{
|
||||
"id": "delete_posts_info",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "{{_global.modal_data.posts_count > 0 ? '$t:sirsoft-board.admin.modals.delete.with_posts_info|posts_count=' + _global.modal_data.posts_count : '$t:sirsoft-board.admin.modals.delete.no_posts_info'}}"
|
||||
},
|
||||
{
|
||||
"id": "delete_main_alert",
|
||||
"type": "composite",
|
||||
"name": "Alert",
|
||||
"props": {
|
||||
"type": "{{_global.modal_data.posts_count > 0 ? 'error' : 'warning'}}",
|
||||
"message": "{{_global.modal_data.posts_count > 0 ? '$t:sirsoft-board.admin.modals.delete.force_delete_warning_with_posts' : '$t:sirsoft-board.admin.modals.delete.force_delete_warning_no_posts'}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "delete_backup_notice",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{_global.modal_data.posts_count > 0}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.modals.delete.backup_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-3 mt-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{_global.isDeleting}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{_global.isDeleting}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"isDeleting": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/boards/{{_global.modal_data?.id}}",
|
||||
"params": {
|
||||
"method": "DELETE",
|
||||
"body": []
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"isDeleting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"forceDeleteBoard": false,
|
||||
"modal_data": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "boards",
|
||||
"sync": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "admin_menu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.board.messages.deleted"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "closeModal"
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"isDeleting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_global.isDeleting}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_global.isDeleting ? '$t:sirsoft-board.common.deleting' : '$t:sirsoft-board.common.delete'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"layout_name": "admin_board_post_form",
|
||||
"permissions": [
|
||||
"sirsoft-board.boards.read"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "{{form_meta?.data?.board?.name}} {{route?.id ? '$t:sirsoft-board.admin.posts.form.edit_title' : (query?.parent_id ? '$t:sirsoft-board.admin.posts.reply.title' : '$t:sirsoft-board.admin.posts.form.create_title')}}",
|
||||
"description": "{{form_meta?.data?.board?.name}}$t:sirsoft-board.admin.posts.page_description_with_board"
|
||||
},
|
||||
"state": {
|
||||
"isSaving": false,
|
||||
"hasChanges": false,
|
||||
"previewMode": false,
|
||||
"tempKey": null,
|
||||
"isDataLoaded": false,
|
||||
"errors": null,
|
||||
"uploadingFiles": []
|
||||
},
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "form_data",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/board/{{route.slug}}/posts/form-data{{route?.id ? '?post_id=' + route.id : (query?.parent_id ? '?parent_id=' + query.parent_id : '')}}",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"cache": false,
|
||||
"refetchOnMount": true,
|
||||
"initLocal": "form",
|
||||
"loading_strategy": "blocking",
|
||||
"errorHandling": {
|
||||
"404": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "form_meta",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/board/{{route.slug}}/posts/form-meta{{route?.id ? '?post_id=' + route.id : (query?.parent_id ? '?parent_id=' + query.parent_id : '')}}",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"cache": false,
|
||||
"refetchOnMount": true,
|
||||
"errorHandling": {
|
||||
"404": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"computed": {
|
||||
"isReadOnly": "{{!!route?.id && !!form_data?.data && form_data?.data?.abilities?.can_write !== true}}"
|
||||
},
|
||||
"init_actions": [
|
||||
{
|
||||
"comment": "tempKey 생성 (신규/수정 모두 - 취소 시 안전을 위해)",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"_local": {
|
||||
"tempKey": "{{('temp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9))}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"slots": {
|
||||
"content": [
|
||||
{
|
||||
"id": "post_form_wrapper",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": {
|
||||
"enabled": true,
|
||||
"data_sources": [
|
||||
"form_data",
|
||||
"form_meta"
|
||||
]
|
||||
},
|
||||
"dataKey": "form",
|
||||
"trackChanges": true,
|
||||
"props": {
|
||||
"className": "p-4 sm:p-6 lg:p-8 bg-gray-50 dark:bg-gray-900"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "page_header",
|
||||
"type": "composite",
|
||||
"name": "PageHeader",
|
||||
"props": {
|
||||
"title": "{{form_meta?.data?.board?.name}} {{route?.id ? '$t:sirsoft-board.admin.posts.form.edit_title' : (query?.parent_id ? '$t:sirsoft-board.admin.posts.reply.title' : '$t:sirsoft-board.admin.posts.form.create_title')}}",
|
||||
"description": "{{form_meta?.data?.board?.name}}$t:sirsoft-board.admin.posts.page_description_with_board"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header_cancel_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 font-medium transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "{{route?.id ? '/admin/board/' + route.slug + '/post/' + route.id : '/admin/board/' + route.slug}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "header_save_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"disabled": "{{_computed.isReadOnly || _local.isSaving || (!!route?.id && !_local.hasChanges)}}",
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": true,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "emitEvent",
|
||||
"params": {
|
||||
"event": "upload:board_attachments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "{{route?.id ? '/api/modules/sirsoft-board/admin/board/' + route.slug + '/posts/' + route.id : '/api/modules/sirsoft-board/admin/board/' + route.slug + '/posts'}}",
|
||||
"params": {
|
||||
"method": "{{route?.id ? 'PUT' : 'POST'}}",
|
||||
"body": "{{Object.assign({}, _local.form, { temp_key: _local.tempKey })}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"hasChanges": false,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "{{route?.id ? '$t:sirsoft-board.admin.posts.messages.update_success' : '$t:sirsoft-board.admin.posts.messages.create_success'}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{route.slug}}/post/{{route?.id ?? result?.data?.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"errors": "{{error.errors}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "$t:sirsoft-board.admin.posts.messages.save_failed",
|
||||
"icon": "triangle-exclamation"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.isSaving ? '$t:sirsoft-board.common.saving' : '$t:sirsoft-board.common.save'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "read_only_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_computed.isReadOnly}}",
|
||||
"props": {
|
||||
"className": "mb-4 flex items-center gap-3 rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 px-4 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "lock",
|
||||
"className": "w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-800 dark:text-amber-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.read_only_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "validation_error",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.errors}}",
|
||||
"props": {
|
||||
"className": "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"className": "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.validation_error"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{typeof _local.errors === 'string'}}",
|
||||
"props": {
|
||||
"className": "mt-2 text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{_local.errors}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Ul",
|
||||
"if": "{{typeof _local.errors === 'object' && Object.keys(_local.errors).length > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 list-disc list-inside space-y-1"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.errors ?? {}).flatMap(([field, messages]) => Array.isArray(messages) ? messages : [messages])",
|
||||
"item_var": "message"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Li",
|
||||
"props": {
|
||||
"className": "text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{message}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "post_form",
|
||||
"type": "basic",
|
||||
"name": "Form",
|
||||
"props": {
|
||||
"id": "post_form"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "form_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"partial": "partials/admin_board_post_form/_parent_post.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_post_form/_form_fields.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_post_form/_attachments.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footer_buttons",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-3 pt-4 mt-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "footer_cancel_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 font-medium transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "{{route?.id ? '/admin/board/' + route.slug + '/post/' + route.id : '/admin/board/' + route.slug}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footer_save_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"disabled": "{{_computed.isReadOnly || _local.isSaving || (!!route?.id && !_local.hasChanges)}}",
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": true,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "emitEvent",
|
||||
"params": {
|
||||
"event": "upload:board_attachments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "{{route?.id ? '/api/modules/sirsoft-board/admin/board/' + route.slug + '/posts/' + route.id : '/api/modules/sirsoft-board/admin/board/' + route.slug + '/posts'}}",
|
||||
"params": {
|
||||
"method": "{{route?.id ? 'PUT' : 'POST'}}",
|
||||
"body": "{{Object.assign({}, _local.form, { temp_key: _local.tempKey })}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"hasChanges": false,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "{{route?.id ? '$t:sirsoft-board.admin.posts.messages.update_success' : '$t:sirsoft-board.admin.posts.messages.create_success'}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{route.slug}}/post/{{route?.id ?? result?.data?.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"errors": "{{error.errors}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "$t:sirsoft-board.admin.posts.messages.save_failed",
|
||||
"icon": "triangle-exclamation"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.isSaving ? '$t:sirsoft-board.common.saving' : '$t:sirsoft-board.common.save'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+365
@@ -0,0 +1,365 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"layout_name": "admin_board_reports_detail",
|
||||
"permissions": [
|
||||
"sirsoft-board.reports.view"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "$t:sirsoft-board.admin.reports.detail.page_title",
|
||||
"description": "$t:sirsoft-board.admin.reports.detail.page_description"
|
||||
},
|
||||
"state": {
|
||||
"_local": {
|
||||
"reporters_page": 1,
|
||||
"reporters_loading": false
|
||||
},
|
||||
"_global": {
|
||||
"actionReason": "",
|
||||
"isSaving": false,
|
||||
"actionType": ""
|
||||
}
|
||||
},
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "report_detail",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/reports/{{route.id}}",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"refetchOnMount": true,
|
||||
"errorHandling": {
|
||||
"404": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reporters_list",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/reports/{{route.id}}/reporters",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"loading_strategy": "progressive",
|
||||
"params": {
|
||||
"page": "{{_local.reporters_page}}",
|
||||
"per_page": 10
|
||||
}
|
||||
}
|
||||
],
|
||||
"slots": {
|
||||
"content": [
|
||||
{
|
||||
"id": "report_detail_wrapper",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-6 bg-gray-50 dark:bg-gray-900"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "p-4 bg-gray-50 dark:bg-gray-900"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "page_header",
|
||||
"type": "composite",
|
||||
"name": "PageHeader",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.reports.detail.page_title",
|
||||
"description": "$t:sirsoft-board.admin.reports.detail.page_description"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "refresh_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/reports/{{route.id}}",
|
||||
"mergeQuery": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrows-rotate",
|
||||
"className": "w-5 h-5"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "back_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 font-medium transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/reports"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrow-left",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.back"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_reports_detail/_alert_status.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_reports_detail/_card_report_info.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_reports_detail/_card_history.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"modals": [
|
||||
{
|
||||
"id": "status_action_modal",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "{{_global.actionType === 'pending' ? '$t:sirsoft-board.admin.reports.modals.status_change_pending_title' : _global.actionType === 'review' ? '$t:sirsoft-board.admin.reports.modals.status_change_review_title' : _global.actionType === 'rejected' ? '$t:sirsoft-board.admin.reports.modals.status_change_rejected_title' : _global.actionType === 'deleted' ? '$t:sirsoft-board.admin.reports.modals.status_change_deleted_title' : '$t:sirsoft-board.admin.reports.modals.status_change_suspended_title'}}",
|
||||
"size": "medium"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "py-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-gray-700 dark:text-gray-300 mb-4"
|
||||
},
|
||||
"text": "{{_global.actionType === 'pending' ? '$t:sirsoft-board.admin.reports.modals.status_change_pending_confirm_single' : _global.actionType === 'review' ? '$t:sirsoft-board.admin.reports.modals.status_change_review_confirm_single' : _global.actionType === 'rejected' ? '$t:sirsoft-board.admin.reports.modals.status_change_rejected_confirm_single' : _global.actionType === 'deleted' ? '$t:sirsoft-board.admin.reports.modals.status_change_deleted_confirm_single' : '$t:sirsoft-board.admin.reports.modals.status_change_suspended_confirm_single'}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_global.actionType === 'rejected' && report_detail.data?.target_status === 'blinded'}}",
|
||||
"props": {
|
||||
"className": "mb-4 flex items-center gap-2 p-2.5 bg-orange-50 dark:bg-orange-900/20 rounded-md"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "eye",
|
||||
"className": "w-4 h-4 text-orange-600 dark:text-orange-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-orange-700 dark:text-orange-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.modals.blinded_will_be_public|count=1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.modals.reason_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Textarea",
|
||||
"props": {
|
||||
"className": "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white",
|
||||
"rows": 4,
|
||||
"placeholder": "$t:sirsoft-board.admin.reports.modals.reason_placeholder",
|
||||
"value": "{{_global.actionReason || ''}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionReason": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-3 mt-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{_global.isSaving}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal"
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionReason": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed {{_global.actionType === 'pending' ? 'bg-yellow-600 hover:bg-yellow-700' : _global.actionType === 'review' ? 'bg-blue-600 hover:bg-blue-700' : _global.actionType === 'rejected' ? 'bg-purple-600 hover:bg-purple-700' : _global.actionType === 'suspended' ? 'bg-red-600 hover:bg-red-700' : _global.actionType === 'deleted' ? 'bg-gray-600 hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600' : 'bg-blue-600 hover:bg-blue-700'}}",
|
||||
"disabled": "{{_global.isSaving}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"isSaving": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/reports/{{route.id}}/status",
|
||||
"params": {
|
||||
"method": "PATCH",
|
||||
"body": {
|
||||
"status": "{{_global.actionType === 'pending' ? 'pending' : _global.actionType === 'review' ? 'review' : _global.actionType === 'rejected' ? 'rejected' : _global.actionType === 'deleted' ? 'deleted' : 'suspended'}}",
|
||||
"process_note": "{{_global.actionReason}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionReason": "",
|
||||
"isSaving": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "closeModal"
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.reports.modals.status_change_success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/reports/{{route.id}}",
|
||||
"mergeQuery": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"isSaving": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_global.isSaving ? $t:common.saving : $t:common.confirm}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+2782
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,648 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"layout_name": "admin_board_settings",
|
||||
"permissions": [
|
||||
"sirsoft-board.settings.read"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "$t:sirsoft-board.admin.settings.title",
|
||||
"description": "$t:sirsoft-board.admin.settings.description"
|
||||
},
|
||||
"state": {
|
||||
"mailTemplateCurrentPage": 1,
|
||||
"mailTemplateTotalPages": 1,
|
||||
"mailTemplateTotalItems": 0
|
||||
},
|
||||
"initLocal": {
|
||||
"editingTemplate": null,
|
||||
"editLang": "ko",
|
||||
"previewData": null,
|
||||
"mailTemplateFilter": {
|
||||
"search": "",
|
||||
"searchType": "all"
|
||||
},
|
||||
"expandedMailTemplateIds": []
|
||||
},
|
||||
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "settings",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/settings",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"initLocal": "form",
|
||||
"refetchOnMount": true,
|
||||
"errorHandling": {
|
||||
"403": {
|
||||
"handler": "showErrorPage",
|
||||
"params": {
|
||||
"target": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "boards_list",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/boards?per_page=999&fields=id,title",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": {
|
||||
"data": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roles",
|
||||
"type": "api",
|
||||
"endpoint": "/api/admin/roles/active",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "board_types",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/board-types",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "boardMailTemplates",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/mail-templates",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"params": {
|
||||
"page": "{{query.page || 1}}",
|
||||
"per_page": "{{query.per_page || 20}}",
|
||||
"search": "{{query.search}}",
|
||||
"search_type": "{{query.search_type}}"
|
||||
},
|
||||
"initLocal": {
|
||||
"_merge": "deep",
|
||||
"mailTemplateCurrentPage": "pagination.current_page",
|
||||
"mailTemplateTotalPages": "pagination.last_page",
|
||||
"mailTemplateTotalItems": "pagination.total"
|
||||
},
|
||||
"fallback": {
|
||||
"data": {
|
||||
"data": [],
|
||||
"pagination": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "notification_channels",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/notification-channels",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true
|
||||
}
|
||||
],
|
||||
"init_actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "_local",
|
||||
"mailTemplateCurrentPage": "{{+(query.page || 1)}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"computed": {
|
||||
"isReadOnly": "{{!!settings?.data && settings?.data?.abilities?.can_update !== true}}",
|
||||
"boardOptions": "{{(() => { const boards = boards_list?.data?.data ?? []; return boards.map(b => ({ value: b.id, label: b.name })); })()}}",
|
||||
"adminPermissions": "{{Object.entries(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(([key]) => key.startsWith('admin.'))}}",
|
||||
"userPermissions": "{{Object.entries(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(([key]) => !key.startsWith('admin.'))}}",
|
||||
"roleOptions": "{{(roles?.data?.data ?? []).map(r => ({ value: r.identifier, label: r.name ?? r.identifier }))}}",
|
||||
"adminPermKeys": "{{Object.entries(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(([key]) => key.startsWith('admin.')).map(([key]) => key)}}",
|
||||
"userPermKeys": "{{Object.entries(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(([key]) => !key.startsWith('admin.')).map(([key]) => key)}}",
|
||||
"allAdminChecked": "{{Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => k.startsWith('admin.')).length > 0 && Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => k.startsWith('admin.')).every(k => (_local.bulkApplyFields ?? []).includes(k))}}",
|
||||
"allUserChecked": "{{Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => !k.startsWith('admin.')).length > 0 && Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => !k.startsWith('admin.')).every(k => (_local.bulkApplyFields ?? []).includes(k))}}"
|
||||
},
|
||||
"slots": {
|
||||
"content": [
|
||||
{
|
||||
"id": "settings_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": "{{_local.isSaving}}",
|
||||
"dataKey": "form",
|
||||
"trackChanges": true,
|
||||
"props": {
|
||||
"className": "bg-gray-50 dark:bg-gray-900 min-h-screen"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "sticky_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "sticky top-0 z-40 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-4 sm:px-6 lg:px-8 pt-4 pb-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header_top_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start justify-between gap-4 mb-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "page_header",
|
||||
"type": "composite",
|
||||
"name": "PageHeader",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.settings.title",
|
||||
"description": "$t:sirsoft-board.admin.settings.description"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "header_save_buttons",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') !== 'mail_templates'}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3 flex-shrink-0 pt-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.hasChanges}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1.5 text-sm text-amber-600 dark:text-amber-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-dot",
|
||||
"size": "xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.unsaved_changes"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cancel_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 font-medium transition-colors text-sm"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "save_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"disabled": "{{_computed.isReadOnly || !_local.hasChanges || _local.isSaving}}",
|
||||
"className": "flex items-center gap-1.5 px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": true,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/settings",
|
||||
"params": {
|
||||
"method": "PUT",
|
||||
"body": "{{(function() { const tab = _global.activeBoardSettingsTab || query.tab || 'general'; const form = _local.form || {}; if (tab === 'general') { return { _tab: 'general', display: form.display || {} }; } if (tab === 'report_policy') { return { _tab: 'report_policy', report_policy: form.report_policy || {}, spam_security: form.spam_security || {}, report_permissions: form.report_permissions || {} }; } if (tab === 'spam_security') { return { _tab: 'spam_security', spam_security: form.spam_security || {} }; } return { _tab: 'basic_defaults', basic_defaults: form.basic_defaults || {}, notification_channels: form.notification_channels || {} }; })()}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"hasChanges": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "settings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "boards_list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "roles"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "board_types"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.settings.save_success"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"errors": "{{error.errors}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "$t:sirsoft-board.admin.settings.save_error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_local.isSaving}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"size": "sm",
|
||||
"spin": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!_local.isSaving}}",
|
||||
"text": "$t:common.save"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.isSaving}}",
|
||||
"text": "$t:common.saving"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "read_only_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_computed.isReadOnly}}",
|
||||
"props": {
|
||||
"className": "mb-3 flex items-center gap-3 rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 px-4 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "lock",
|
||||
"size": "lg",
|
||||
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-800 dark:text-amber-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.read_only_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tab_navigation",
|
||||
"type": "composite",
|
||||
"name": "TabNavigation",
|
||||
"props": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "general",
|
||||
"label": "$t:sirsoft-board.admin.settings.tabs.general"
|
||||
},
|
||||
{
|
||||
"id": "basic_defaults",
|
||||
"label": "$t:sirsoft-board.admin.settings.tabs.basic_defaults"
|
||||
},
|
||||
{
|
||||
"id": "report_policy",
|
||||
"label": "$t:sirsoft-board.admin.settings.tabs.report_policy"
|
||||
},
|
||||
{
|
||||
"id": "spam_security",
|
||||
"label": "$t:sirsoft-board.admin.settings.tabs.spam_security"
|
||||
},
|
||||
{
|
||||
"id": "mail_templates",
|
||||
"label": "$t:sirsoft-board.admin.settings.tabs.mail_templates"
|
||||
}
|
||||
],
|
||||
"activeTabId": "{{_global.activeBoardSettingsTab || query.tab || 'general'}}",
|
||||
"variant": "default"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onTabChange",
|
||||
"type": "change",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"activeBoardSettingsTab": "{{$args[0]}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "replaceUrl",
|
||||
"params": {
|
||||
"query": {
|
||||
"tab": "{{$args[0]}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sub_tab_navigation",
|
||||
"type": "composite",
|
||||
"name": "TabNavigationScroll",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "basic",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.basic"
|
||||
},
|
||||
{
|
||||
"id": "permissions",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.permissions"
|
||||
},
|
||||
{
|
||||
"id": "list",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.list"
|
||||
},
|
||||
{
|
||||
"id": "post",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.post"
|
||||
},
|
||||
{
|
||||
"id": "reply",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.reply"
|
||||
},
|
||||
{
|
||||
"id": "comment",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.comment"
|
||||
},
|
||||
{
|
||||
"id": "attachment",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.attachment"
|
||||
},
|
||||
{
|
||||
"id": "notification",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.notification"
|
||||
},
|
||||
{
|
||||
"id": "bulk_apply",
|
||||
"label": "$t:sirsoft-board.admin.settings.board_settings_tabs.bulk_apply"
|
||||
}
|
||||
],
|
||||
"className": "bg-white dark:bg-gray-800 flex gap-1 border-b border-gray-200 dark:border-gray-700 px-6 sm:px-8 lg:px-10",
|
||||
"sectionIdPrefix": "",
|
||||
"enableScrollSpy": true,
|
||||
"scrollContainerId": "right_content_area",
|
||||
"scrollSpyOffset": 180,
|
||||
"scrollOffset": 180
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "main_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-4 sm:p-6 lg:p-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "validation_error",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.errors}}",
|
||||
"props": {
|
||||
"className": "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"size": "lg",
|
||||
"className": "text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.validation_error"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{typeof _local.errors === 'string'}}",
|
||||
"props": {
|
||||
"className": "mt-2 text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{_local.errors}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Ul",
|
||||
"if": "{{typeof _local.errors === 'object' && Object.keys(_local.errors).length > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 list-disc list-inside space-y-1"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.errors ?? {}).flatMap(([field, messages]) => Array.isArray(messages) ? messages : [messages])",
|
||||
"item_var": "message"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Li",
|
||||
"props": {
|
||||
"className": "text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{message}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_general.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_basic.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_permissions.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_list.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_post.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_reply.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_comment.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_attachment.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_notification.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_board_settings_bulk_apply.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_report_policy.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_spam_security.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_tab_mail_templates.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"modals": [
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_bulk_apply_modal.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_form/_board_type_manage_modal.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_modal_mail_template_edit.json"
|
||||
},
|
||||
{
|
||||
"partial": "partials/admin_board_settings/_modal_mail_template_preview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
+746
@@ -0,0 +1,746 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Board Type Management Modal - CRUD for board types"
|
||||
},
|
||||
"id": "board_type_manage_modal",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.board_types.modal_title",
|
||||
"width": "640px",
|
||||
"closeOnBackdropClick": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_modal_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{$parent._computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_usage_guide",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-xs text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg px-3 py-2.5"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_usage_guide_icon",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fa-solid fa-circle-info",
|
||||
"className": "mt-0.5 shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bt_usage_guide_text",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "leading-relaxed"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.usage_guide"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_list_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "max-h-64 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_table",
|
||||
"type": "basic",
|
||||
"name": "Table",
|
||||
"props": {
|
||||
"className": "min-w-full divide-y divide-gray-200 dark:divide-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_thead",
|
||||
"type": "basic",
|
||||
"name": "Thead",
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_thead_row",
|
||||
"type": "basic",
|
||||
"name": "Tr",
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_th_slug",
|
||||
"type": "basic",
|
||||
"name": "Th",
|
||||
"props": {
|
||||
"className": "px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.slug"
|
||||
},
|
||||
{
|
||||
"id": "bt_th_name",
|
||||
"type": "basic",
|
||||
"name": "Th",
|
||||
"props": {
|
||||
"className": "px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.name"
|
||||
},
|
||||
{
|
||||
"id": "bt_th_actions",
|
||||
"type": "basic",
|
||||
"name": "Th",
|
||||
"props": {
|
||||
"className": "px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.actions"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_tbody",
|
||||
"type": "basic",
|
||||
"name": "Tbody",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_empty_row",
|
||||
"type": "basic",
|
||||
"name": "Tr",
|
||||
"if": "{{!_global.boardTypesList || _global.boardTypesList.length === 0}}",
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_empty_cell",
|
||||
"type": "basic",
|
||||
"name": "Td",
|
||||
"props": {
|
||||
"colSpan": 3,
|
||||
"className": "px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.empty"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_row",
|
||||
"type": "basic",
|
||||
"name": "Tr",
|
||||
"if": "{{_global.boardTypesList && _global.boardTypesList.length > 0}}",
|
||||
"iteration": {
|
||||
"source": "{{_global.boardTypesList ?? []}}",
|
||||
"item_var": "btItem",
|
||||
"index_var": "btIdx"
|
||||
},
|
||||
"props": {
|
||||
"className": "hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50 transition-colors duration-150"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_global.btEditingItem?.id === btItem.id ? 'true' : 'false'}}",
|
||||
"variants": {
|
||||
"true": "bg-blue-50 dark:bg-blue-900/20",
|
||||
"false": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_td_slug",
|
||||
"type": "basic",
|
||||
"name": "Td",
|
||||
"props": {
|
||||
"className": "px-3 py-2 text-sm text-gray-900 dark:text-gray-100 font-mono"
|
||||
},
|
||||
"text": "{{btItem.slug}}"
|
||||
},
|
||||
{
|
||||
"id": "bt_td_name",
|
||||
"type": "basic",
|
||||
"name": "Td",
|
||||
"props": {
|
||||
"className": "px-3 py-2 text-sm text-gray-900 dark:text-gray-100"
|
||||
},
|
||||
"text": "{{btItem.name[_global.locale ?? 'ko'] ?? btItem.name.ko ?? btItem.slug}}"
|
||||
},
|
||||
{
|
||||
"id": "bt_td_actions",
|
||||
"type": "basic",
|
||||
"name": "Td",
|
||||
"props": {
|
||||
"className": "px-3 py-2 text-center"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_action_btns",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-center gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_edit_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "p-1 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-400",
|
||||
"title": "$t:common.edit",
|
||||
"disabled": "{{_computed.isReadOnly || _global.btSaving}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btEditingItem": "{{btItem}}",
|
||||
"btFormMode": "edit",
|
||||
"btFormSlug": "{{btItem.slug}}",
|
||||
"btFormName": "{{typeof btItem.name === 'object' ? btItem.name : {ko: btItem.name ?? '', en: ''}}}",
|
||||
"btFormErrors": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_edit_icon",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "pencil",
|
||||
"size": "sm"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_delete_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "p-1 text-gray-500 dark:text-gray-400 hover:text-red-600 dark:text-red-400 dark:hover:text-red-400 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"title": "$t:common.delete",
|
||||
"disabled": "{{_global.btDeletingId != null || _global.btSaving}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btDeletingId": "{{btItem.id}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board-types/{{btItem.id}}",
|
||||
"params": {
|
||||
"method": "DELETE"
|
||||
},
|
||||
"confirm": "$t:sirsoft-board.admin.board_types.delete_confirm",
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btDeletingId": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board-types",
|
||||
"params": {
|
||||
"method": "GET"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"boardTypesList": "{{response.data}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "boards"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "board_types"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.board_types.delete_success"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btDeletingId": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.errors ? Object.values(error.errors).flat()[0] : error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_delete_icon",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_global.btDeletingId !== btItem.id}}",
|
||||
"props": {
|
||||
"name": "trash",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bt_delete_spinner",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_global.btDeletingId === btItem.id}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
}
|
||||
],
|
||||
"if": "{{!_computed.isReadOnly}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_form_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border-t border-gray-200 dark:border-gray-700 pt-4 rounded-lg transition-all duration-200"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_global.btFormMode}}",
|
||||
"variants": {
|
||||
"edit": "ring-2 ring-blue-300 dark:ring-blue-600 bg-blue-50/30 dark:bg-blue-900/10 p-4 -mx-1",
|
||||
"add": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_form_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between mb-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_form_title",
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-sm font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{_global.btFormMode === 'edit' ? '$t:sirsoft-board.admin.board_types.edit_title' : '$t:sirsoft-board.admin.board_types.add_title'}}"
|
||||
},
|
||||
{
|
||||
"id": "bt_form_cancel_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{_global.btFormMode === 'edit'}}",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "px-2.5 py-1 text-xs font-medium text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
},
|
||||
"text": "$t:common.cancel",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btFormMode": "add",
|
||||
"btEditingItem": null,
|
||||
"btFormSlug": "",
|
||||
"btFormName": {
|
||||
"ko": "",
|
||||
"en": ""
|
||||
},
|
||||
"btFormErrors": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_form_fields",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_field_slug",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_label_slug",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.slug"
|
||||
},
|
||||
{
|
||||
"id": "bt_input_slug",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"value": "{{_global.btFormSlug ?? ''}}",
|
||||
"placeholder": "basic",
|
||||
"className": "w-full px-2 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500",
|
||||
"disabled": "{{_computed.isReadOnly || _global.btFormMode === 'edit'}}"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_global.btFormErrors?.slug ? 'error' : 'normal'}}",
|
||||
"variants": {
|
||||
"error": "border-red-500 dark:border-red-400",
|
||||
"normal": "border-gray-300 dark:border-gray-600"
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "input",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btFormSlug": "{{$event.target.value}}",
|
||||
"btFormErrors": "{{(() => { const e = {...(_global.btFormErrors ?? {})}; delete e.slug; return Object.keys(e).length ? e : null; })()}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_slug_hint",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{_global.btFormMode !== 'edit' && !_global.btFormErrors?.slug}}",
|
||||
"props": {
|
||||
"className": "mt-1 text-xs text-gray-400 dark:text-gray-500"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.slug_hint"
|
||||
},
|
||||
{
|
||||
"id": "bt_slug_error",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{_global.btFormErrors?.slug}}",
|
||||
"props": {
|
||||
"className": "mt-1 text-xs text-red-500 dark:text-red-400"
|
||||
},
|
||||
"text": "{{_global.btFormErrors?.slug?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_field_name",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_label_name",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.board_types.name"
|
||||
},
|
||||
{
|
||||
"id": "bt_input_name",
|
||||
"type": "composite",
|
||||
"name": "MultilingualInput",
|
||||
"props": {
|
||||
"value": "{{_global.btFormName ?? {ko: '', en: ''}}}",
|
||||
"layout": "tabs",
|
||||
"name": "bt_form_name",
|
||||
"placeholder": "$t:sirsoft-board.admin.board_types.name_placeholder",
|
||||
"maxLength": 100,
|
||||
"required": true,
|
||||
"error": "{{_global.btFormErrors?.['name.ko']?.[0] ?? ''}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onChange",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btFormName": "{{$args[0]?.target?.value ?? _global.btFormName}}",
|
||||
"btFormErrors": "{{(() => { const e = {...(_global.btFormErrors ?? {})}; delete e['name.ko']; delete e['name']; return Object.keys(e).length ? e : null; })()}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_name_error",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{_global.btFormErrors?.['name.ko'] || _global.btFormErrors?.['name']}}",
|
||||
"props": {
|
||||
"className": "mt-1 text-xs text-red-500 dark:text-red-400"
|
||||
},
|
||||
"text": "{{_global.btFormErrors?.['name.ko']?.[0] ?? _global.btFormErrors?.['name']?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_form_submit",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end mt-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_submit_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{_computed.isReadOnly || _global.btSaving || !_global.btFormSlug || !_global.btFormName?.ko}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btSaving": true,
|
||||
"btFormErrors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "{{_global.btFormMode === 'edit' ? '/api/modules/sirsoft-board/admin/board-types/' + _global.btEditingItem.id : '/api/modules/sirsoft-board/admin/board-types'}}",
|
||||
"params": {
|
||||
"method": "{{_global.btFormMode === 'edit' ? 'PUT' : 'POST'}}",
|
||||
"body": {
|
||||
"slug": "{{_global.btFormSlug}}",
|
||||
"name": "{{_global.btFormName ?? {ko: '', en: ''}}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btSaving": false,
|
||||
"btFormMode": "add",
|
||||
"btEditingItem": null,
|
||||
"btFormSlug": "",
|
||||
"btFormName": {
|
||||
"ko": "",
|
||||
"en": ""
|
||||
},
|
||||
"btFormErrors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board-types",
|
||||
"params": {
|
||||
"method": "GET"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"boardTypesList": "{{response.data}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "boards"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "board_types"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "{{_global.btFormMode === 'edit' ? '$t:sirsoft-board.admin.board_types.update_success' : '$t:sirsoft-board.admin.board_types.create_success'}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btSaving": false,
|
||||
"btFormErrors": "{{error.errors ?? null}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.errors ? Object.values(error.errors).flat()[0] : error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_submit_spinner",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_global.btSaving}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bt_submit_text",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_global.btFormMode === 'edit' ? (_global.btSaving ? '$t:common.saving' : '$t:common.save') : (_global.btSaving ? '$t:sirsoft-board.admin.board_types.adding' : '$t:sirsoft-board.admin.board_types.add')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bt_modal_footer",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end mt-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_modal_close_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btFormMode": "add",
|
||||
"btEditingItem": null,
|
||||
"btFormSlug": "",
|
||||
"btFormName": {
|
||||
"ko": "",
|
||||
"en": ""
|
||||
},
|
||||
"btFormErrors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "closeModal"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"id": "bt_close_text",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.close"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+713
@@ -0,0 +1,713 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Board Form - Basic Tab Content"
|
||||
},
|
||||
"id": "tab_content_basic",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"id": "tab_content_basic"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_basic_card",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"background": "white",
|
||||
"border": true,
|
||||
"shadow": "md",
|
||||
"rounded": true,
|
||||
"padding": "xl",
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_basic_settings",
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-6 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.tabs.basic"
|
||||
},
|
||||
{
|
||||
"id": "field_is_active",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-between items-start mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.is_active"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.is_active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "is_active",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "flex-shrink-0 ml-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_slug",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_slug_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"required": true,
|
||||
"className": "block text-sm font-medium text-gray-900 dark:text-white mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.slug"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 ml-1"
|
||||
},
|
||||
"text": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_slug_input",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.placeholders.slug",
|
||||
"required": true,
|
||||
"disabled": "{{!!route?.id || _computed.isReadOnly}}",
|
||||
"className": "w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 {{!!route?.id || _computed.isReadOnly ? 'border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 cursor-not-allowed' : 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.slug}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.slug?.[0] ?? ''}}"
|
||||
},
|
||||
{
|
||||
"id": "field_slug_description",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.slug"
|
||||
},
|
||||
{
|
||||
"id": "field_menu_guide",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mt-4 p-4 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 mb-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "w-4 h-4 text-gray-600 dark:text-gray-400 mt-0.5 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.menu.manual_guide"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 text-xs text-gray-600 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-baseline gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "•"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-medium"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.menu.manual_admin_label:"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "ml-2 px-2 py-0.5 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded text-gray-800 dark:text-gray-200 font-mono"
|
||||
},
|
||||
"text": "/admin/board/{{_local.form?.slug ?? 'notice'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{!!route?.id}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-blue-500 dark:hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "plus",
|
||||
"className": "w-3 h-3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.menu.add_to_admin_menu"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "apiCall",
|
||||
"target": "/api/modules/sirsoft-board/admin/boards/{{route?.id}}/add-to-menu",
|
||||
"params": {
|
||||
"method": "POST"
|
||||
},
|
||||
"auth_required": true,
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "{{result.message}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "admin_menu",
|
||||
"sync": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-baseline gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "•"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-medium"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.menu.manual_user_label:"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "ml-2 px-2 py-0.5 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded text-gray-800 dark:text-gray-200 font-mono"
|
||||
},
|
||||
"text": "/board/{{_local.form?.slug ?? 'notice'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_name",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_name_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"required": true,
|
||||
"className": "block text-sm font-medium text-gray-900 dark:text-white mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.name"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 ml-1"
|
||||
},
|
||||
"text": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_name_input",
|
||||
"type": "composite",
|
||||
"name": "MultilingualInput",
|
||||
"props": {
|
||||
"name": "name",
|
||||
"layout": "tabs",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.placeholders.name",
|
||||
"maxLength": 100,
|
||||
"required": true,
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.name}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.name?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_description",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_description_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-900 dark:text-white mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_description_input",
|
||||
"type": "composite",
|
||||
"name": "MultilingualInput",
|
||||
"props": {
|
||||
"name": "description",
|
||||
"layout": "tabs",
|
||||
"inputType": "textarea",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.placeholders.description",
|
||||
"rows": 3,
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.description}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.description?.[0] ?? ''}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_type",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_type_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"required": true,
|
||||
"className": "block text-sm font-medium text-gray-900 dark:text-white mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.type"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_type_select",
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "type",
|
||||
"options": "{{boards?.data?.board_types?.map(t => ({value: t.slug, label: (t.name[_global.locale ?? 'ko'] ?? t.name.ko ?? t.slug) + ' (' + t.slug + ')'})) ?? []}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.placeholders.select_type",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "w-full"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.type"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_categories",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6 pb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_categories_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.categories"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_categories_description",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.categories"
|
||||
},
|
||||
{
|
||||
"id": "field_categories_input",
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"name": "categories",
|
||||
"options": "{{_local.form?.category_options ?? []}}",
|
||||
"creatable": true,
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "before_remove",
|
||||
"handler": "check_category_usage",
|
||||
"on_blocked": {
|
||||
"handler": "setState",
|
||||
"target": "_local.showCategoryRemoveModal",
|
||||
"value": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.categories}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.categories?.[0] ?? ''}}"
|
||||
},
|
||||
{
|
||||
"id": "field_categories_notice",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-amber-600 dark:text-amber-400 mt-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.categories_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_secret_mode",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex pt-4 justify-between items-center"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_secret_mode_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.secret_mode"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_secret_mode_select",
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "secret_mode",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"options": [
|
||||
{
|
||||
"value": "disabled",
|
||||
"label": "$t:sirsoft-board.admin.form.options.secret_mode.disabled"
|
||||
},
|
||||
{
|
||||
"value": "enabled",
|
||||
"label": "$t:sirsoft-board.admin.form.options.secret_mode.enabled"
|
||||
},
|
||||
{
|
||||
"value": "always",
|
||||
"label": "$t:sirsoft-board.admin.form.options.secret_mode.always"
|
||||
}
|
||||
],
|
||||
"className": "w-48"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_show_view_count",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex pt-4 justify-between items-start"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.show_view_count"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.show_view_count"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "show_view_count",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "flex-shrink-0 ml-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_use_report",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex pt-4 justify-between items-start"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.use_report"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.use_report"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "use_report",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "flex-shrink-0 ml-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Board Form - List Tab Content"
|
||||
},
|
||||
"id": "tab_content_list",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"id": "tab_content_list"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_list_card",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"background": "white",
|
||||
"border": true,
|
||||
"shadow": "md",
|
||||
"rounded": true,
|
||||
"padding": "xl",
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_list",
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-6 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.tabs.list"
|
||||
},
|
||||
{
|
||||
"id": "list_sort_group",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid mb-6 pb-8 grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "order_by_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": ""
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "order_by_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.order_by.label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "order_by_select",
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "order_by",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "w-full",
|
||||
"options": [
|
||||
{
|
||||
"value": "created_at",
|
||||
"label": "$t:sirsoft-board.admin.form.fields.order_by.options.created_at"
|
||||
},
|
||||
{
|
||||
"value": "view_count",
|
||||
"label": "$t:sirsoft-board.admin.form.fields.order_by.options.view_count"
|
||||
},
|
||||
{
|
||||
"value": "title",
|
||||
"label": "$t:sirsoft-board.admin.form.fields.order_by.options.title"
|
||||
},
|
||||
{
|
||||
"value": "author",
|
||||
"label": "$t:sirsoft-board.admin.form.fields.order_by.options.author"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "order_direction_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": ""
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "order_direction_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.order_direction.label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "order_direction_select",
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "order_direction",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "w-full",
|
||||
"options": [
|
||||
{
|
||||
"value": "DESC",
|
||||
"label": "$t:sirsoft-board.admin.form.fields.order_direction.options.desc"
|
||||
},
|
||||
{
|
||||
"value": "ASC",
|
||||
"label": "$t:sirsoft-board.admin.form.fields.order_direction.options.asc"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "list_per_page_group",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "per_page_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": ""
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "per_page_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.per_page.label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "per_page_input",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "number",
|
||||
"name": "per_page",
|
||||
"required": true,
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "per_page_hint",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "mt-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.per_page.hint|min={{board_config?.data?.limits?.per_page_min ?? 5}}|max={{board_config?.data?.limits?.per_page_max ?? 100}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.per_page}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.per_page?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "per_page_mobile_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": ""
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "per_page_mobile_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.fields.per_page_mobile.label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "per_page_mobile_input",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "number",
|
||||
"name": "per_page_mobile",
|
||||
"required": true,
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "per_page_mobile_hint",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "mt-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.per_page_mobile.hint|min={{board_config?.data?.limits?.per_page_min ?? 5}}|max={{board_config?.data?.limits?.per_page_max ?? 100}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.per_page_mobile}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.per_page_mobile?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Board Form - Notification Tab Content"
|
||||
},
|
||||
"id": "tab_content_notification",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"id": "tab_content_notification"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_notification_card",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"background": "white",
|
||||
"border": true,
|
||||
"shadow": "md",
|
||||
"rounded": true,
|
||||
"padding": "xl",
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_notification",
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.tabs.notification"
|
||||
},
|
||||
{
|
||||
"id": "notification_settings_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "작성자 댓글 알림",
|
||||
"id": "notify_author_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-between items-start py-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.notify_author.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.notify_author.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "notify_author",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "flex-shrink-0 ml-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "notify_author_channels_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3 pb-4 ml-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-400 flex-shrink-0"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.notification_channels.channels_label"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"name": "notify_author_channels",
|
||||
"options": "{{notification_channels?.data ?? []}}",
|
||||
"className": "w-64 flex-shrink-0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "신규 게시글 관리자 알림",
|
||||
"id": "notify_admin_on_post_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-between items-start py-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.notify_admin_on_post.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.notify_admin_on_post.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "notify_admin_on_post",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "flex-shrink-0 ml-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "notify_admin_channels_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3 pb-4 ml-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-400 flex-shrink-0"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.notification_channels.channels_label"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"name": "notify_admin_on_post_channels",
|
||||
"options": "{{notification_channels?.data ?? []}}",
|
||||
"className": "w-64 flex-shrink-0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+725
@@ -0,0 +1,725 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Board Form - Permissions Tab Content"
|
||||
},
|
||||
"id": "tab_content_permissions",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"id": "tab_content_permissions"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_permissions_card",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"background": "white",
|
||||
"border": true,
|
||||
"shadow": "md",
|
||||
"rounded": true,
|
||||
"padding": "xl",
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_permissions_header",
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-6 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.tabs.permissions"
|
||||
},
|
||||
{
|
||||
"id": "permissions_section_managers",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "permissions_managers_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.managers_title"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_managers_description",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.managers_description"
|
||||
},
|
||||
{
|
||||
"id": "permissions_managers_taginput",
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"value": "{{_local.form?.board_manager_ids ?? []}}",
|
||||
"options": "{{ [...(_local.form?.board_managers ?? []), ...(_local.managerSearchResults ?? [])].filter((u, i, a) => a.findIndex(x => x.uuid === u.uuid) === i).map(u => ({ value: u.uuid, label: u.name + ' (' + u.email + ')' })) }}",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.permissions.search_placeholder",
|
||||
"noOptionsMessage": "$t:sirsoft-board.admin.form.permissions.no_search_results",
|
||||
"className": "w-full",
|
||||
"defaultVariant": "blue",
|
||||
"isSearchable": true,
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onInputChange",
|
||||
"handler": "apiCall",
|
||||
"debounce": 300,
|
||||
"auth_required": true,
|
||||
"target": "/api/admin/users",
|
||||
"params": {
|
||||
"method": "GET",
|
||||
"body": {
|
||||
"filters[0][field]": "all",
|
||||
"filters[0][value]": "{{$event.target.value}}",
|
||||
"filters[0][operator]": "like",
|
||||
"per_page": 20
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"managerSearchResults": "{{result.data?.data ?? []}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"managerSearchResults": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"hasChanges": true,
|
||||
"managerSearchResults": [],
|
||||
"form": {
|
||||
"board_manager_ids": "{{$event.target.value ?? []}}",
|
||||
"board_managers": "{{ [...(_local.form?.board_managers ?? []), ...(_local.managerSearchResults ?? [])].filter(u => ($event.target.value ?? []).includes(u.uuid)).filter((u, i, a) => a.findIndex(x => x.uuid === u.uuid) === i) }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{ !!_local.errors?.board_manager_ids }}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1 block"
|
||||
},
|
||||
"text": "{{ _local.errors?.board_manager_ids?.[0] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_section_steps",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "permissions_steps_label",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.steps_title"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_steps_description",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.steps_description"
|
||||
},
|
||||
{
|
||||
"id": "permissions_steps_taginput",
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"value": "{{_local.form?.board_step_ids ?? []}}",
|
||||
"options": "{{ [...(_local.form?.board_steps ?? []), ...(_local.stepSearchResults ?? [])].filter((u, i, a) => a.findIndex(x => x.uuid === u.uuid) === i).map(u => ({ value: u.uuid, label: u.name + ' (' + u.email + ')' })) }}",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.permissions.search_placeholder",
|
||||
"noOptionsMessage": "$t:sirsoft-board.admin.form.permissions.no_search_results",
|
||||
"className": "w-full",
|
||||
"defaultVariant": "green",
|
||||
"isSearchable": true,
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onInputChange",
|
||||
"handler": "apiCall",
|
||||
"debounce": 300,
|
||||
"auth_required": true,
|
||||
"target": "/api/admin/users",
|
||||
"params": {
|
||||
"method": "GET",
|
||||
"body": {
|
||||
"filters[0][field]": "all",
|
||||
"filters[0][value]": "{{$event.target.value}}",
|
||||
"filters[0][operator]": "like",
|
||||
"per_page": 20
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"stepSearchResults": "{{result.data?.data ?? []}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"stepSearchResults": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"hasChanges": true,
|
||||
"stepSearchResults": [],
|
||||
"form": {
|
||||
"board_step_ids": "{{$event.target.value ?? []}}",
|
||||
"board_steps": "{{ [...(_local.form?.board_steps ?? []), ...(_local.stepSearchResults ?? [])].filter(u => ($event.target.value ?? []).includes(u.uuid)).filter((u, i, a) => a.findIndex(x => x.uuid === u.uuid) === i) }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{ !!_local.errors?.board_step_ids }}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1 block"
|
||||
},
|
||||
"text": "{{ _local.errors?.board_step_ids?.[0] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_role_access_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "pt-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H4",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.settings_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.settings_description"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{ !!route?.id }}",
|
||||
"props": {
|
||||
"className": "text-xs text-blue-600 dark:text-blue-400 mt-2 flex items-center gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "info-circle",
|
||||
"size": "sm",
|
||||
"className": "text-blue-500 dark:text-blue-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.auto_role_notice"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_create_mode_notice",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{ !route?.id }}",
|
||||
"props": {
|
||||
"className": "mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "info-circle",
|
||||
"size": "lg",
|
||||
"className": "text-blue-500 dark:text-blue-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-blue-700 dark:text-blue-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.create_mode_permissions_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_error_alert",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{ !!route?.id && (_local.errors?.permissions || Object.keys(_local.errors ?? {}).some(k => k.startsWith('permissions.'))) }}",
|
||||
"props": {
|
||||
"className": "mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "exclamation-circle",
|
||||
"size": "lg",
|
||||
"className": "text-red-500 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.error_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-red-700 dark:text-red-300 mt-1"
|
||||
},
|
||||
"text": "{{ _local.errors?.permissions?.[0] ?? '$t:sirsoft-board.admin.form.permissions.error_description' }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_section_admin",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{ !!route?.id }}",
|
||||
"props": {
|
||||
"className": "mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Accordion",
|
||||
"props": {
|
||||
"defaultOpen": true,
|
||||
"className": "border border-gray-200 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"slot": "trigger",
|
||||
"className": "flex items-center gap-2 w-full px-2 py-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "shield",
|
||||
"size": "lg",
|
||||
"className": "text-purple-500 dark:text-purple-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H4",
|
||||
"props": {
|
||||
"className": "font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.admin_permissions_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-down",
|
||||
"size": "sm",
|
||||
"className": "text-gray-400 dark:text-gray-500 ml-auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"slot": "content",
|
||||
"className": "px-4 py-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.admin_permissions_description"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.form?.permissions !== undefined}}",
|
||||
"props": {
|
||||
"className": "space-y-2 grid grid-cols-1 md:grid-cols-2 md:gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.form?.permissions ?? {}).filter(([key]) => key.startsWith('admin')).map(([key, val]) => ({ ...val, _key: key }))",
|
||||
"item_var": "perm"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "admin_perm_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"key": "{{perm?._key}}",
|
||||
"props": {
|
||||
"className": "{{ _local.errors?.['permissions.' + perm?._key + '.roles'] ? 'bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2 border border-red-300 dark:border-red-700' : 'bg-gray-50 dark:bg-gray-700 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-600' }}"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.name}}_desc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"name": "permissions.{{perm?._key}}.roles",
|
||||
"value": "{{ _local.form?.permissions?.[perm?._key]?.roles ?? [] }}",
|
||||
"options": "{{ (roles?.data?.data ?? []).map(r => ({ value: r?.identifier, label: r?.name })) }}",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.permissions.select_roles",
|
||||
"className": "w-full",
|
||||
"defaultVariant": "purple",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"hasChanges": "{{ route?.id ? true : _local.hasChanges }}",
|
||||
"form": {
|
||||
"permissions": "{{ Object.assign({}, _local.form?.permissions ?? {}, { [perm?._key]: { ..._local.form?.permissions?.[perm?._key], roles: $event.target.value } }) }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{ _local.errors?.['permissions.' + perm?._key + '.roles'] }}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1 block"
|
||||
},
|
||||
"text": "{{ _local.errors?.['permissions.' + perm?._key + '.roles']?.[0] ?? '$t:sirsoft-board.admin.form.permissions.error_roles_required' }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_section_user",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{ !!route?.id }}",
|
||||
"props": {
|
||||
"className": "mt-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Accordion",
|
||||
"props": {
|
||||
"defaultOpen": true,
|
||||
"className": "border border-gray-200 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"slot": "trigger",
|
||||
"className": "flex items-center gap-2 w-full px-2 py-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "users",
|
||||
"size": "lg",
|
||||
"className": "text-blue-500 dark:text-blue-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H4",
|
||||
"props": {
|
||||
"className": "font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.user_permissions_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-down",
|
||||
"size": "sm",
|
||||
"className": "text-gray-400 dark:text-gray-500 ml-auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"slot": "content",
|
||||
"className": "px-4 py-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.permissions.user_permissions_description"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.form?.permissions !== undefined}}",
|
||||
"props": {
|
||||
"className": "space-y-2 grid grid-cols-1 md:grid-cols-2 md:gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.form?.permissions ?? {}).filter(([key]) => !key.startsWith('admin')).map(([key, val]) => ({ ...val, _key: key }))",
|
||||
"item_var": "perm"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "user_perm_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"key": "{{perm?._key}}",
|
||||
"props": {
|
||||
"className": "{{ _local.errors?.['permissions.' + perm?._key + '.roles'] ? 'bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2 border border-red-300 dark:border-red-700' : 'bg-gray-50 dark:bg-gray-700 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-600' }}"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.name}}_desc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"name": "permissions.{{perm?._key}}.roles",
|
||||
"value": "{{ _local.form?.permissions?.[perm?._key]?.roles ?? [] }}",
|
||||
"options": "{{ (roles?.data?.data ?? []).map(r => ({ value: r?.identifier, label: r?.name })) }}",
|
||||
"placeholder": "$t:sirsoft-board.admin.form.permissions.select_roles",
|
||||
"className": "w-full",
|
||||
"defaultVariant": "blue",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"hasChanges": "{{ route?.id ? true : _local.hasChanges }}",
|
||||
"form": {
|
||||
"permissions": "{{ Object.assign({}, _local.form?.permissions ?? {}, { [perm?._key]: { ..._local.form?.permissions?.[perm?._key], roles: $event.target.value } }) }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{ _local.errors?.['permissions.' + perm?._key + '.roles'] }}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1 block"
|
||||
},
|
||||
"text": "{{ _local.errors?.['permissions.' + perm?._key + '.roles']?.[0] ?? '$t:sirsoft-board.admin.form.permissions.error_roles_required' }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+1041
File diff suppressed because it is too large.
Load diff
+732
@@ -0,0 +1,732 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Admin Board Post Detail - Single Comment Component (Enhanced Design)"
|
||||
},
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"id": "comment_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start justify-between gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "comment_left_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 flex-1 min-w-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "depth_indicator",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-gray-400 dark:text-gray-500 flex-shrink-0 text-sm"
|
||||
},
|
||||
"text": "↳",
|
||||
"if": "{{comment.depth > 0}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-1 flex-1 min-w-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "author_name_menu",
|
||||
"type": "composite",
|
||||
"name": "ActionMenu",
|
||||
"if": "{{!comment?.author?.is_guest}}",
|
||||
"props": {
|
||||
"triggerLabel": "{{comment?.author?.name}}",
|
||||
"triggerIconName": "",
|
||||
"triggerClassName": "font-semibold text-gray-900 dark:text-white",
|
||||
"position": "right",
|
||||
"items": [
|
||||
{
|
||||
"id": "view_user_info",
|
||||
"label": "$t:sirsoft-board.admin.posts.detail.user_info",
|
||||
"iconName": "user"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onItemClick",
|
||||
"type": "click",
|
||||
"handler": "switch",
|
||||
"value": "{{$args[0]}}",
|
||||
"cases": {
|
||||
"view_user_info": {
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/users/{{comment?.author?.uuid}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{comment?.author?.is_guest}}",
|
||||
"props": {
|
||||
"className": "text-sm font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{comment?.author?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "({{comment?.author?.email}})",
|
||||
"if": "{{comment?.author?.email}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.guest",
|
||||
"if": "{{comment?.author.is_guest}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.admin_badge",
|
||||
"if": "{{comment?.author.is_admin}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "· {{comment?.created_at}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"if": "{{comment?.ip_address}}",
|
||||
"text": "· IP: {{comment?.ip_address}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "comment_right_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center flex-shrink-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "reply_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{post?.data?.board?.use_reply && (comment?.depth ?? 0) < (post?.data?.board?.max_comment_depth ?? 10) && comment?.status !== 'blinded' && comment?.status !== 'deleted' && !comment?.deleted_at && post?.data?.status !== 'blinded' && post?.data?.status !== 'deleted' && !post?.data?.deleted_at}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{post?.data?.abilities?.can_write_comments !== true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"replyingTo": "{{_local.replyingTo === comment?.id ? null : comment?.id}}",
|
||||
"editingComment": null,
|
||||
"replySubmitting": false,
|
||||
"reply_comment_content": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "comment",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.reply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "신고 버튼 — 미신고 상태 (모달 오픈)",
|
||||
"id": "report_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{post?.data?.board?.use_report && comment?.author?.uuid !== current_user?.data?.uuid && comment?.status !== 'blinded' && comment?.status !== 'deleted' && !comment?.deleted_at && !comment?.is_already_reported}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"modal_data": {
|
||||
"id": "{{comment?.id}}",
|
||||
"type": "comment",
|
||||
"author": "{{comment?.author?.name}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "report_modal"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "flag",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.report"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "신고 버튼 — 이미 신고한 상태 (클릭 불가)",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{post?.data?.board?.use_report && comment?.author?.uuid !== current_user?.data?.uuid && comment?.status !== 'blinded' && comment?.status !== 'deleted' && !comment?.deleted_at && comment?.is_already_reported}}",
|
||||
"props": {
|
||||
"disabled": true,
|
||||
"className": "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-400 dark:text-gray-500 rounded cursor-not-allowed opacity-60"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "flag",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.reported"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "comment_action_menu",
|
||||
"type": "composite",
|
||||
"name": "ActionMenu",
|
||||
"props": {
|
||||
"triggerLabel": "",
|
||||
"triggerIconName": "ellipsis-vertical",
|
||||
"position": "right",
|
||||
"items": "{{comment?.deleted_at ? [{id: 'restore', label: '$t:sirsoft-board.admin.posts.actions.restore', iconName: 'arrows-rotate', variant: 'success', disabled: comment?.abilities?.can_manage !== true}] : [{id: 'edit', label: '$t:sirsoft-board.admin.posts.detail.edit', iconName: 'pencil', disabled: !((comment?.is_author && comment?.abilities?.can_write_comments) || comment?.abilities?.can_manage)}, ...((comment?.status === 'published') ? [{id: 'blind', label: '$t:sirsoft-board.admin.posts.actions.blind', iconName: 'eye-slash', variant: 'warning', disabled: comment?.abilities?.can_manage !== true}] : []), ...((comment?.status === 'blinded') ? [{id: 'restore', label: '$t:sirsoft-board.admin.posts.actions.restore', iconName: 'arrows-rotate', variant: 'success', disabled: comment?.abilities?.can_manage !== true}] : []), {id: 'delete', label: '$t:common.delete', iconName: 'trash', variant: 'danger', disabled: !((comment?.is_author && comment?.abilities?.can_write_comments) || comment?.abilities?.can_manage)}]}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onItemClick",
|
||||
"type": "click",
|
||||
"handler": "switch",
|
||||
"value": "{{$args[0]}}",
|
||||
"cases": {
|
||||
"edit": {
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"editingComment": "{{comment?.id}}",
|
||||
"editingCommentPostId": "{{comment?.post_id}}",
|
||||
"edit_comment_content": "{{comment?.content}}",
|
||||
"replyingTo": null,
|
||||
"editSubmitting": false
|
||||
}
|
||||
},
|
||||
"blind": {
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"blind_modal_data": {
|
||||
"id": "{{comment?.id}}",
|
||||
"slug": "{{route.slug}}",
|
||||
"type": "comment"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "blind_modal"
|
||||
}
|
||||
]
|
||||
},
|
||||
"restore": {
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"restore_modal_data": {
|
||||
"id": "{{comment?.id}}",
|
||||
"slug": "{{route.slug}}",
|
||||
"type": "comment"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "restore_modal"
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"modal_data": {
|
||||
"id": "{{comment?.id}}",
|
||||
"type": "comment"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "delete_modal"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "comment_content_wrapper",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.editingComment !== comment.id}}",
|
||||
"children": [
|
||||
{
|
||||
"id": "deleted_comment_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{comment?.deleted_at}}",
|
||||
"props": {
|
||||
"className": "mt-1.5 px-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center flex-wrap gap-x-4 gap-y-2 text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "trash",
|
||||
"className": "w-3.5 h-3.5 text-red-400 dark:text-red-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.deleted_comment_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{comment?.abilities?.can_manage !== true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"showDeletedCommentId": "{{_local.showDeletedCommentId === comment?.id ? null : comment?.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": "{{_local.showDeletedCommentId === comment?.id ? '$t:sirsoft-board.admin.posts.detail.hide_original_content' : '$t:sirsoft-board.admin.posts.detail.view_original_content'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.showDeletedCommentId === comment?.id}}",
|
||||
"props": {
|
||||
"className": "mt-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-sm text-gray-600 dark:text-gray-400 whitespace-pre-line leading-relaxed"
|
||||
},
|
||||
"text": "{{comment.content}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blinded_comment_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{comment?.status === 'blinded' && !comment?.deleted_at}}",
|
||||
"props": {
|
||||
"className": "mt-1.5 px-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center flex-wrap gap-x-4 gap-y-2 text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center flex-wrap gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "eye-slash",
|
||||
"className": "w-3.5 h-3.5 text-gray-400 dark:text-gray-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.blinded_comment_notice"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{comment?.abilities?.can_manage && (comment?.action_logs ?? []).filter(log => log.action === 'blind').length > 0}}",
|
||||
"props": {
|
||||
"className": "text-amber-600 dark:text-amber-400"
|
||||
},
|
||||
"text": "($t:sirsoft-board.admin.posts.detail.blind_reason_label {{(comment?.action_logs ?? []).filter(log => log.action === 'blind').slice(-1)[0]?.reason ?? '-'}})"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{comment?.abilities?.can_manage !== true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"showBlindedCommentId": "{{_local.showBlindedCommentId === comment?.id ? null : comment?.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": "{{_local.showBlindedCommentId === comment?.id ? '$t:sirsoft-board.admin.posts.detail.hide_original_content' : '$t:sirsoft-board.admin.posts.detail.view_original_content'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.showBlindedCommentId === comment?.id}}",
|
||||
"props": {
|
||||
"className": "mt-2 p-3 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded text-sm text-gray-600 dark:text-gray-400 whitespace-pre-line leading-relaxed"
|
||||
},
|
||||
"text": "{{comment.content}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "normal_comment_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{comment?.status !== 'blinded' && !comment?.deleted_at}}",
|
||||
"props": {
|
||||
"className": "mt-1.5 px-4 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line leading-relaxed"
|
||||
},
|
||||
"text": "{{comment.content}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "comment_edit_form",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mt-3 pl-10"
|
||||
},
|
||||
"if": "{{_local.editingComment === comment.id}}",
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Textarea",
|
||||
"props": {
|
||||
"value": "{{_local.edit_comment_content ?? comment.content}}",
|
||||
"rows": 3,
|
||||
"className": "w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white mb-2"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "input",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"edit_comment_content": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-3 py-1.5 text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"editingComment": null,
|
||||
"edit_comment_content": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{_local.editSubmitting === true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"if": "{{_local.editSubmitting !== true}}",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"editSubmitting": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board/{{route.slug}}/posts/{{comment?.post_id}}/comments/{{comment?.id}}",
|
||||
"params": {
|
||||
"method": "PUT",
|
||||
"body": {
|
||||
"content": "{{_local.edit_comment_content}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "post"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"editingComment": null,
|
||||
"edit_comment_content": "",
|
||||
"editSubmitting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.posts.detail.comment_edit_success"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"editSubmitting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
},
|
||||
"if": "{{_local.editSubmitting}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "replies_toggle_button",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{comment.depth === 0 && comment.replies_count > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 px-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"collapsedReplies": "{{Object.assign({}, _local.collapsedReplies || {}, {[comment.id]: !(_local.collapsedReplies?.[comment.id] ?? true)})}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "{{(_local.collapsedReplies?.[comment.id] ?? true) ? 'chevron-down' : 'chevron-up'}}",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{(_local.collapsedReplies?.[comment.id] ?? true) ? '$t:sirsoft-board.admin.posts.detail.show_replies|count=' + comment.replies_count : '$t:sirsoft-board.admin.posts.detail.hide_replies'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Admin Board Post Detail - Comments Section (Header Separated)"
|
||||
},
|
||||
"id": "comments_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{post?.data?.board?.use_comment}}",
|
||||
"props": {
|
||||
"className": "mt-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "comments_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-6 pb-3 border-b border-gray-200 dark:border-gray-700 pb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "comments",
|
||||
"className": "w-5 h-5 text-green-600 dark:text-green-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.comments_title|count={{(post?.data?.comments ?? []).length}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "comments_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"id": "comments_list_container",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": ""
|
||||
},
|
||||
"if": "{{(post?.data?.comments ?? []).length > 0}}",
|
||||
"children": [
|
||||
{
|
||||
"id": "comment_wrapper",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "post?.data?.comments ?? []",
|
||||
"item_var": "comment",
|
||||
"index_var": "idx"
|
||||
},
|
||||
"if": "{{(comment?.depth ?? 0) === 0 || (_local.collapsedReplies?.[$computed.commentRootMap?.[comment.id]] === false)}}",
|
||||
"children": [
|
||||
{
|
||||
"id": "comment_item",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border-b border-gray-200 dark:border-gray-700 px-4 py-3 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50 transition-colors",
|
||||
"style": {
|
||||
"marginLeft": "{{Math.min(comment.depth ?? 0, 10) * 1}}rem"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"partial": "_comment.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reply_form_for_comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.replyingTo === comment.id && post?.data?.status !== 'blinded' && post?.data?.status !== 'deleted' && !post?.data?.deleted_at}}",
|
||||
"props": {
|
||||
"className": "px-4 py-3 border-b border-gray-200 dark:border-gray-700",
|
||||
"style": {
|
||||
"marginLeft": "{{Math.min((comment.depth ?? 0) + 1, 10) * 1}}rem"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "turn-up",
|
||||
"className": "w-3.5 h-3.5 fa-rotate-90 mt-1 text-gray-500 dark:text-gray-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.replying_to|name={{comment.author.name}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Textarea",
|
||||
"props": {
|
||||
"value": "{{_local.reply_comment_content ?? ''}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.posts.detail.reply_placeholder",
|
||||
"rows": 2,
|
||||
"disabled": "{{post?.data?.abilities?.can_write_comments !== true}}",
|
||||
"className": "w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white mb-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "input",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"reply_comment_content": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-3 py-1.5 text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"replyingTo": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{post?.data?.abilities?.can_write_comments !== true || _local.replySubmitting === true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"if": "{{_local.replySubmitting !== true}}",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"replySubmitting": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board/{{route.slug}}/posts/{{comment?.post_id}}/comments",
|
||||
"params": {
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"content": "{{_local.reply_comment_content}}",
|
||||
"parent_id": "{{comment?.id}}",
|
||||
"depth": "{{(comment?.depth ?? 0) + 1}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "post"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"replyingTo": null,
|
||||
"replyingToPostId": null,
|
||||
"replyingToDepth": null,
|
||||
"reply_comment_content": "",
|
||||
"replySubmitting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.posts.detail.reply_submit_success"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"replySubmitting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{!_local.replySubmitting}}",
|
||||
"props": {
|
||||
"name": "paper-plane",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_local.replySubmitting}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.replySubmitting ? '$t:sirsoft-board.admin.posts.detail.reply_submitting' : '$t:sirsoft-board.admin.posts.detail.reply'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "no_comments_message",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-12 text-center"
|
||||
},
|
||||
"if": "{{(post?.data?.comments ?? []).length === 0}}",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "comment",
|
||||
"className": "w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.no_comments"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "comment_input_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{post?.data?.board?.use_comment && post?.data?.status !== 'blinded' && post?.data?.status !== 'deleted' && !post?.data?.deleted_at}}",
|
||||
"props": {
|
||||
"className": "p-6 border-t border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "user-circle",
|
||||
"className": "w-8 h-8 text-gray-400 dark:text-gray-500 flex-shrink-0 mt-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Textarea",
|
||||
"props": {
|
||||
"value": "{{_local.new_comment_content ?? ''}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.posts.detail.comment_placeholder",
|
||||
"rows": 3,
|
||||
"disabled": "{{post?.data?.abilities?.can_write_comments !== true}}",
|
||||
"className": "w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "input",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"new_comment_content": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end mt-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "px-4 py-2 text-sm bg-gray-800 dark:bg-gray-700 text-white rounded-lg hover:bg-gray-900 dark:hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{post?.data?.abilities?.can_write_comments !== true || _local.commentSubmitting === true}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"if": "{{_local.commentSubmitting !== true}}",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"commentSubmitting": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board/{{route.slug}}/posts/{{route.id}}/comments",
|
||||
"params": {
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"content": "{{_local.new_comment_content}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "post"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"new_comment_content": "",
|
||||
"commentSubmitting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.posts.detail.comment_submit_success"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"commentSubmitting": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{error.message}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{!_local.commentSubmitting}}",
|
||||
"props": {
|
||||
"name": "paper-plane",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_local.commentSubmitting}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.commentSubmitting ? '$t:sirsoft-board.admin.posts.detail.comment_submitting' : '$t:sirsoft-board.admin.posts.detail.comment_submit'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+1254
File diff suppressed because it is too large.
Load diff
+459
@@ -0,0 +1,459 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Admin Board Reply Card Content - Simplified View Only"
|
||||
},
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"id": "reply_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-6 border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "title_row",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-row items-start justify-between gap-4 mb-4"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex flex-col gap-3 mb-4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "title_with_badges",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1 min-w-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "title_with_prefix",
|
||||
"type": "basic",
|
||||
"name": "H2",
|
||||
"props": {
|
||||
"className": "text-xl font-semibold text-gray-900 dark:text-white flex items-center gap-2 flex-wrap"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 flex-wrap"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "reply_arrow_icon",
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrow-right",
|
||||
"className": "w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "category_badge_prefix",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
},
|
||||
"text": "{{item.category}}",
|
||||
"if": "{{item.category}}"
|
||||
},
|
||||
{
|
||||
"id": "notice_prefix",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-blue-600 dark:text-blue-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.badge.notice",
|
||||
"if": "{{item.is_notice}}"
|
||||
},
|
||||
{
|
||||
"id": "title_text",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{item.title}}"
|
||||
},
|
||||
{
|
||||
"id": "attachment_icon",
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"if": "{{item.has_attachment}}",
|
||||
"props": {
|
||||
"name": "paperclip",
|
||||
"className": "w-4 h-4 text-gray-500 dark:text-gray-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "new_badge",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{item.is_new}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.common.new"
|
||||
},
|
||||
{
|
||||
"id": "secret_icon",
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "lock",
|
||||
"className": "w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0"
|
||||
},
|
||||
"if": "{{item.is_secret}}"
|
||||
},
|
||||
{
|
||||
"id": "status_badge_blinded",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.status_blinded",
|
||||
"if": "{{item.status === 'blinded'}}"
|
||||
},
|
||||
{
|
||||
"id": "status_badge_deleted",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-800 text-white dark:bg-gray-600 dark:text-gray-100"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.status_deleted",
|
||||
"if": "{{item.deleted_at}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "detail_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1.5 px-3 py-1.5 text-sm bg-gray-800 dark:bg-gray-700 text-white rounded-lg hover:bg-gray-900 dark:hover:bg-gray-600 transition-colors"
|
||||
},
|
||||
"if": "{{item.id != route.id}}",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{route.slug}}/post/{{item.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.view_detail_button"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reply_meta",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-wrap items-center gap-2 text-sm text-gray-600 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "author_name_menu",
|
||||
"type": "composite",
|
||||
"name": "ActionMenu",
|
||||
"if": "{{!item.author?.is_guest}}",
|
||||
"props": {
|
||||
"triggerLabel": "{{item.author?.name}}",
|
||||
"triggerIconName": "",
|
||||
"triggerClassName": "font-medium text-gray-900 dark:text-white",
|
||||
"position": "right",
|
||||
"items": [
|
||||
{
|
||||
"id": "view_user_info",
|
||||
"label": "$t:sirsoft-board.admin.posts.detail.user_info",
|
||||
"iconName": "user"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onItemClick",
|
||||
"type": "click",
|
||||
"handler": "switch",
|
||||
"value": "{{$args[0]}}",
|
||||
"cases": {
|
||||
"view_user_info": {
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/users/{{item.author?.uuid}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{item.author?.is_guest}}",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{item.author?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "({{item.author?.email}})",
|
||||
"if": "{{item.author?.email}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.guest",
|
||||
"if": "{{item.author?.is_guest}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "·"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{item.created_at}}"
|
||||
},
|
||||
{
|
||||
"id": "comment_count_separator",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{(item.comment_count ?? 0) > 0}}",
|
||||
"text": "·"
|
||||
},
|
||||
{
|
||||
"id": "comment_count",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(item.comment_count ?? 0) > 0}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "comment",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.comments_title|count={{item.comment_count}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{item.ip_address}}",
|
||||
"text": "·"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{item.ip_address}}",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "IP: {{item.ip_address}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "attachments_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-6 py-4 border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"if": "{{(item.attachments ?? []).length > 0}}",
|
||||
"children": [
|
||||
{
|
||||
"id": "attachments_toggle",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"showAttachments": "{{!_local.showAttachments}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "paperclip",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.posts.detail.attachments|count={{(item.attachments ?? []).length}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "{{_local.showAttachments ? 'chevron-up' : 'chevron-down'}}",
|
||||
"className": "w-4 h-4 text-gray-500 dark:text-gray-400"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "attachments_list",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mt-3 ml-4 grid grid-cols-1 gap-2"
|
||||
},
|
||||
"if": "{{_local.showAttachments}}",
|
||||
"iteration": {
|
||||
"source": "item.attachments ?? []",
|
||||
"item_var": "attachment",
|
||||
"index_var": "index"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "attachment_card",
|
||||
"type": "basic",
|
||||
"name": "A",
|
||||
"props": {
|
||||
"href": "{{attachment.download_url}}",
|
||||
"className": "flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700 hover:border-gray-300 dark:border-gray-600 dark:hover:border-gray-500 transition-all cursor-pointer group no-underline"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "download",
|
||||
"className": "w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:text-blue-400 dark:group-hover:text-blue-400 transition-colors flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white truncate flex-1"
|
||||
},
|
||||
"text": "{{attachment.original_filename}} ({{attachment.size_formatted}})"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reply_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "HtmlContent",
|
||||
"props": {
|
||||
"content": "{{item?.content}}",
|
||||
"isHtml": "{{(item?.content_mode ?? 'text') === 'html'}}",
|
||||
"className": "{{_local.expandedReplies?.[item.id] ? '' : 'line-clamp-3'}} text-gray-900 dark:text-gray-100"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{(item?.content?.length ?? 0) > 150}}",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "mt-2 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center gap-1"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"expandedReplies": "{{Object.assign({}, _local.expandedReplies || {}, {[item.id]: !_local.expandedReplies?.[item.id]})}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.expandedReplies?.[item.id] ? '$t:sirsoft-board.admin.posts.form.hide_content' : '$t:sirsoft-board.admin.posts.form.show_full_content'}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "{{_local.expandedReplies?.[item.id] ? 'chevron-up' : 'chevron-down'}}",
|
||||
"className": "w-3 h-3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Post Form - Attachments Section (Compact List Style)"
|
||||
},
|
||||
"id": "attachments_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{form_meta?.data?.board?.use_file_upload}}",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "paperclip",
|
||||
"className": "w-5 h-5 text-gray-700 dark:text-gray-300"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.attachments"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "FileUploader",
|
||||
"props": {
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"collection": "post_attachments",
|
||||
"maxFiles": "{{form_meta?.data?.board?.max_file_count ?? 5}}",
|
||||
"maxSize": "{{form_meta?.data?.board?.max_file_size ?? 10}}",
|
||||
"accept": "{{form_meta?.data?.board?.allowed_extensions ? '.' + form_meta.data.board.allowed_extensions.join(',.') : '.jpg,.jpeg,.png,.gif,.pdf,.zip'}}",
|
||||
"initialFiles": "{{form_meta?.data?.attachments ?? []}}",
|
||||
"autoUpload": false,
|
||||
"uploadTriggerEvent": "upload:board_attachments",
|
||||
"compact": true,
|
||||
"confirmBeforeRemove": true,
|
||||
"confirmRemoveTitle": "$t:sirsoft-board.admin.posts.form.confirm_remove_attachment_title",
|
||||
"confirmRemoveMessage": "$t:sirsoft-board.admin.posts.form.confirm_remove_attachment_message",
|
||||
"uploadParams": {
|
||||
"temp_key": "{{_local.tempKey}}"
|
||||
},
|
||||
"apiEndpoints": {
|
||||
"upload": "/api/modules/sirsoft-board/admin/board/{{route?.slug}}/attachments",
|
||||
"delete": "/api/modules/sirsoft-board/admin/board/{{route?.slug}}/attachments/:id",
|
||||
"reorder": "/api/modules/sirsoft-board/admin/board/{{route?.slug}}/attachments/reorder"
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onFilesChange",
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"if": "{{!_local.hasChanges}}",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"hasChanges": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "onUploadError",
|
||||
"type": "change",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"uploadError": "{{$args[0]}}",
|
||||
"isSaving": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "{{$args[0]}}",
|
||||
"icon": "triangle-exclamation"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Post Form - All Form Fields in One Card"
|
||||
},
|
||||
"id": "post_form_fields",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "main_form_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 space-y-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "author_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{route?.id && form_meta?.data?.author}}",
|
||||
"props": {
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_author_name",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.author_info"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{form_meta?.data?.author?.email ? form_meta.data.author.name + ' (' + form_meta.data.author.email + ')' : form_meta?.data?.author?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{form_meta?.data?.author?.is_guest}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.guest"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_created_at",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.created_at"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{form_meta?.data?.created_at}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "post_options_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{form_meta?.data?.board?.user_abilities?.can_manage || form_meta?.data?.board?.secret_mode !== 'disabled'}}",
|
||||
"props": {
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.post_options"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "notice_card",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"if": "{{!_local.form?.parent_id}}",
|
||||
"props": {
|
||||
"disabled": "{{_computed.isReadOnly || !form_meta?.data?.board?.user_abilities?.can_manage}}",
|
||||
"className": "bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 cursor-pointer hover:border-blue-400 dark:hover:border-blue-600 transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"name": "is_notice",
|
||||
"disabled": "{{_computed.isReadOnly || !form_meta?.data?.board?.user_abilities?.can_manage}}",
|
||||
"className": "mt-0.5 w-4 h-4 text-blue-600 dark:text-blue-400 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "bullhorn",
|
||||
"className": "w-4 h-4 text-blue-600 dark:text-blue-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.is_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.notice_description"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "secret_card",
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"if": "{{form_meta?.data?.board?.secret_mode !== 'disabled'}}",
|
||||
"props": {
|
||||
"className": "bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 cursor-pointer hover:border-amber-400 dark:hover:border-amber-600 transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"name": "is_secret",
|
||||
"disabled": "{{_computed.isReadOnly || form_meta?.data?.board?.secret_mode === 'always'}}",
|
||||
"className": "mt-0.5 w-4 h-4 text-amber-600 bg-gray-100 border-gray-300 rounded focus:ring-amber-500 dark:focus:ring-amber-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "lock",
|
||||
"className": "w-4 h-4 text-amber-600 dark:text-amber-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.is_secret"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.secret_description"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "category_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{form_meta?.data?.board?.categories && form_meta.data.board.categories.length > 0}}",
|
||||
"props": {
|
||||
"className": "pb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.category"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "category",
|
||||
"value": "{{_local.form?.category ?? ''}}",
|
||||
"disabled": "{{_computed.isReadOnly || !!_local.form?.parent_id}}",
|
||||
"className": "block w-full sm:w-64 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-gray-900 dark:text-white focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Option",
|
||||
"props": {
|
||||
"value": ""
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.select_category"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Option",
|
||||
"iteration": {
|
||||
"source": "form_meta?.data?.board?.categories ?? []",
|
||||
"item_var": "category"
|
||||
},
|
||||
"props": {
|
||||
"value": "{{category}}"
|
||||
},
|
||||
"text": "{{category}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "title_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"name": "title",
|
||||
"value": "{{_local.form?.title ?? ''}}",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.posts.form.title_placeholder",
|
||||
"className": "block w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "content_section",
|
||||
"type": "composite",
|
||||
"name": "HtmlEditor",
|
||||
"props": {
|
||||
"content": "{{_local.form?.content ?? ''}}",
|
||||
"isHtml": "{{(_local.form?.content_mode ?? 'text') === 'html'}}",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"rows": 15,
|
||||
"placeholder": "$t:sirsoft-board.admin.posts.form.content_placeholder",
|
||||
"label": "$t:sirsoft-board.admin.posts.form.content",
|
||||
"showHtmlModeToggle": true,
|
||||
"name": "content"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onChange",
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"form": {
|
||||
"content": "{{$args[0].target.value}}"
|
||||
},
|
||||
"hasChanges": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "onIsHtmlChange",
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"form": {
|
||||
"content_mode": "{{$args[0].target.checked ? 'html' : 'text'}}"
|
||||
},
|
||||
"hasChanges": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Post Form - Parent Post Information (답글 작성 시에만 표시)"
|
||||
},
|
||||
"id": "parent_post_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(form_data?.data?.parent_id || query?.parent_id) && form_meta?.data?.parent_post?.id}}",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "reply",
|
||||
"className": "w-4 h-4 text-blue-600 dark:text-blue-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.form.reply_to"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{form_meta?.data?.parent_post?.is_notice}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.notice"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{form_meta?.data?.parent_post?.is_secret}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.posts.secret"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H4",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{form_meta?.data?.parent_post?.title}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "A",
|
||||
"props": {
|
||||
"href": "/admin/board/{{route.slug}}/{{form_meta?.data?.parent_post?.id}}",
|
||||
"title": "$t:sirsoft-board.admin.posts.form.view_original",
|
||||
"className": "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrow-up-right-from-square",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{form_meta?.data?.parent_post?.user?.name ?? '$t:sirsoft-board.admin.posts.unknown_author'}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "·"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{form_meta?.data?.parent_post?.created_at}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{form_meta?.data?.parent_post?.content}}",
|
||||
"props": {
|
||||
"className": "bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "HtmlContent",
|
||||
"props": {
|
||||
"content": "{{form_meta?.data?.parent_post?.content}}",
|
||||
"isHtml": "{{(form_meta?.data?.parent_post?.content_mode ?? 'text') === 'html'}}",
|
||||
"className": "{{_local.parentPostExpanded ? '' : 'line-clamp-3'}} text-sm text-gray-600 dark:text-gray-300 mb-2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{(form_meta?.data?.parent_post?.content?.length ?? 0) > 150}}",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center gap-1"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"parentPostExpanded": "{{!_local.parentPostExpanded}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{_local.parentPostExpanded ? '$t:sirsoft-board.admin.posts.form.hide_content' : '$t:sirsoft-board.admin.posts.form.show_full_content'}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "{{_local.parentPostExpanded ? 'chevron-up' : 'chevron-down'}}",
|
||||
"className": "w-3 h-3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Status Stats Grid and Alert Box with Dynamic Actions for Admin Report Detail View"
|
||||
},
|
||||
"id": "status_wrapper",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{report_detail?.data}}",
|
||||
"props": {
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "status_alert",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-6 py-4 border rounded-lg {{report_detail?.data?.status === 'pending' || report_detail?.data?.status === 'review' || report_detail?.data?.status === 'rejected' || report_detail?.data?.status === 'suspended' || report_detail?.data?.status === 'deleted' ? 'flex flex-row items-end justify-between gap-4' : ''}} {{report_detail?.data?.status === 'pending' ? 'bg-yellow-50 dark:bg-yellow-900 border-yellow-300 dark:border-yellow-800' : report_detail?.data?.status === 'review' ? 'bg-blue-50 dark:bg-blue-900 border-blue-300 dark:border-blue-800' : report_detail?.data?.status === 'rejected' ? 'bg-purple-50 dark:bg-purple-900 border-purple-200 dark:border-purple-700' : report_detail?.data?.status === 'deleted' ? 'bg-gray-200 dark:bg-gray-700 border-gray-400 dark:border-gray-600' : 'bg-red-50 dark:bg-red-900 border-red-300 dark:border-red-800'}}"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "px-6 py-4 border rounded-lg {{report_detail?.data?.status === 'pending' || report_detail?.data?.status === 'review' || report_detail?.data?.status === 'rejected' || report_detail?.data?.status === 'suspended' || report_detail?.data?.status === 'deleted' ? 'flex flex-col gap-3' : ''}} {{report_detail?.data?.status === 'pending' ? 'bg-yellow-50 dark:bg-yellow-900 border-yellow-300 dark:border-yellow-800' : report_detail?.data?.status === 'review' ? 'bg-blue-50 dark:bg-blue-900 border-blue-300 dark:border-blue-800' : report_detail?.data?.status === 'rejected' ? 'bg-purple-50 dark:bg-purple-900 border-purple-200 dark:border-purple-700' : report_detail?.data?.status === 'deleted' ? 'bg-gray-200 dark:bg-gray-700 border-gray-400 dark:border-gray-600' : 'bg-red-50 dark:bg-red-900 border-red-300 dark:border-red-800'}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{report_detail?.data?.status === 'pending' || report_detail?.data?.status === 'review' || report_detail?.data?.status === 'rejected' || report_detail?.data?.status === 'suspended' ? 'flex items-start gap-3 flex-1' : 'flex items-start gap-3'}}"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{report_detail?.data?.status === 'pending'}}",
|
||||
"props": {
|
||||
"name": "exclamation-circle",
|
||||
"className": "w-5 h-5 text-yellow-600 dark:text-yellow-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{report_detail?.data?.status === 'review'}}",
|
||||
"props": {
|
||||
"name": "clock",
|
||||
"className": "w-5 h-5 {{report_detail?.data?.status === 'review' ? 'text-blue-600 dark:text-blue-400' : ''}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{report_detail?.data?.status === 'rejected'}}",
|
||||
"props": {
|
||||
"name": "times-circle",
|
||||
"className": "w-5 h-5 text-purple-600 dark:text-purple-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{report_detail?.data?.status === 'suspended'}}",
|
||||
"props": {
|
||||
"name": "exclamation-triangle",
|
||||
"className": "w-5 h-5 {{report_detail?.data?.status === 'suspended' ? 'text-red-600 dark:text-red-400' : ''}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{report_detail?.data?.status === 'deleted'}}",
|
||||
"props": {
|
||||
"name": "ban",
|
||||
"className": "w-5 h-5 text-gray-700 dark:text-gray-300"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "font-semibold text-base {{report_detail?.data?.status === 'pending' ? 'text-yellow-700 dark:text-yellow-300' : report_detail?.data?.status === 'review' ? 'text-blue-700 dark:text-blue-300' : report_detail?.data?.status === 'rejected' ? 'text-purple-700 dark:text-purple-200' : report_detail?.data?.status === 'deleted' ? 'text-gray-900 dark:text-gray-300' : 'text-red-700 dark:text-red-300'}}"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "font-semibold text-sm {{report_detail?.data?.status === 'pending' ? 'text-yellow-700 dark:text-yellow-300' : report_detail?.data?.status === 'review' ? 'text-blue-700 dark:text-blue-300' : report_detail?.data?.status === 'rejected' ? 'text-purple-700 dark:text-purple-200' : report_detail?.data?.status === 'deleted' ? 'text-gray-900 dark:text-gray-300' : 'text-red-700 dark:text-red-300'}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.alert_title_{{report_detail?.data?.status}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm {{report_detail?.data?.status === 'pending' ? 'text-yellow-600 dark:text-yellow-400' : report_detail?.data?.status === 'review' ? 'text-blue-600 dark:text-blue-400' : report_detail?.data?.status === 'rejected' ? 'text-purple-600 dark:text-purple-400' : report_detail?.data?.status === 'deleted' ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}}"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-xs {{report_detail?.data?.status === 'pending' ? 'text-yellow-600 dark:text-yellow-400' : report_detail?.data?.status === 'review' ? 'text-blue-600 dark:text-blue-400' : report_detail?.data?.status === 'rejected' ? 'text-purple-600 dark:text-purple-400' : report_detail?.data?.status === 'deleted' ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.alert_message_{{report_detail?.data?.status}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{report_detail?.data?.available_actions?.length > 0}}",
|
||||
"props": {
|
||||
"className": "flex flex-wrap gap-2 items-end"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{report_detail?.data?.available_actions?.includes('pending') && report_detail?.data?.status !== 'rejected' && report_detail?.data?.status !== 'suspended'}}",
|
||||
"props": {
|
||||
"disabled": "{{report_detail?.data?.abilities?.can_manage !== true}}",
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 text-xs font-medium transition-colors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionType": "pending",
|
||||
"actionReason": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "openModal",
|
||||
"target": "status_action_modal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-left",
|
||||
"className": "w-4 h-4"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"name": "chevron-left",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.actions.back_to_pending"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{report_detail?.data?.available_actions?.includes('review')}}",
|
||||
"props": {
|
||||
"disabled": "{{report_detail?.data?.abilities?.can_manage !== true}}",
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-xs font-medium transition-colors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionType": "review",
|
||||
"actionReason": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "openModal",
|
||||
"target": "status_action_modal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "{{report_detail?.data?.status === 'pending' ? 'check-circle' : 'clock'}}",
|
||||
"className": "w-4 h-4"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"name": "{{report_detail?.data?.status === 'pending' ? 'check-circle' : 'clock'}}",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{report_detail?.data?.status === 'pending' ? '$t:sirsoft-board.admin.reports.detail.actions.start_review' : '$t:sirsoft-board.admin.reports.detail.actions.re_review'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{report_detail?.data?.available_actions?.includes('rejected')}}",
|
||||
"props": {
|
||||
"disabled": "{{report_detail?.data?.abilities?.can_manage !== true}}",
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-xs font-medium transition-colors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionType": "rejected",
|
||||
"actionReason": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "openModal",
|
||||
"target": "status_action_modal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "times-circle",
|
||||
"className": "w-4 h-4"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"name": "times-circle",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.actions.reject"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{report_detail?.data?.available_actions?.includes('suspended')}}",
|
||||
"props": {
|
||||
"disabled": "{{report_detail?.data?.abilities?.can_manage !== true}}",
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-xs font-medium transition-colors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionType": "suspended",
|
||||
"actionReason": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "openModal",
|
||||
"target": "status_action_modal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "exclamation-triangle",
|
||||
"className": "w-4 h-4"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"name": "exclamation-triangle",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.actions.suspend"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{report_detail?.data?.available_actions?.includes('deleted')}}",
|
||||
"props": {
|
||||
"disabled": "{{report_detail?.data?.abilities?.can_manage !== true}}",
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-3 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600 text-xs font-medium transition-colors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"actionType": "deleted",
|
||||
"actionReason": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "openModal",
|
||||
"target": "status_action_modal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "trash",
|
||||
"className": "w-4 h-4"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"name": "trash",
|
||||
"className": "w-3.5 h-3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.actions.delete"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Histories Card for Admin Report Detail View"
|
||||
},
|
||||
"id": "card_history",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "history_title",
|
||||
"type": "basic",
|
||||
"name": "H2",
|
||||
"props": {
|
||||
"className": "text-lg font-bold text-gray-900 dark:text-white mb-4"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.histories_title"
|
||||
},
|
||||
{
|
||||
"id": "history_data_grid",
|
||||
"type": "composite",
|
||||
"name": "DataGrid",
|
||||
"if": "{{report_detail?.data?.histories && report_detail?.data?.histories.length > 0}}",
|
||||
"props": {
|
||||
"className": "histories-compact-grid",
|
||||
"columns": [
|
||||
{
|
||||
"field": "created_at",
|
||||
"header": "$t:sirsoft-board.admin.reports.detail.table.date",
|
||||
"sortable": false,
|
||||
"required": true,
|
||||
"cellChildren": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "{{row.created_at}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"field": "processor_name",
|
||||
"header": "$t:sirsoft-board.admin.reports.detail.table.processor",
|
||||
"sortable": false,
|
||||
"required": true,
|
||||
"cellChildren": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{row.processor_name}}{{row.processor_email ? ' (' + row.processor_email + ')' : ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"field": "type",
|
||||
"header": "$t:sirsoft-board.admin.reports.detail.table.action",
|
||||
"sortable": false,
|
||||
"required": true,
|
||||
"cellChildren": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{row.type === 'reported' ? 'bg-blue-100 text-blue-800 dark:bg-blue-600 dark:text-white' : row.type === 're_reported' ? 'bg-indigo-100 text-indigo-800 dark:bg-indigo-600 dark:text-white' : row.type === 'rejected' ? 'bg-purple-100 text-purple-800 dark:bg-purple-600 dark:text-white' : row.type === 'suspended' ? 'bg-orange-100 text-orange-800 dark:bg-orange-600 dark:text-white' : row.type === 'deleted' ? 'bg-gray-600 text-white dark:bg-gray-500 dark:text-white' : row.type === 'restored' ? 'bg-green-100 text-green-800 dark:bg-green-600 dark:text-white' : row.type === 'review' ? 'bg-cyan-100 text-cyan-800 dark:bg-cyan-600 dark:text-white' : 'bg-gray-100 text-gray-800 dark:bg-gray-600 dark:text-gray-100'}}"
|
||||
},
|
||||
"text": "{{row.type === 'reported' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.reported' : row.type === 're_reported' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.re_reported' : row.type === 'rejected' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.rejected' : row.type === 'suspended' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.suspended' : row.type === 'deleted' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.deleted' : row.type === 'restored' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.restored' : row.type === 'review' ? '$t:sirsoft-board.admin.reports.detail.history_timeline.review' : row.action_label ?? row.type}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"field": "reason",
|
||||
"header": "$t:sirsoft-board.admin.reports.detail.table.reason",
|
||||
"sortable": false,
|
||||
"required": true,
|
||||
"cellChildren": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-600 dark:text-gray-400 leading-relaxed whitespace-pre-line break-words max-w-md"
|
||||
},
|
||||
"text": "{{row.reason}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"data": "{{report_detail?.data?.histories ?? []}}",
|
||||
"sortable": false,
|
||||
"pagination": false,
|
||||
"selectable": false,
|
||||
"showColumnSelector": false,
|
||||
"responsiveBreakpoint": 768,
|
||||
"emptyMessage": "$t:sirsoft-board.admin.reports.detail.no_histories"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "history_empty",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{!report_detail?.data?.histories || report_detail?.data?.histories.length === 0}}",
|
||||
"props": {
|
||||
"className": "text-center py-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.no_histories"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+811
@@ -0,0 +1,811 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "Report Info Card for Admin Report Detail View"
|
||||
},
|
||||
"id": "card_report_info",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"id": "reported_content_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "reported_content_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between mb-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H2",
|
||||
"props": {
|
||||
"className": "text-lg font-bold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.report_info_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{report_detail?.data}}",
|
||||
"props": {
|
||||
"className": "{{report_detail?.data?.target_type === 'comment' ? 'inline-flex items-center px-3 py-1.5 rounded text-xs font-bold bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' : 'inline-flex items-center px-3 py-1.5 rounded text-xs font-bold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'}}"
|
||||
},
|
||||
"text": "$t:sirsoft-board.enums.report_type.{{report_detail?.data?.target_type}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 rounded-lg px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1.5 text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chart-bar",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-bold"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.report_summary_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs"
|
||||
},
|
||||
"text": "{{report_detail?.data?.reason_summary ?? '-'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "w-px h-4 bg-gray-300 dark:bg-gray-600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1.5 text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "calendar",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-bold"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.first_reported_at_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs"
|
||||
},
|
||||
"text": "{{report_detail?.data?.first_reported_at ?? '-'}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "w-px h-4 bg-gray-300 dark:bg-gray-600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "target_status_info",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{report_detail?.data?.target_status}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1.5 text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "w-4 h-4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "font-bold"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.target_status_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs {{report_detail?.data?.target_status === 'published' ? 'text-green-600 dark:text-green-400' : report_detail?.data?.target_status === 'blinded' ? 'text-orange-600 dark:text-orange-400' : 'text-gray-500 dark:text-gray-400'}}"
|
||||
},
|
||||
"text": "{{report_detail?.data?.target_status_label ?? report_detail?.data?.target_status ?? '-'}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{report_detail?.data?.target_status === 'blinded' && report_detail?.data?.blind_trigger_type_label}}",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "({{report_detail?.data?.blind_trigger_type_label}})"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reported_content_grid",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 lg:grid-cols-2 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_author",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "{{report_detail?.data?.target_type === 'post' ? '$t:sirsoft-board.admin.reports.detail.content_author_label' : '$t:sirsoft-board.admin.reports.detail.comment_author_label'}}"
|
||||
},
|
||||
{
|
||||
"comment": "회원 작성자: ActionMenu로 사용자 정보 드롭다운",
|
||||
"type": "composite",
|
||||
"name": "ActionMenu",
|
||||
"if": "{{!(report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.author?.is_guest : report_detail?.data?.comment?.author?.is_guest)}}",
|
||||
"props": {
|
||||
"triggerLabel": "{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.author?.name : report_detail?.data?.comment?.author?.name}}",
|
||||
"triggerIconName": "",
|
||||
"position": "right",
|
||||
"items": [
|
||||
{
|
||||
"id": "view_author",
|
||||
"label": "$t:sirsoft-board.admin.reports.detail.user_info",
|
||||
"iconName": "user"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onItemClick",
|
||||
"type": "click",
|
||||
"handler": "switch",
|
||||
"value": "{{$args[0]}}",
|
||||
"cases": {
|
||||
"view_author": {
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/users/{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.author?.uuid : report_detail?.data?.comment?.author?.uuid}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "비회원 작성자: 이름 + 배지 (드롭다운 없음)",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.author?.is_guest : report_detail?.data?.comment?.author?.is_guest}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.author?.name : report_detail?.data?.comment?.author?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Badge",
|
||||
"props": {
|
||||
"color": "yellow",
|
||||
"size": "sm"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.guest_badge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{(report_detail?.data?.target_type === 'post' && report_detail?.data?.post?.author?.email) || (report_detail?.data?.target_type === 'comment' && report_detail?.data?.comment?.author?.email)}}",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.author?.email : report_detail?.data?.comment?.author?.email}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_board",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.board_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{report_detail?.data?.board?.slug}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer transition-colors group"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{report_detail?.data?.board?.slug}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "border-b border-transparent group-hover:border-blue-600 dark:group-hover:border-blue-400 transition-all"
|
||||
},
|
||||
"text": "{{report_detail?.data?.board?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrow-up-right-from-square",
|
||||
"className": "w-3 h-3"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!report_detail?.data?.board?.slug}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{report_detail?.data?.board?.name}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-400 dark:text-gray-500/80"
|
||||
},
|
||||
"text": "($t:sirsoft-board.admin.reports.detail.board_deleted)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_post_title",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{report_detail?.data?.target_type === 'comment'}}",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.post_title_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{report_detail?.data?.board?.slug && report_detail?.data?.post?.id}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 text-sm font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer transition-colors group"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{report_detail?.data?.board?.slug}}/post/{{report_detail?.data?.post?.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "border-b border-transparent group-hover:border-blue-600 dark:group-hover:border-blue-400 transition-all"
|
||||
},
|
||||
"text": "{{report_detail?.data?.post?.title}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrow-up-right-from-square",
|
||||
"className": "w-3 h-3 transform translate-x-0 group-hover:translate-x-0.5 transition-all"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!report_detail?.data?.board?.slug || !report_detail?.data?.post?.id}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{report_detail?.data?.post?.title}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_title",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{report_detail?.data?.target_type === 'post'}}",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.title_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{report_detail?.data?.board?.slug && report_detail?.data?.post?.id}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 text-sm font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer transition-colors group"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/board/{{report_detail?.data?.board?.slug}}/post/{{report_detail?.data?.post?.id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "border-b border-transparent group-hover:border-blue-600 dark:group-hover:border-blue-400 transition-all"
|
||||
},
|
||||
"text": "{{report_detail?.data?.post?.title}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "arrow-up-right-from-square",
|
||||
"className": "w-3 h-3 transform translate-x-0 group-hover:translate-x-0.5 transition-all"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!report_detail?.data?.board?.slug || !report_detail?.data?.post?.id}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{report_detail?.data?.post?.title}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_created_at",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "{{report_detail?.data?.target_type === 'post' ? '$t:sirsoft-board.admin.reports.detail.post_created_at_label' : '$t:sirsoft-board.admin.reports.detail.comment_created_at_label'}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "clock",
|
||||
"className": "w-4 h-4 text-gray-500 dark:text-gray-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.created_at : report_detail?.data?.comment?.created_at}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reported_content_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "pt-4 mt-4 border-t border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.reported_content_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-gray-50 dark:bg-gray-900 rounded-lg p-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "HtmlContent",
|
||||
"props": {
|
||||
"content": "{{report_detail?.data?.target_type === 'post' ? report_detail?.data?.post?.content : report_detail?.data?.comment?.content}}",
|
||||
"isHtml": "{{report_detail?.data?.target_type === 'post' && (report_detail?.data?.post?.content_mode ?? 'text') === 'html'}}",
|
||||
"className": "text-sm text-gray-900 dark:text-gray-100"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reporters_list_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "reporters_list_header",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H2",
|
||||
"props": {
|
||||
"className": "text-lg font-bold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.reports_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{report_detail?.data}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-3 py-1.5 rounded text-xs font-bold bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.report_count_with_unit|count={{reporters_list?.data?.pagination?.total ?? report_detail?.data?.report_count ?? 0}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reporters_list_container",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": "{{_local.reporters_loading}}",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-3"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "reporters_list?.data?.data ?? report_detail?.data?.reporters ?? []",
|
||||
"item_var": "reporter"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "reporter_item_card",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "card_inner_grid",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "grid grid-cols-2 lg:grid-cols-5 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "reporter_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.reporter_label"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "ActionMenu",
|
||||
"if": "{{reporter.reporter}}",
|
||||
"props": {
|
||||
"triggerLabel": "{{reporter.reporter?.name ?? '-'}}",
|
||||
"triggerIconName": "",
|
||||
"position": "right",
|
||||
"items": [
|
||||
{
|
||||
"id": "view_reporter",
|
||||
"label": "$t:sirsoft-board.admin.reports.detail.user_info",
|
||||
"iconName": "user"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onItemClick",
|
||||
"type": "click",
|
||||
"handler": "switch",
|
||||
"value": "{{$args[0]}}",
|
||||
"cases": {
|
||||
"view_reporter": {
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/users/{{reporter.reporter?.uuid}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!reporter.reporter}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.deleted_user"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{reporter.reporter?.email}}",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
},
|
||||
"text": "{{reporter.reporter?.email}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reported_at_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.reported_at_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "{{reporter.reported_at}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reason_type_field",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.reason_type_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.enums.report_reason_type.{{reporter.reason_type}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reporter_item_reason",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "md:col-span-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.reports.detail.reason_detail_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300 line-clamp-3"
|
||||
},
|
||||
"text": "{{reporter.reason_detail}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reporters_pagination",
|
||||
"type": "composite",
|
||||
"name": "Pagination",
|
||||
"if": "{{(reporters_list?.data?.pagination?.last_page ?? 1) > 1}}",
|
||||
"props": {
|
||||
"currentPage": "{{reporters_list?.data?.pagination?.current_page ?? 1}}",
|
||||
"totalPages": "{{reporters_list?.data?.pagination?.last_page ?? 1}}",
|
||||
"className": "mt-4 justify-end"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onPageChange",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"reporters_page": "{{$args[0]}}",
|
||||
"reporters_loading": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "reporters_list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"reporters_loading": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 일괄 적용 확인 모달"
|
||||
},
|
||||
"id": "bulk_apply_confirm_modal",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.settings.bulk_apply_modal.title",
|
||||
"size": "lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "저장/되돌릴수없음 경고 배너",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": { "className": "text-sm text-amber-800 dark:text-amber-200" },
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "권한 항목 선택 시 안내 배너",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(function() { const fields = _global.bulkApplySnapshot?.fields ?? []; return fields.some(f => f.includes('.') || f === 'manager'); })()}}",
|
||||
"props": {
|
||||
"className": "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "text-blue-600 dark:text-blue-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": { "className": "text-sm text-blue-800 dark:text-blue-200" },
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.permission_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "선택된 항목 목록 (섹션별 그루핑)",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-4 py-2.5 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "list-check",
|
||||
"size": "sm",
|
||||
"className": "text-gray-500 dark:text-gray-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.selected_fields"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "inline-flex items-center justify-center min-w-5 h-5 px-1.5 bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 text-xs font-bold rounded-full"
|
||||
},
|
||||
"text": "{{(_global.bulkApplySnapshot?.fields ?? []).length}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "max-h-48 overflow-y-auto"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!(_global.bulkApplySnapshot?.fields ?? []).length}}",
|
||||
"props": {
|
||||
"className": "block p-3 text-sm text-gray-400 dark:text-gray-500 italic"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.no_fields"
|
||||
},
|
||||
{
|
||||
"comment": "섹션별 그루핑 iteration",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "(function() { const fields = _global.bulkApplySnapshot?.fields ?? []; const sectionMap = { type:'basic', show_view_count:'basic', secret_mode:'basic', use_comment:'basic', use_reply:'basic', use_report:'basic', per_page:'list', per_page_mobile:'list', order_by:'list', order_direction:'list', min_title_length:'post', max_title_length:'post', min_content_length:'post', max_content_length:'post', max_reply_depth:'post', blocked_keywords:'post', new_display_hours:'post', comment_order:'comment', min_comment_length:'comment', max_comment_length:'comment', max_comment_depth:'comment', use_file_upload:'attachment', max_file_size:'attachment', max_file_count:'attachment', allowed_extensions:'attachment', notify_admin_on_post:'notification', notify_author:'notification', manager:'permissions' }; const sectionOrder = ['basic','list','post','comment','attachment','notification','permissions']; const grouped = {}; fields.forEach(k => { const sk = k.includes('.') ? 'permissions' : (sectionMap[k] ?? 'basic'); if (!grouped[sk]) grouped[sk] = []; grouped[sk].push(k); }); return sectionOrder.filter(sk => grouped[sk]).map(sk => ({ sectionKey: sk, items: grouped[sk] })); })()",
|
||||
"item_var": "sectionGroup"
|
||||
},
|
||||
"props": {
|
||||
"className": "border-b border-gray-100 dark:border-gray-700/50 last:border-b-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-3 py-1.5 bg-gray-100 dark:bg-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": { "className": "text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide" },
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.{{sectionGroup.sectionKey}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "px-3 py-2 flex flex-wrap gap-1.5" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "sectionGroup.items",
|
||||
"item_var": "fieldKey"
|
||||
},
|
||||
"props": {
|
||||
"className": "inline-flex items-center px-2 py-0.5 bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 text-xs rounded"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!fieldKey.includes('.') && fieldKey !== 'manager'}}",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.field_labels.{{fieldKey}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{fieldKey.includes('.') || fieldKey === 'manager'}}",
|
||||
"text": "$t:sirsoft-board.board.permissions.{{fieldKey}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "선택된 게시판 목록 (전체/선택 무관 항상 표시)",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-4 py-2.5 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "table-columns",
|
||||
"size": "sm",
|
||||
"className": "text-gray-500 dark:text-gray-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.selected_boards"
|
||||
},
|
||||
{
|
||||
"comment": "선택 게시판 개수 배지",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_global.bulkApplySnapshot?.applyAll === false}}",
|
||||
"props": {
|
||||
"className": "inline-flex items-center justify-center min-w-5 h-5 px-1.5 bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 text-xs font-bold rounded-full"
|
||||
},
|
||||
"text": "{{(_global.bulkApplySnapshot?.boardIds ?? []).length}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-3 flex flex-wrap gap-1.5 min-h-12"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_global.bulkApplySnapshot?.applyAll !== false}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400 italic"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.all_boards"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_global.bulkApplySnapshot?.applyAll === false && !(_global.bulkApplySnapshot?.boardIds ?? []).length}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-400 dark:text-gray-500 italic"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.no_boards"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_global.bulkApplySnapshot?.applyAll === false}}",
|
||||
"iteration": {
|
||||
"source": "(_global.bulkApplySnapshot?.boardIds ?? []).map(id => { const opt = ($parent._computed.boardOptions ?? []).find(o => o.value == id); return opt ? opt.label : id; })",
|
||||
"item_var": "boardName"
|
||||
},
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 text-xs font-medium rounded-full"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{boardName}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "저장 실패 에러 표시",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{$parent._local.bulkApplySaveError}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm text-red-600 dark:text-red-400 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": { "name": "circle-exclamation", "className": "w-4 h-4 flex-shrink-0" }
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.save_error"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-3 pt-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"disabled": "{{$parent._local.isBulkApplying}}",
|
||||
"className": "px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bulk_apply_confirm_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"disabled": "{{$parent._local.isBulkApplying || !(_global.bulkApplySnapshot?.fields ?? []).length || (_global.bulkApplySnapshot?.applyAll === false && !(_global.bulkApplySnapshot?.boardIds ?? []).length)}}",
|
||||
"className": "flex items-center gap-1.5 px-4 py-2 bg-amber-600 dark:bg-amber-700 text-white rounded-lg hover:bg-amber-700 dark:hover:bg-amber-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"comment": "스냅샷은 모달 열기 전(탭 버튼 클릭 시) 이미 _global에 저장됨 - 여기서는 진행 상태만 변경",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isBulkApplying": true,
|
||||
"bulkApplySaveError": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/settings",
|
||||
"params": {
|
||||
"method": "PUT",
|
||||
"body": "{{(function() { const snap = _global.bulkApplySnapshot || {}; const tab = snap.tab || 'basic_defaults'; const form = snap.form || {}; if (tab === 'report_policy') { return { _tab: 'report_policy', report_policy: form.report_policy || {} }; } if (tab === 'spam_security') { return { _tab: 'spam_security', spam_security: form.spam_security || {} }; } return { _tab: 'basic_defaults', basic_defaults: form.basic_defaults || {}, notification_channels: form.notification_channels || {} }; })()}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"hasChanges": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/settings/bulk-apply",
|
||||
"params": {
|
||||
"method": "POST",
|
||||
"body": "{{(function() { const snap = _global.bulkApplySnapshot || {}; return { fields: snap.fields ?? [], apply_all: snap.applyAll !== false, board_ids: snap.applyAll !== false ? [] : (snap.boardIds ?? []) }; })()}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isBulkApplying": false,
|
||||
"bulkApplyFields": [],
|
||||
"bulkApplyAll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bulkApplySnapshot": null
|
||||
}
|
||||
},
|
||||
{ "handler": "closeModal" },
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:sirsoft-board.admin.settings.bulk_apply_modal.success|count={{response?.data?.updated_count ?? 0}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isBulkApplying": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bulkApplySnapshot": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "$t:sirsoft-board.admin.settings.bulk_apply_modal.error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isBulkApplying": false,
|
||||
"bulkApplySaveError": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bulkApplySnapshot": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{$parent._local.isBulkApplying}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "w-4 h-4 animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!$parent._local.isBulkApplying}}",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.confirm"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{$parent._local.isBulkApplying}}",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply_modal.processing"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+641
@@ -0,0 +1,641 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 메일 템플릿 편집 모달"
|
||||
},
|
||||
"id": "modal_board_mail_template_edit",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.title",
|
||||
"size": "xl",
|
||||
"closeOnOverlayClick": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_edit_form_container",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": "{{$parent._local.isSaving}}",
|
||||
"props": {
|
||||
"className": "flex flex-col max-h-[70vh]"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{$parent._computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1 overflow-y-auto space-y-5 pr-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_validation_error_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{$parent._local.templateErrors}}",
|
||||
"props": {
|
||||
"className": "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"size": "sm",
|
||||
"className": "text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.validation_error"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Ul",
|
||||
"if": "{{typeof $parent._local.templateErrors === 'object' && Object.keys($parent._local.templateErrors).length > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 list-disc list-inside space-y-1"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "Object.entries($parent._local.templateErrors ?? {}).flatMap(([field, messages]) => Array.isArray(messages) ? messages : [messages])",
|
||||
"item_var": "errMsg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Li",
|
||||
"props": {
|
||||
"className": "text-sm text-red-700 dark:text-red-300"
|
||||
},
|
||||
"text": "{{errMsg}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-500 dark:text-gray-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.type_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm font-semibold text-gray-900 dark:text-white"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.types.{{$parent._local.editingTemplate?.type ?? ''}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs font-mono px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded"
|
||||
},
|
||||
"text": "{{$parent._local.editingTemplate?.type ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "{{($parent._local.editLang ?? $locale) === 'ko' ? 'border-b-2 border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}} px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
},
|
||||
"text": "한국어",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editLang": "ko"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "{{($parent._local.editLang ?? $locale) === 'en' ? 'border-b-2 border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}} px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
},
|
||||
"text": "English",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editLang": "en"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_subject_ko",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{($parent._local.editLang ?? $locale) === 'ko'}}",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.subject"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 ml-1"
|
||||
},
|
||||
"text": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"value": "{{$parent._local.editingTemplate?.subject?.ko ?? ''}}",
|
||||
"className": "{{$parent._local.templateErrors?.['subject.ko'] ? 'w-full px-3 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-red-500 focus:border-red-500' : 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500'}}",
|
||||
"disabled": "{{$parent._computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editingTemplate.subject.ko": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{$parent._local.templateErrors?.['subject.ko']}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{$parent._local.templateErrors?.['subject.ko']?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_subject_en",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{($parent._local.editLang ?? $locale) === 'en'}}",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.subject"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"value": "{{$parent._local.editingTemplate?.subject?.en ?? ''}}",
|
||||
"className": "{{$parent._local.templateErrors?.['subject.en'] ? 'w-full px-3 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-red-500 focus:border-red-500' : 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500'}}",
|
||||
"disabled": "{{$parent._computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editingTemplate.subject.en": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{$parent._local.templateErrors?.['subject.en']}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{$parent._local.templateErrors?.['subject.en']?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_body_ko",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{($parent._local.editLang ?? $locale) === 'ko'}}",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.body"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 ml-1"
|
||||
},
|
||||
"text": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "HtmlEditor",
|
||||
"props": {
|
||||
"content": "{{$parent._local.editingTemplate?.body?.ko ?? ''}}",
|
||||
"isHtml": true,
|
||||
"showHtmlModeToggle": false,
|
||||
"className": "{{$parent._local.templateErrors?.['body.ko'] ? 'min-h-[200px] border border-red-500 rounded-md' : 'min-h-[200px]'}}",
|
||||
"disabled": "{{$parent._computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editingTemplate.body.ko": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{$parent._local.templateErrors?.['body.ko']}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{$parent._local.templateErrors?.['body.ko']?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_body_en",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{($parent._local.editLang ?? $locale) === 'en'}}",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.body"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "HtmlEditor",
|
||||
"props": {
|
||||
"content": "{{$parent._local.editingTemplate?.body?.en ?? ''}}",
|
||||
"isHtml": true,
|
||||
"showHtmlModeToggle": false,
|
||||
"className": "{{$parent._local.templateErrors?.['body.en'] ? 'min-h-[200px] border border-red-500 rounded-md' : 'min-h-[200px]'}}",
|
||||
"disabled": "{{$parent._computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editingTemplate.body.en": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{$parent._local.templateErrors?.['body.en']}}",
|
||||
"props": {
|
||||
"className": "text-red-500 dark:text-red-400 text-xs mt-1"
|
||||
},
|
||||
"text": "{{$parent._local.templateErrors?.['body.en']?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_is_active_toggle",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "boardTemplateIsActive",
|
||||
"checked": "{{$parent._local.editingTemplate?.is_active ?? true}}",
|
||||
"disabled": "{{$parent._computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"editingTemplate.is_active": "{{$event.target.checked}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.is_active_label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_variables_info",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{($parent._local.editingTemplate?.variables ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md p-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H4",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.edit_modal.variables_title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-wrap gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"iteration": {
|
||||
"source": "{{$parent._local.editingTemplate?.variables ?? []}}",
|
||||
"item_var": "v",
|
||||
"index_var": "vi"
|
||||
},
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1 text-xs px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{v.key}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "} - {{v.description ?? ''}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_edit_modal_footer",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end items-center pt-4 border-t border-gray-200 dark:border-gray-700 mt-4 flex-shrink-0 sticky bottom-0 bg-white dark:bg-gray-800 pb-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_btn_cancel_edit",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-sm px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{$parent._local.isSaving ?? false}}"
|
||||
},
|
||||
"text": "$t:common.cancel",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal",
|
||||
"target": "modal_board_mail_template_edit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_btn_save_template",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "flex items-center gap-2 text-sm px-4 py-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"disabled": "{{$parent._computed.isReadOnly || $parent._local.isSaving ?? false}}"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{$parent._local.isSaving}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "animate-spin",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{$parent._local.isSaving ? '$t:common.saving' : '$t:common.save'}}"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isSaving": true,
|
||||
"templateErrors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "{{'/api/modules/sirsoft-board/admin/mail-templates/' + $parent._local.editingTemplate?.id}}",
|
||||
"params": {
|
||||
"method": "PUT",
|
||||
"body": "{{(function() { const tpl = $parent._local.editingTemplate ?? {}; return { subject: tpl.subject ?? {}, body: tpl.body ?? {}, is_active: tpl.is_active ?? true }; })()}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isSaving": false,
|
||||
"templateErrors": null,
|
||||
"editingTemplate": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "closeModal",
|
||||
"target": "modal_board_mail_template_edit"
|
||||
},
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"message": "$t:sirsoft-board.admin.settings.mail_templates.save_success",
|
||||
"type": "success"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "$parent._local",
|
||||
"isSaving": false,
|
||||
"templateErrors": "{{error.errors}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"message": "$t:sirsoft-board.admin.settings.mail_templates.save_error",
|
||||
"type": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 메일 템플릿 미리보기 모달"
|
||||
},
|
||||
"id": "modal_board_mail_template_preview",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-board.admin.settings.mail_templates.preview_modal.title",
|
||||
"size": "lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_preview_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.preview_modal.subject_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-base font-medium text-gray-900 dark:text-white bg-gray-50 dark:bg-gray-900 rounded-md px-3 py-2"
|
||||
},
|
||||
"text": "{{_local.previewData?.subject ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.mail_templates.preview_modal.body_label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden",
|
||||
"dangerouslySetInnerHTML": "{{_local.previewData?.body ?? ''}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end pt-3 border-t border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_btn_close_preview",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-sm px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
|
||||
},
|
||||
"text": "$t:common.close",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal",
|
||||
"target": "modal_board_mail_template_preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+3363
File diff suppressed because it is too large.
Load diff
+151
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 첨부파일 탭"
|
||||
},
|
||||
"id": "attachment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_attachment",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-2 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.attachment"
|
||||
},
|
||||
{
|
||||
"id": "field_use_file_upload",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('use_file_upload') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('use_file_upload') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'use_file_upload') : [...(_local.bulkApplyFields ?? []), 'use_file_upload']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('use_file_upload')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.use_file_upload" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.use_file_upload" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "composite", "name": "Toggle", "props": { "name": "basic_defaults.use_file_upload", "className": "flex-shrink-0", "disabled": "{{_computed.isReadOnly}}" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_max_file_size",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('max_file_size') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('max_file_size') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'max_file_size') : [...(_local.bulkApplyFields ?? []), 'max_file_size']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('max_file_size')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.max_file_size" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.max_file_size" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "basic", "name": "Div", "props": { "className": "flex-shrink-0" }, "children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "number", "name": "basic_defaults.max_file_size", "min": 1, "className": "{{(_local.errors?.['basic_defaults.max_file_size']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}", "disabled": "{{_computed.isReadOnly}}" } },
|
||||
{ "type": "basic", "name": "Span", "if": "{{_local.errors?.['basic_defaults.max_file_size']}}", "props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" }, "text": "{{(Array.isArray(_local.errors?.['basic_defaults.max_file_size']) ? _local.errors['basic_defaults.max_file_size'][0] : _local.errors?.['basic_defaults.max_file_size']) ?? ''}}" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_max_file_count",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('max_file_count') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('max_file_count') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'max_file_count') : [...(_local.bulkApplyFields ?? []), 'max_file_count']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('max_file_count')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.max_file_count" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.max_file_count" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "basic", "name": "Div", "props": { "className": "flex-shrink-0" }, "children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "number", "name": "basic_defaults.max_file_count", "min": 1, "max": 20, "className": "{{(_local.errors?.['basic_defaults.max_file_count']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}", "disabled": "{{_computed.isReadOnly}}" } },
|
||||
{ "type": "basic", "name": "Span", "if": "{{_local.errors?.['basic_defaults.max_file_count']}}", "props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" }, "text": "{{(Array.isArray(_local.errors?.['basic_defaults.max_file_count']) ? _local.errors['basic_defaults.max_file_count'][0] : _local.errors?.['basic_defaults.max_file_count']) ?? ''}}" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_allowed_extensions",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('allowed_extensions') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "px-3 py-2" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer mb-2" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('allowed_extensions') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'allowed_extensions') : [...(_local.bulkApplyFields ?? []), 'allowed_extensions']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('allowed_extensions')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.allowed_extensions" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.allowed_extensions" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite", "name": "TagInput",
|
||||
"props": {
|
||||
"name": "basic_defaults.allowed_extensions",
|
||||
"placeholder": "$t:sirsoft-board.admin.settings.fields.allowed_extensions_placeholder",
|
||||
"creatable": true,
|
||||
"className": "w-full",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 기본 탭 (기본 설정 + 일괄 적용)"
|
||||
},
|
||||
"id": "basic",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "========== 페이지 안내 배너 ==========",
|
||||
"id": "page_info_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-3 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "text-blue-500 dark:text-blue-400 flex-shrink-0 mt-0.5",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-blue-700 dark:text-blue-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.page_info"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "========== 섹션 1: 기본 설정 ==========",
|
||||
"id": "section_basic",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-1 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.basic"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-500 dark:text-gray-400 mb-4"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.basic_description"
|
||||
},
|
||||
{
|
||||
"id": "field_type",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('type') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('type') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'type') : [...(_local.bulkApplyFields ?? []), 'type']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('type')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.type"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "settings_btn_manage_board_types",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"variant": "ghost",
|
||||
"size": "sm",
|
||||
"className": "inline-flex items-center gap-1 text-xs rounded-full px-2.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "gear",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.board_types.manage"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"btFormMode": "add",
|
||||
"btEditingItem": null,
|
||||
"btFormSlug": "",
|
||||
"btFormName": {
|
||||
"ko": "",
|
||||
"en": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/modules/sirsoft-board/admin/board-types",
|
||||
"params": {
|
||||
"method": "GET"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"boardTypesList": "{{response.data}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "board_type_manage_modal"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 pl-6 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.type"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "basic_defaults.type",
|
||||
"options": "{{board_types?.data?.map(t => ({value: t.slug, label: (t.name[_global.locale ?? 'ko'] ?? t.name.ko ?? t.slug) + ' (' + t.slug + ')'})) ?? []}}",
|
||||
"className": "w-48 flex-shrink-0 text-sm",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_secret_mode",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('secret_mode') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('secret_mode') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'secret_mode') : [...(_local.bulkApplyFields ?? []), 'secret_mode']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('secret_mode')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.secret_mode"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.secret_mode"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "basic_defaults.secret_mode",
|
||||
"options": [
|
||||
{ "value": "disabled", "label": "$t:sirsoft-board.admin.settings.options.secret_mode.disabled" },
|
||||
{ "value": "enabled", "label": "$t:sirsoft-board.admin.settings.options.secret_mode.enabled" },
|
||||
{ "value": "always", "label": "$t:sirsoft-board.admin.settings.options.secret_mode.always" }
|
||||
],
|
||||
"className": "w-48 flex-shrink-0 text-sm",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_show_view_count",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('show_view_count') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('show_view_count') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'show_view_count') : [...(_local.bulkApplyFields ?? []), 'show_view_count']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('show_view_count')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.show_view_count"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.show_view_count"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "basic_defaults.show_view_count",
|
||||
"className": "flex-shrink-0",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_use_report",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('use_report') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('use_report') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'use_report') : [...(_local.bulkApplyFields ?? []), 'use_report']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('use_report')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.use_report"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.use_report"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "basic_defaults.use_report",
|
||||
"className": "flex-shrink-0",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 일괄 적용 섹션"
|
||||
},
|
||||
"id": "bulk_apply",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "========== 섹션 9: 기존 게시판에 일괄 적용 ==========",
|
||||
"id": "section_bulk_apply",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-4 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-800 dark:text-amber-200"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.checked_fields_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{!(_local.bulkApplyFields ?? []).length}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-exclamation",
|
||||
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-700 dark:text-amber-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.no_fields_selected"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_local.bulkApplyFields ?? []).length > 0 && _local.bulkApplyAll === false && !(_local._bulkApplyBoardIds ?? []).length}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-exclamation",
|
||||
"className": "text-red-500 dark:text-red-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-red-600 dark:text-red-400"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.no_boards_selected"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "no_boards_notice",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{!(boards_list?.data?.data ?? []).length}}",
|
||||
"props": {
|
||||
"className": "p-4 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600 rounded-lg flex items-center gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "text-gray-500 dark:text-gray-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-600 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.no_boards_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bulk_apply_target",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "space-y-2 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.target_boards"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "radio",
|
||||
"name": "_bulkApplyAll",
|
||||
"className": "radio",
|
||||
"checked": "{{_local.bulkApplyAll !== false}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyAll": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_all"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "radio",
|
||||
"name": "_bulkApplyAll",
|
||||
"className": "radio",
|
||||
"checked": "{{_local.bulkApplyAll === false}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyAll": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_selected"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.bulkApplyAll === false}}",
|
||||
"props": {
|
||||
"className": "mt-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"value": "{{_local._bulkApplyBoardIds ?? []}}",
|
||||
"options": "{{$computed.boardOptions ?? []}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.settings.bulk_apply.select_boards_placeholder",
|
||||
"className": "w-full",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"_bulkApplyBoardIds": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "flex justify-end"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"disabled": "{{_computed.isReadOnly || !(_local.bulkApplyFields ?? []).length || (_local.bulkApplyAll === false && !(_local._bulkApplyBoardIds ?? []).length)}}",
|
||||
"className": "flex items-center gap-2 px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"comment": "모달 열기 전 현재 _local 상태를 _global 스냅샷에 저장 + 오류 상태 초기화",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bulkApplySnapshot": "{{(function() { const fields = _local.bulkApplyFields ?? []; const applyAll = _local.bulkApplyAll !== false; const boardIds = _local._bulkApplyBoardIds ?? []; const tab = _global.activeBoardSettingsTab || 'basic_defaults'; const form = _local.form || {}; return { fields: fields, applyAll: applyAll, boardIds: boardIds, tab: tab, form: form }; })()}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "이전 오류 메시지 초기화",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplySaveError": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "openModal",
|
||||
"target": "bulk_apply_confirm_modal"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_button"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 댓글 탭"
|
||||
},
|
||||
"id": "comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_comment",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-2 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.comment"
|
||||
},
|
||||
{
|
||||
"id": "field_use_comment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('use_comment') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('use_comment') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'use_comment') : [...(_local.bulkApplyFields ?? []), 'use_comment']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('use_comment')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.use_comment" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.use_comment" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "composite", "name": "Toggle", "props": { "name": "basic_defaults.use_comment", "className": "flex-shrink-0", "disabled": "{{_computed.isReadOnly}}" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_max_comment_depth",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('max_comment_depth') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('max_comment_depth') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'max_comment_depth') : [...(_local.bulkApplyFields ?? []), 'max_comment_depth']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('max_comment_depth')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.max_comment_depth" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.max_comment_depth" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "basic", "name": "Div", "props": { "className": "flex-shrink-0" }, "children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "number", "name": "basic_defaults.max_comment_depth", "min": 0, "max": 10, "className": "{{(_local.errors?.['basic_defaults.max_comment_depth']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}", "disabled": "{{_computed.isReadOnly}}" } },
|
||||
{ "type": "basic", "name": "Span", "if": "{{_local.errors?.['basic_defaults.max_comment_depth']}}", "props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" }, "text": "{{(Array.isArray(_local.errors?.['basic_defaults.max_comment_depth']) ? _local.errors['basic_defaults.max_comment_depth'][0] : _local.errors?.['basic_defaults.max_comment_depth']) ?? ''}}" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_comment_order",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('comment_order') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('comment_order') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'comment_order') : [...(_local.bulkApplyFields ?? []), 'comment_order']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('comment_order')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.comment_order" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.comment_order" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "composite", "name": "Select", "props": { "name": "basic_defaults.comment_order", "options": [{ "value": "ASC", "label": "$t:sirsoft-board.admin.settings.options.comment_order.ASC" }, { "value": "DESC", "label": "$t:sirsoft-board.admin.settings.options.comment_order.DESC" }], "className": "w-48 flex-shrink-0 text-sm", "disabled": "{{_computed.isReadOnly}}" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_min_comment_length",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('min_comment_length') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('min_comment_length') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'min_comment_length') : [...(_local.bulkApplyFields ?? []), 'min_comment_length']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('min_comment_length')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.min_comment_length" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.min_comment_length" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "basic", "name": "Div", "props": { "className": "flex-shrink-0" }, "children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "number", "name": "basic_defaults.min_comment_length", "min": 1, "max": 1000, "className": "{{(_local.errors?.['basic_defaults.min_comment_length']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}", "disabled": "{{_computed.isReadOnly}}" } },
|
||||
{ "type": "basic", "name": "Span", "if": "{{_local.errors?.['basic_defaults.min_comment_length']}}", "props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" }, "text": "{{(Array.isArray(_local.errors?.['basic_defaults.min_comment_length']) ? _local.errors['basic_defaults.min_comment_length'][0] : _local.errors?.['basic_defaults.min_comment_length']) ?? ''}}" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_max_comment_length",
|
||||
"type": "basic", "name": "Div",
|
||||
"props": { "className": "{{(_local.bulkApplyFields ?? []).includes('max_comment_length') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Div", "props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic", "name": "Label", "props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer" },
|
||||
"actions": [{ "type": "click", "handler": "setState", "params": { "target": "local", "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('max_comment_length') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'max_comment_length') : [...(_local.bulkApplyFields ?? []), 'max_comment_length']}}" } }],
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "checkbox", "className": "checkbox flex-shrink-0 mt-0.5", "checked": "{{(_local.bulkApplyFields ?? []).includes('max_comment_length')}}" } },
|
||||
{ "type": "basic", "name": "Div", "children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.max_comment_length" },
|
||||
{ "type": "basic", "name": "P", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" }, "text": "$t:sirsoft-board.admin.settings.fields.descriptions.max_comment_length" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ "type": "basic", "name": "Div", "props": { "className": "flex-shrink-0" }, "children": [
|
||||
{ "type": "basic", "name": "Input", "props": { "type": "number", "name": "basic_defaults.max_comment_length", "min": 1, "max": 10000, "className": "{{(_local.errors?.['basic_defaults.max_comment_length']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}", "disabled": "{{_computed.isReadOnly}}" } },
|
||||
{ "type": "basic", "name": "Span", "if": "{{_local.errors?.['basic_defaults.max_comment_length']}}", "props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" }, "text": "{{(Array.isArray(_local.errors?.['basic_defaults.max_comment_length']) ? _local.errors['basic_defaults.max_comment_length'][0] : _local.errors?.['basic_defaults.max_comment_length']) ?? ''}}" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 목록 탭"
|
||||
},
|
||||
"id": "list",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_list",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-2 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.list"
|
||||
},
|
||||
{
|
||||
"id": "field_order_by",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('order_by') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('order_by') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'order_by') : [...(_local.bulkApplyFields ?? []), 'order_by']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('order_by')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.order_by"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.order_by"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "basic_defaults.order_by",
|
||||
"options": [
|
||||
{ "value": "created_at", "label": "$t:sirsoft-board.admin.settings.options.order_by.created_at" },
|
||||
{ "value": "view_count", "label": "$t:sirsoft-board.admin.settings.options.order_by.view_count" },
|
||||
{ "value": "title", "label": "$t:sirsoft-board.admin.settings.options.order_by.title" },
|
||||
{ "value": "author", "label": "$t:sirsoft-board.admin.settings.options.order_by.author" }
|
||||
],
|
||||
"className": "w-48 flex-shrink-0 text-sm",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_order_direction",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('order_direction') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('order_direction') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'order_direction') : [...(_local.bulkApplyFields ?? []), 'order_direction']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('order_direction')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.order_direction"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.order_direction"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "basic_defaults.order_direction",
|
||||
"options": [
|
||||
{ "value": "DESC", "label": "$t:sirsoft-board.admin.settings.options.order_direction.DESC" },
|
||||
{ "value": "ASC", "label": "$t:sirsoft-board.admin.settings.options.order_direction.ASC" }
|
||||
],
|
||||
"className": "w-48 flex-shrink-0 text-sm",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_per_page",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('per_page') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('per_page') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'per_page') : [...(_local.bulkApplyFields ?? []), 'per_page']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('per_page')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.per_page"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.per_page"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "flex-shrink-0" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "number",
|
||||
"name": "basic_defaults.per_page",
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"className": "{{(_local.errors?.['basic_defaults.per_page']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.['basic_defaults.per_page']}}",
|
||||
"props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" },
|
||||
"text": "{{(Array.isArray(_local.errors?.['basic_defaults.per_page']) ? _local.errors['basic_defaults.per_page'][0] : _local.errors?.['basic_defaults.per_page']) ?? ''}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_per_page_mobile",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('per_page_mobile') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-4 px-3 py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('per_page_mobile') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'per_page_mobile') : [...(_local.bulkApplyFields ?? []), 'per_page_mobile']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('per_page_mobile')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.per_page_mobile"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.per_page_mobile"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "flex-shrink-0" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "number",
|
||||
"name": "basic_defaults.per_page_mobile",
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"className": "{{(_local.errors?.['basic_defaults.per_page_mobile']) ? 'w-32 px-3 py-2 text-sm border border-red-500 dark:border-red-500 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white' : 'w-32 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white'}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.['basic_defaults.per_page_mobile']}}",
|
||||
"props": { "className": "text-xs text-red-600 dark:text-red-400 block mt-1" },
|
||||
"text": "{{(Array.isArray(_local.errors?.['basic_defaults.per_page_mobile']) ? _local.errors['basic_defaults.per_page_mobile'][0] : _local.errors?.['basic_defaults.per_page_mobile']) ?? ''}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 알림 탭"
|
||||
},
|
||||
"id": "notification",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_notification",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-2 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.notification"
|
||||
},
|
||||
{
|
||||
"id": "field_notify_author",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('notify_author') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer" },
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('notify_author') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'notify_author' && f !== 'notify_author_channels') : [...(_local.bulkApplyFields ?? []), 'notify_author', 'notify_author_channels']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('notify_author')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.notify_author" },
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" },
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.notify_author_description"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "basic_defaults.notify_author",
|
||||
"className": "flex-shrink-0",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_author_channels",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "flex items-center gap-3 px-3 pb-2 ml-6" },
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Label", "props": { "className": "text-sm text-gray-600 dark:text-gray-400 flex-shrink-0" }, "text": "$t:sirsoft-board.admin.settings.notification_channels.channels_label" },
|
||||
{ "type": "composite", "name": "TagInput", "props": { "name": "basic_defaults.notify_author_channels", "options": "{{notification_channels?.data ?? []}}", "className": "w-64 flex-shrink-0" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_admin_on_post",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes('notify_admin_on_post') ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'}} transition-colors"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "flex items-center gap-4 px-3 py-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": { "className": "flex items-start gap-2 text-sm font-medium text-gray-900 dark:text-white flex-1 cursor-pointer" },
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('notify_admin_on_post') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'notify_admin_on_post' && f !== 'notify_admin_on_post_channels') : [...(_local.bulkApplyFields ?? []), 'notify_admin_on_post', 'notify_admin_on_post_channels']}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('notify_admin_on_post')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-board.admin.settings.fields.notify_admin_on_post" },
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal" },
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.notify_admin_on_post_description"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "basic_defaults.notify_admin_on_post",
|
||||
"className": "flex-shrink-0",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_notify_admin_channels",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "flex items-center gap-3 px-3 pb-2 ml-6" },
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Label", "props": { "className": "text-sm text-gray-600 dark:text-gray-400 flex-shrink-0" }, "text": "$t:sirsoft-board.admin.settings.notification_channels.channels_label" },
|
||||
{ "type": "composite", "name": "TagInput", "props": { "name": "basic_defaults.notify_admin_on_post_channels", "options": "{{notification_channels?.data ?? []}}", "className": "w-64 flex-shrink-0" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"meta": {
|
||||
"is_partial": true,
|
||||
"description": "게시판 설정 > 권한 탭"
|
||||
},
|
||||
"id": "permissions",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.activeBoardSettingsTab || query.tab || 'general') === 'basic_defaults'}}",
|
||||
"props": {
|
||||
"className": "mt-6 space-y-6"
|
||||
},
|
||||
"classMap": {
|
||||
"key": "{{_computed.isReadOnly ? 'readonly' : 'normal'}}",
|
||||
"variants": {
|
||||
"readonly": "opacity-60",
|
||||
"normal": ""
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "section_permissions",
|
||||
"type": "layout",
|
||||
"name": "SectionLayout",
|
||||
"props": {
|
||||
"className": "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-lg font-semibold text-gray-900 dark:text-white pb-3 mb-2 border-b-2 border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.sections.permissions"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 mb-3"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.permissions_table.description"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 p-3 mb-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "text-blue-500 dark:text-blue-400 flex-shrink-0",
|
||||
"size": "sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": { "className": "text-xs text-blue-700 dark:text-blue-300" },
|
||||
"text": "$t:sirsoft-board.admin.settings.permissions_table.bulk_apply_notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_admin_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mb-6 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700 cursor-pointer select-none"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => k.startsWith('admin.')).every(k => (_local.bulkApplyFields ?? []).includes(k)) ? (_local.bulkApplyFields ?? []).filter(k => !k.startsWith('admin.')) : (_local.bulkApplyFields ?? []).concat(Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => k.startsWith('admin.'))).filter((v, i, a) => a.indexOf(v) === i)}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 pointer-events-none",
|
||||
"checked": "{{Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => k.startsWith('admin.')).length > 0 && Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => k.startsWith('admin.')).every(k => (_local.bulkApplyFields ?? []).includes(k))}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "shield",
|
||||
"size": "sm",
|
||||
"className": "text-purple-500 dark:text-purple-400 pointer-events-none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-800 dark:text-gray-100 pointer-events-none"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.permissions_table.admin_section"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "divide-y divide-gray-200 dark:divide-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "$computed.adminPermissions ?? []",
|
||||
"item_var": "perm"
|
||||
},
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes(perm?.[0]) ? 'bg-purple-50 dark:bg-purple-900/20' : 'bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700'}} flex flex-col gap-2 px-4 py-2.5 transition-colors cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes(perm?.[0]) ? (_local.bulkApplyFields ?? []).filter(f => f !== perm?.[0]) : [...(_local.bulkApplyFields ?? []), perm?.[0]]}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 pointer-events-none"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes(perm?.[0])}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-0.5"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.[0]}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-400 dark:text-gray-500"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.[0]}}_desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "pl-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "perm_tag_admin_{{perm?.[0]}}",
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"value": "{{(_local.form.basic_defaults.default_board_permissions ?? {})[perm?.[0]] ?? []}}",
|
||||
"options": "{{(roles?.data?.data ?? []).map(r => ({ value: r.identifier, label: r.name ?? r.identifier }))}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.settings.permissions_table.select_roles",
|
||||
"className": "w-full",
|
||||
"defaultVariant": "purple",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"form.basic_defaults.default_board_permissions": "{{Object.fromEntries(Object.entries(_local.form.basic_defaults.default_board_permissions ?? {}).map(([k,v]) => k === perm?.[0] ? [k, $event.target.value] : [k, v]))}}",
|
||||
"hasChanges": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "permissions_user_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700 cursor-pointer select-none"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => !k.startsWith('admin.')).every(k => (_local.bulkApplyFields ?? []).includes(k)) ? (_local.bulkApplyFields ?? []).filter(k => k.startsWith('admin.')) : (_local.bulkApplyFields ?? []).concat(Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => !k.startsWith('admin.'))).filter((v, i, a) => a.indexOf(v) === i)}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 pointer-events-none",
|
||||
"checked": "{{Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => !k.startsWith('admin.')).length > 0 && Object.keys(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(k => !k.startsWith('admin.')).every(k => (_local.bulkApplyFields ?? []).includes(k))}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "users",
|
||||
"size": "sm",
|
||||
"className": "text-blue-500 dark:text-blue-400 pointer-events-none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-800 dark:text-gray-100 pointer-events-none"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.permissions_table.user_section"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "divide-y divide-gray-200 dark:divide-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "$computed.userPermissions ?? []",
|
||||
"item_var": "perm"
|
||||
},
|
||||
"props": {
|
||||
"className": "{{(_local.bulkApplyFields ?? []).includes(perm?.[0]) ? 'bg-blue-50 dark:bg-blue-900/20' : 'bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700'}} flex flex-col gap-2 px-4 py-2.5 transition-colors cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes(perm?.[0]) ? (_local.bulkApplyFields ?? []).filter(f => f !== perm?.[0]) : [...(_local.bulkApplyFields ?? []), perm?.[0]]}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-start gap-2 pointer-events-none"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox flex-shrink-0 mt-0.5",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes(perm?.[0])}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-0.5"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.[0]}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-400 dark:text-gray-500"
|
||||
},
|
||||
"text": "$t:sirsoft-board.board.permissions.{{perm?.[0]}}_desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "pl-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "perm_tag_user_{{perm?.[0]}}",
|
||||
"type": "composite",
|
||||
"name": "TagInput",
|
||||
"props": {
|
||||
"value": "{{(_local.form.basic_defaults.default_board_permissions ?? {})[perm?.[0]] ?? []}}",
|
||||
"options": "{{(roles?.data?.data ?? []).map(r => ({ value: r.identifier, label: r.name ?? r.identifier }))}}",
|
||||
"placeholder": "$t:sirsoft-board.admin.settings.permissions_table.select_roles",
|
||||
"className": "w-full",
|
||||
"defaultVariant": "blue",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"form.basic_defaults.default_board_permissions": "{{Object.fromEntries(Object.entries(_local.form.basic_defaults.default_board_permissions ?? {}).map(([k,v]) => k === perm?.[0] ? [k, $event.target.value] : [k, v]))}}",
|
||||
"hasChanges": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loaded 100 of 3967 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user