feat(board): 게시글 반응(추천/비추천) 프론트엔드 화면
4단계 화면 3면 + 백엔드 수용. - 게시글 상세(show.json): 신고 버튼 위에 반응 영역 — use_reaction && 유형 존재 시 노출, 유형 iteration(아이콘+라벨+개수, 0도 노출), 본인 글 비활성화, 같은 유형 재클릭=해제·다른 유형=전환, 비로그인 안내 후 로그인 유도 - 관리 설정 기본값 탭 + 개별 게시판 편집 폼: 반응 사용 토글 + 유형 체크박스 (reactionTypes data_source iteration, active_reaction_types code 토글) - FormRequest(Store/Update Board, Store/BulkApply Settings)에 use_reaction· active_reaction_types 화이트리스트/캐스트/시드 추가 (미추가 시 토글 미저장) - BoardResource.icon_name(fa-* 토큰) — Icon 컴포넌트 접두사 중복 방지 - ko/en 다국어 + boards/settings API 문서 갱신 - Vitest 10건(템플릿 4 + 모듈 6) green, board 관리/설정 87건 회귀 green ja 언어팩·시나리오 매니페스트·E2E·버전 bump·CHANGELOG 는 5단계.
This commit is contained in:
1 parent
4060a91d28
commit
2ba0c2ba2d
22 files changed
+662
-6
No files matched your search
@@ -134,6 +134,8 @@ HTTP/1.1 200
|
||||
| use_comment | body | boolean | 예 | — | 댓글 기능 사용 여부 |
|
||||
| use_reply | body | boolean | 예 | — | 게시글 답변(원글에 대한 답글) 기능 사용 여부 |
|
||||
| use_report | body | boolean | 예 | — | 게시글/댓글 신고 기능 사용 여부 |
|
||||
| use_reaction | body | boolean | 예 | — | 게시글 반응(추천/비추천) 기능 사용 여부 |
|
||||
| active_reaction_types | body | array | 아니오 | — | 이 게시판에서 켠 반응 유형 code 목록 (문자열 배열, 예: `["like","dislike"]`) |
|
||||
| comment_order | body | string | 예 | `ASC`, `DESC` | 댓글 정렬 순서 (ASC 오름차순 / DESC 내림차순) |
|
||||
| new_display_hours | body | integer | 아니오 | min 0, max 720 | 신규(NEW) 표시를 유지할 기간 (시간 단위, 최대 720시간=30일). `0` 은 NEW 배지를 표시하지 않음 |
|
||||
| min_title_length | body | integer | 아니오 | min 0, max 200 | 게시글 제목 최소 글자 수 |
|
||||
@@ -1251,6 +1253,8 @@ HTTP/1.1 200
|
||||
| use_comment | body | boolean | 예 | — | 댓글 기능 사용 여부 |
|
||||
| use_reply | body | boolean | 예 | — | 게시글 답변(원글에 대한 답글) 기능 사용 여부 |
|
||||
| use_report | body | boolean | 예 | — | 게시글/댓글 신고 기능 사용 여부 |
|
||||
| use_reaction | body | boolean | 예 | — | 게시글 반응(추천/비추천) 기능 사용 여부 |
|
||||
| active_reaction_types | body | array | 아니오 | — | 이 게시판에서 켠 반응 유형 code 목록 (문자열 배열, 예: `["like","dislike"]`) |
|
||||
| comment_order | body | string | 예 | `ASC`, `DESC` | 댓글 정렬 순서 (ASC 오름차순 / DESC 내림차순) |
|
||||
| new_display_hours | body | integer | 아니오 | min 0, max 720 | 신규(NEW) 표시를 유지할 기간 (시간 단위, 최대 720시간=30일). `0` 은 NEW 배지를 표시하지 않음 |
|
||||
| min_title_length | body | integer | 아니오 | min 0, max 200 | 게시글 제목 최소 글자 수 |
|
||||
|
||||
@@ -361,7 +361,7 @@ HTTP/1.1 200
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| _tab | body | string | 아니오 | `basic_defaults`, `report_policy`, `spam_security`, `general`, `seo`, `notifications`, `notification_definitions` | 현재 편집 중인 탭을 나타내는 메타 값. 탭 단위 부분 저장의 컨텍스트를 식별하는 용도이며 설정값으로는 저장되지 않습니다. |
|
||||
| notifications | body | array | 아니오 | — | 알림 채널 설정. `channels` 배열의 각 항목에 채널 식별자(id), 활성화 여부(is_active), 정렬 순서(sort_order)를 담아 저장합니다. |
|
||||
| basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. `basic_defaults.allowed_extensions` 는 `basic_defaults.use_file_upload` 가 `true` 일 때만 최소 1개가 필수이며, `false`/`null` 이면 검증에서 제외되어 빈 배열도 허용됩니다. `min_title_length`·`min_comment_length`·`new_display_hours` 의 하한은 `config('sirsoft-board.limits')` 선언을 따르며 `0` 을 허용합니다. |
|
||||
| basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. 반응 기능 기본값으로 `basic_defaults.use_reaction`(boolean) 과 `basic_defaults.active_reaction_types`(반응 유형 code 문자열 배열)를 포함하며, 새 게시판 생성 시 이 값이 개별 게시판의 초기값으로 시드됩니다. `basic_defaults.allowed_extensions` 는 `basic_defaults.use_file_upload` 가 `true` 일 때만 최소 1개가 필수이며, `false`/`null` 이면 검증에서 제외되어 빈 배열도 허용됩니다. `min_title_length`·`min_comment_length`·`new_display_hours` 의 하한은 `config('sirsoft-board.limits')` 선언을 따르며 `0` 을 허용합니다. |
|
||||
| report_policy | body | array | 아니오 | — | 신고 정책 카테고리 값. 자동 숨김 임계치/대상, 일일 신고 한도, 거부 누적 제한, 관리자·작성자 신고 알림 설정을 저장합니다. |
|
||||
| report_policy.auto_hide_threshold | body | integer | 아니오 | min 0, max 100 | 자동 숨김 신고 수 (이 횟수 이상 신고되면 자동 숨김 처리) |
|
||||
| report_policy.auto_hide_target | body | string | 아니오 | `post`, `comment`, `both` | 자동 숨김 대상 (게시글 / 댓글 / 둘 다) |
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// e2e:allow 반응 사용 토글 + 유형 체크박스 저장은 본 레이아웃 회귀테스트(vitest) + MCP 실브라우저 검증(토글/체크/저장)으로 커버. 게시판 환경설정 화면 Playwright 인프라 부재
|
||||
/**
|
||||
* 게시판 반응(추천/비추천) 설정 UI 회귀 가드 (이슈 #525 확정 02·11·13)
|
||||
*
|
||||
* @description
|
||||
* 고정 대상:
|
||||
* 1) 환경설정 기본값 탭(_tab_board_settings_basic)에 use_reaction 토글이 있고
|
||||
* basic_defaults.use_reaction 에 바인딩된다 (확정 13, 새 게시판 기본값).
|
||||
* 2) 유형 체크박스 서브패널은 use_reaction on 일 때만 노출되며,
|
||||
* reactionTypes 데이터소스를 iteration 해 basic_defaults.active_reaction_types(code 배열)를
|
||||
* 토글한다 (확정 02·11, DB 유형 목록 동적 렌더).
|
||||
* 3) 개별 게시판 편집 폼(admin_board_form/_tab_basic)에도 동일 구조가 use_reaction /
|
||||
* active_reaction_types 에 바인딩되어 존재한다 (게시판별 override).
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import settingsTab from '../../../layouts/admin/partials/admin_board_settings/_tab_board_settings_basic.json';
|
||||
import formTab from '../../../layouts/admin/partials/admin_board_form/_tab_basic.json';
|
||||
|
||||
const settingsJson = JSON.stringify(settingsTab);
|
||||
const formJson = JSON.stringify(formTab);
|
||||
|
||||
describe('환경설정 기본값 탭 — 반응 사용 토글 + 유형 체크박스', () => {
|
||||
it('use_reaction 토글이 basic_defaults.use_reaction 에 바인딩된다', () => {
|
||||
expect(/"name":\s*"basic_defaults\.use_reaction"/.test(settingsJson)).toBe(true);
|
||||
});
|
||||
|
||||
it('유형 체크박스 서브패널은 use_reaction on 일 때만 노출된다', () => {
|
||||
expect(/_local\.form\?\.basic_defaults\?\.use_reaction === true/.test(settingsJson)).toBe(true);
|
||||
});
|
||||
|
||||
it('reactionTypes 데이터소스를 iteration 해 유형을 동적 렌더한다', () => {
|
||||
expect(/reactionTypes\?\.data\?\.reaction_types/.test(settingsJson)).toBe(true);
|
||||
expect(/"item_var":\s*"reactionType"/.test(settingsJson)).toBe(true);
|
||||
});
|
||||
|
||||
it('체크박스가 active_reaction_types(code 배열) 를 include/exclude 로 토글한다', () => {
|
||||
// 체크 상태: code 포함 여부
|
||||
expect(
|
||||
/\(_local\.form\?\.basic_defaults\?\.active_reaction_types \?\? \[\]\)\.includes\(reactionType\.code\)/.test(
|
||||
settingsJson,
|
||||
),
|
||||
).toBe(true);
|
||||
// 저장: 포함 시 filter 제거, 미포함 시 spread 추가 (upsert 토글)
|
||||
expect(/"form\.basic_defaults\.active_reaction_types"/.test(settingsJson)).toBe(true);
|
||||
expect(/filter\(c => c !== reactionType\.code\)/.test(settingsJson)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('개별 게시판 편집 폼 — 반응 사용 토글 + 유형 체크박스 (override)', () => {
|
||||
it('use_reaction 토글이 use_reaction 에 바인딩된다', () => {
|
||||
expect(/"name":\s*"use_reaction"/.test(formJson)).toBe(true);
|
||||
});
|
||||
|
||||
it('유형 체크박스는 use_reaction on 일 때만 노출되고 active_reaction_types 를 토글한다', () => {
|
||||
expect(/_local\.form\?\.use_reaction === true/.test(formJson)).toBe(true);
|
||||
expect(/reactionTypes\?\.data\?\.reaction_types/.test(formJson)).toBe(true);
|
||||
expect(
|
||||
/\(_local\.form\?\.active_reaction_types \?\? \[\]\)\.includes\(reactionType\.code\)/.test(formJson),
|
||||
).toBe(true);
|
||||
expect(/"form\.active_reaction_types"/.test(formJson)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
"editor": {
|
||||
"data_source": {
|
||||
"availableChannels": "Available Channels",
|
||||
"reactionTypes": "Reaction Types",
|
||||
"boardIdentityPolicies": "Board Identity Policies",
|
||||
"boardIdentityPurposes": "Board Identity Purposes",
|
||||
"boardNotificationDefinitions": "Board Notification Definitions",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"editor": {
|
||||
"data_source": {
|
||||
"availableChannels": "사용 가능 채널",
|
||||
"reactionTypes": "반응 유형",
|
||||
"boardIdentityPolicies": "게시판 본인인증 정책",
|
||||
"boardIdentityPurposes": "게시판 본인인증 목적",
|
||||
"boardNotificationDefinitions": "게시판 알림 정의",
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"use_comment": "Enable Comments",
|
||||
"use_reply": "Enable Reply Posts",
|
||||
"use_report": "Enable Reports",
|
||||
"use_reaction": "Enable Reactions",
|
||||
"active_reaction_types": "Select Reaction Types",
|
||||
"secret_mode": "Secret Mode",
|
||||
"order_by": {
|
||||
"label": "Sort By",
|
||||
@@ -146,6 +148,7 @@
|
||||
"use_comment": "When enabled, you can configure detailed comment settings (length, depth, order).",
|
||||
"use_reply": "When enabled, you can set the maximum reply depth.",
|
||||
"use_report": "Enable report feature for inappropriate posts/comments",
|
||||
"use_reaction": "Enable reactions such as recommend/not recommend on posts",
|
||||
"type": "Board types can be managed in Settings > Basic Settings."
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -95,6 +95,8 @@
|
||||
"notify_author": "Author Notification",
|
||||
"notify_author_description": "Send email to the original author when a comment or reply is posted",
|
||||
"use_report": "Enable Reports",
|
||||
"use_reaction": "Enable Reactions",
|
||||
"active_reaction_types": "Select Reaction Types",
|
||||
"auto_hide_threshold": "Auto Blind Threshold",
|
||||
"auto_hide_threshold_hint": "Automatically blind content when reports reach this count. (0=disabled, 1\u2013100)",
|
||||
"auto_hide_target": "Blind Target",
|
||||
@@ -155,6 +157,7 @@
|
||||
"secret_mode": "Set the default behavior for the secret post feature",
|
||||
"show_view_count": "Display view count on post list and detail pages",
|
||||
"use_report": "Enable reporting of inappropriate posts and comments",
|
||||
"use_reaction": "Enable reactions such as recommend\/not recommend on posts",
|
||||
"new_display_hours": "Duration to display [New] badge after posting (in hours)",
|
||||
"per_page": "Number of posts to display per page on desktop ({{min}}–{{max}})",
|
||||
"per_page_mobile": "Number of posts to display per page on mobile ({{min}}–{{max}})",
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"use_comment": "댓글 사용",
|
||||
"use_reply": "답변글 사용",
|
||||
"use_report": "신고 사용",
|
||||
"use_reaction": "반응 사용",
|
||||
"active_reaction_types": "사용할 반응 유형 선택",
|
||||
"secret_mode": "비밀글 모드",
|
||||
"order_by": {
|
||||
"label": "정렬 기준",
|
||||
@@ -146,6 +148,7 @@
|
||||
"use_comment": "활성화하면 댓글 관련 세부 설정(길이, 깊이, 정렬)을 지정할 수 있습니다.",
|
||||
"use_reply": "활성화하면 답변글 최대 깊이를 설정할 수 있습니다.",
|
||||
"use_report": "부적절한 게시글/댓글 신고 기능을 활성화합니다",
|
||||
"use_reaction": "게시글에 추천/비추천 등 반응을 남기는 기능을 활성화합니다",
|
||||
"type": "게시판 유형은 환경설정 > 기본설정에서 관리할 수 있습니다."
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -95,6 +95,8 @@
|
||||
"notify_author": "작성자 알림",
|
||||
"notify_author_description": "댓글, 답변글 작성 시 원글 작성자에게 이메일 발송",
|
||||
"use_report": "신고 사용",
|
||||
"use_reaction": "반응 사용",
|
||||
"active_reaction_types": "사용할 반응 유형 선택",
|
||||
"auto_hide_threshold": "자동 숨김 기준 (회)",
|
||||
"auto_hide_threshold_hint": "이 횟수 이상 신고가 접수되면 자동으로 블라인드 처리됩니다. (0=비활성, 1~100)",
|
||||
"auto_hide_target": "적용 대상",
|
||||
@@ -155,6 +157,7 @@
|
||||
"secret_mode": "비밀글 기능의 기본 동작을 설정합니다",
|
||||
"show_view_count": "게시글 목록 및 상세에서 조회수를 표시합니다",
|
||||
"use_report": "부적절한 게시글\/댓글 신고 기능을 활성화합니다",
|
||||
"use_reaction": "게시글에 추천\/비추천 등 반응을 남기는 기능을 활성화합니다",
|
||||
"new_display_hours": "게시글 작성 후 [New] 배지를 표시할 기간 (시간 단위)",
|
||||
"per_page": "PC 환경에서 한 페이지에 표시할 게시글 수입니다 ({{min}}~{{max}})",
|
||||
"per_page_mobile": "모바일 환경에서 한 페이지에 표시할 게시글 수입니다 ({{min}}~{{max}})",
|
||||
|
||||
@@ -82,6 +82,20 @@
|
||||
"channels": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reactionTypes",
|
||||
"label_key": "$t:sirsoft-board.editor.data_source.reactionTypes",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/reaction-types",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": {
|
||||
"reaction_types": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"slots": {
|
||||
|
||||
@@ -158,6 +158,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reactionTypes",
|
||||
"label_key": "$t:sirsoft-board.editor.data_source.reactionTypes",
|
||||
"type": "api",
|
||||
"endpoint": "/api/modules/sirsoft-board/admin/reaction-types",
|
||||
"method": "GET",
|
||||
"if": "{{(query.tab || _global.activeBoardSettingsTab || 'general') === 'basic_defaults'}}",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": {
|
||||
"reaction_types": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "boardIdentityPolicies",
|
||||
"label_key": "$t:sirsoft-board.editor.data_source.boardIdentityPolicies",
|
||||
|
||||
+124
-1
@@ -161,7 +161,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "circle-info",
|
||||
"className": "text-secondary w-4 h-4 mt-0.5 flex-shrink-0"
|
||||
"className": "text-secondary text-base mt-0.5 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -673,6 +673,129 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_use_reaction",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-start justify-between"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-heading"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.use_reaction"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.descriptions.use_reaction"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "use_reaction",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"className": "flex-shrink-0 ml-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "사용할 반응 유형 선택 — 반응 사용 on 일 때만 노출. DB 등록 유형 전체(reactionTypes)를 순회해 체크박스로 active_reaction_types(code 배열) 를 토글.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.form?.use_reaction === true}}",
|
||||
"props": {
|
||||
"className": "mt-3 pl-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.form.fields.active_reaction_types"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "{{reactionTypes?.data?.reaction_types ?? []}}",
|
||||
"item_var": "reactionType",
|
||||
"index_var": "reactionIdx"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex-start gap-2 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox-aligned",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"checked": "{{(_local.form?.active_reaction_types ?? []).includes(reactionType.code)}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"form.active_reaction_types": "{{(_local.form?.active_reaction_types ?? []).includes(reactionType.code) ? (_local.form?.active_reaction_types ?? []).filter(c => c !== reactionType.code) : [...(_local.form?.active_reaction_types ?? []), reactionType.code]}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{reactionType.name ?? reactionType.code}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+149
@@ -470,6 +470,155 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_use_reaction",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": ""
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex-start gap-2 text-heading flex-1 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox-aligned",
|
||||
"checked": "{{(_local.bulkApplyFields ?? []).includes('use_reaction')}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('use_reaction') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'use_reaction') : [...(_local.bulkApplyFields ?? []), 'use_reaction']}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.use_reaction"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-field-hint"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.use_reaction"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"name": "basic_defaults.use_reaction",
|
||||
"className": "flex-shrink-0",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "사용할 반응 유형 선택 — 반응 사용 on 일 때만 노출. DB 등록 유형 전체(reactionTypes)를 순회해 체크박스로 basic_defaults.active_reaction_types(code 배열) 를 토글.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.form?.basic_defaults?.use_reaction === true}}",
|
||||
"props": {
|
||||
"className": "row-content-indent mt-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-field-hint mb-2"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.fields.active_reaction_types"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"iteration": {
|
||||
"source": "{{reactionTypes?.data?.reaction_types ?? []}}",
|
||||
"item_var": "reactionType",
|
||||
"index_var": "reactionIdx"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "flex-start gap-2 cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox-aligned",
|
||||
"disabled": "{{_computed.isReadOnly}}",
|
||||
"checked": "{{(_local.form?.basic_defaults?.active_reaction_types ?? []).includes(reactionType.code)}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"form.basic_defaults.active_reaction_types": "{{(_local.form?.basic_defaults?.active_reaction_types ?? []).includes(reactionType.code) ? (_local.form?.basic_defaults?.active_reaction_types ?? []).filter(c => c !== reactionType.code) : [...(_local.form?.basic_defaults?.active_reaction_types ?? []), reactionType.code]}}",
|
||||
"hasChanges": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "{{reactionType.name ?? reactionType.code}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ class BulkApplySettingsRequest extends FormRequest
|
||||
'comment_order',
|
||||
'show_view_count',
|
||||
'use_report',
|
||||
'use_reaction',
|
||||
'active_reaction_types',
|
||||
'min_title_length',
|
||||
'max_title_length',
|
||||
'min_content_length',
|
||||
|
||||
@@ -65,6 +65,7 @@ class StoreBoardSettingsRequest extends FormRequest
|
||||
'basic_defaults.use_reply',
|
||||
'basic_defaults.show_view_count',
|
||||
'basic_defaults.use_report',
|
||||
'basic_defaults.use_reaction',
|
||||
'basic_defaults.use_file_upload',
|
||||
'basic_defaults.notify_admin_on_post',
|
||||
'basic_defaults.notify_author',
|
||||
@@ -176,6 +177,9 @@ class StoreBoardSettingsRequest extends FormRequest
|
||||
'basic_defaults.comment_order' => ['nullable', 'string', 'in:ASC,DESC'],
|
||||
'basic_defaults.show_view_count' => ['nullable', 'boolean'],
|
||||
'basic_defaults.use_report' => ['nullable', 'boolean'],
|
||||
'basic_defaults.use_reaction' => ['nullable', 'boolean'],
|
||||
'basic_defaults.active_reaction_types' => ['nullable', 'array'],
|
||||
'basic_defaults.active_reaction_types.*' => ['string', 'max:50'],
|
||||
'basic_defaults.min_title_length' => ['nullable', 'integer', "min:{$minTitleLengthMin}", "max:{$minTitleLengthMax}"],
|
||||
'basic_defaults.max_title_length' => ['nullable', 'integer', "min:{$maxTitleLengthMin}", "max:{$maxTitleLengthMax}"],
|
||||
'basic_defaults.min_content_length' => ['nullable', 'integer', "min:{$minContentLengthMin}", "max:{$minContentLengthMax}"],
|
||||
|
||||
@@ -53,6 +53,8 @@ class StoreBoardRequest extends FormRequest
|
||||
'secret_mode' => $settings['secret_mode'] ?? 'disabled',
|
||||
'use_comment' => $settings['use_comment'] ?? true,
|
||||
'use_reply' => $settings['use_reply'] ?? true,
|
||||
'use_reaction' => $settings['use_reaction'] ?? true,
|
||||
'active_reaction_types' => $settings['active_reaction_types'] ?? [],
|
||||
'max_reply_depth' => $settings['max_reply_depth'] ?? 5,
|
||||
'max_comment_depth' => $settings['max_comment_depth'] ?? 10,
|
||||
'use_file_upload' => $settings['use_file_upload'] ?? false,
|
||||
@@ -86,7 +88,7 @@ class StoreBoardRequest extends FormRequest
|
||||
// boolean 필드 캐스팅 (Toggle 컴포넌트가 "on"/"off" 문자열을 전송할 수 있음)
|
||||
$booleanFields = [
|
||||
'is_active', 'use_comment', 'use_reply', 'use_file_upload',
|
||||
'use_report', 'show_view_count', 'is_notice',
|
||||
'use_report', 'use_reaction', 'show_view_count', 'is_notice',
|
||||
'notify_admin_on_post', 'notify_author',
|
||||
];
|
||||
|
||||
@@ -169,6 +171,9 @@ class StoreBoardRequest extends FormRequest
|
||||
'use_comment' => ['required', 'boolean'],
|
||||
'use_reply' => ['required', 'boolean'],
|
||||
'use_report' => ['required', 'boolean'],
|
||||
'use_reaction' => ['required', 'boolean'],
|
||||
'active_reaction_types' => ['sometimes', 'array'],
|
||||
'active_reaction_types.*' => ['string'],
|
||||
'comment_order' => ['required', 'in:ASC,DESC'],
|
||||
'new_display_hours' => ['nullable', 'integer', "min:{$newDisplayHoursMin}", "max:{$newDisplayHoursMax}"],
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class UpdateBoardRequest extends FormRequest
|
||||
// boolean 필드 캐스팅 (Toggle 컴포넌트가 "on"/"off" 문자열을 전송할 수 있음)
|
||||
$booleanFields = [
|
||||
'is_active', 'use_comment', 'use_reply', 'use_file_upload',
|
||||
'use_report', 'show_view_count', 'is_notice',
|
||||
'use_report', 'use_reaction', 'show_view_count', 'is_notice',
|
||||
'notify_admin_on_post', 'notify_author',
|
||||
];
|
||||
|
||||
@@ -164,6 +164,9 @@ class UpdateBoardRequest extends FormRequest
|
||||
'use_comment' => ['sometimes', 'required', 'boolean'],
|
||||
'use_reply' => ['sometimes', 'required', 'boolean'],
|
||||
'use_report' => ['sometimes', 'required', 'boolean'],
|
||||
'use_reaction' => ['sometimes', 'required', 'boolean'],
|
||||
'active_reaction_types' => ['sometimes', 'array'],
|
||||
'active_reaction_types.*' => ['string'],
|
||||
'comment_order' => ['sometimes', 'required', 'in:ASC,DESC'],
|
||||
'new_display_hours' => ['sometimes', 'nullable', 'integer', "min:{$newDisplayHoursMin}", "max:{$newDisplayHoursMax}"],
|
||||
|
||||
@@ -261,6 +264,7 @@ class UpdateBoardRequest extends FormRequest
|
||||
$settingFieldKeys = [
|
||||
'per_page', 'per_page_mobile', 'order_by', 'order_direction',
|
||||
'secret_mode', 'use_comment', 'use_reply', 'use_report',
|
||||
'use_reaction', 'active_reaction_types',
|
||||
'comment_order', 'show_view_count',
|
||||
'max_reply_depth', 'max_comment_depth',
|
||||
'min_title_length', 'max_title_length',
|
||||
|
||||
@@ -295,9 +295,34 @@ class BoardResource extends BaseApiResource
|
||||
'code' => $type->code,
|
||||
'name' => $type->getLocalizedName(),
|
||||
'icon' => $type->icon,
|
||||
// Icon 컴포넌트 name prop 용 — 스타일 접두사(fas/far 등)를 떼고 fa-* 토큰만 추출
|
||||
'icon_name' => self::normalizeIconName($type->icon),
|
||||
])->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 Font Awesome 클래스(`fas fa-thumbs-up`)에서 Icon 컴포넌트 name prop 용
|
||||
* 아이콘 토큰(`fa-thumbs-up`)만 추출합니다. Icon 컴포넌트가 스타일 접두사를 자체 부착하므로
|
||||
* 원본 클래스를 그대로 name 에 넘기면 접두사가 중복됩니다.
|
||||
*
|
||||
* @param string|null $icon 저장된 아이콘 클래스
|
||||
* @return string|null Icon name prop 값 (없으면 null)
|
||||
*/
|
||||
private static function normalizeIconName(?string $icon): ?string
|
||||
{
|
||||
if ($icon === null || $icon === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (preg_split('/\s+/', trim($icon)) as $token) {
|
||||
if (str_starts_with($token, 'fa-')) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
return $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* 허용 확장자를 배열로 반환합니다.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user