본문으로 건너뛰기

LessonThread

레슨별 Q&A 포럼 — thread/reply 검증 + 1단계 reply depth + 권한 정�

상태

항목
Layerdomain
TierL1
Statuswip
Version0.9.0
가격Free (free)
**카�
�고리**Community

개요

개요

LessonThread 는 multi-saas-kit 의 Layer 3 Domain Plugin (첫 Domain) � 니다. 레슨 단위 Q&A 포럼을 표준화한 thread / reply 의 페이로드 검증 + 액� � 권한 정� 을 제공합니다.

Annotation 의 AuthorRole enum 을 재사용하여 권한 분기를 단순화.

핵심 컴포넌트

ThreadPayloadValidator (Pure)

academy.how LessonThreadController::store / storeReply 흐름 추출.

Thread 검증 (validateThread):

  • title 1~max(255), trim 후 빈 문자열 reject
  • body 1~max(20_000)
  • multibyte 친화 (mb_strlen)

Reply 검증 (validateReply):

  • thread is_locked reject
  • body 1~max(20_000)
  • parent_id 가 다른 thread 의 reply 면 parent_invalid
  • parent 가 이미 child reply 면 reply_depth_exceeded (1단계 만 허용, config 로 조정 가능)
  • staff 답글이면 is_teacher_reply=true 자동 마킹
  • empty / blank parent_id 는 top-level 로 처리

ThreadActionPolicy (Pure)

academy.how LessonThreadPolicy / LessonThreadReplyPolicy 의 분기 추출. Role + Ownership 만으로 결정 (tenant/saas 격리는 호출자 � 임).

| 액� � | 정� | |------|------| | canCreateThread | role 만 있으면 OK | | canUpdateThread / canDeleteThread | 본인 OR staff | | canTogglePin / canToggleLock | 기본 staff only (config 토글) | | canCreateReply | open thread = 누구나, locked thread = staff | | canUpdateReply / canDeleteReply | 본인 OR staff | | canToggleAccept | staff 는 항상. accept_staff_only=false 일 때 thread 작성자도 |

Exception

InvalidThreadPayloadExceptionreason 필드로 i18n 가능 (예: thread_locked, reply_depth_exceeded).

설정 (config/lesson-thread.php)

return [
'enabled' => env('PLG_LESSON_THREAD_ENABLED', true),
'limits' => [
'title_max' => 255,
'thread_body_max' => 20_000,
'reply_body_max' => 20_000,
],
'replies' => [
'max_depth' => 1, // parent 가 child 면 차단
'accept_staff_only' => true,
],
'admin_actions' => [
'pin_staff_only' => true,
'lock_staff_only' => true,
],
];

사용 예시

use App\Plugins\Annotation\Enums\AuthorRole;
use App\Plugins\LessonThread\Services\{ThreadPayloadValidator, ThreadActionPolicy};

$validator = ThreadPayloadValidator::fromConfig(config('lesson-thread'));
$policy = ThreadActionPolicy::fromConfig(config('lesson-thread'));

$role = AuthorRole::from($user->annotationRole());

// Thread 생성
if (! $policy->canCreateThread($role)) abort(403);
$normalized = $validator->validateThread($request->all(), $role);
LessonThread::create([...$normalized, 'lesson_id' => $lessonId, ...]);

// Reply 생성
if (! $policy->canCreateReply($role, $thread->is_locked)) abort(403);
$replyData = $validator->validateReply(
payload: $request->all(),
threadMeta: ['thread_id' => $thread->id, 'is_locked' => $thread->is_locked],
parentMeta: $parent ? ['thread_id' => $parent->thread_id, 'parent_id' => $parent->parent_id] : null,
role: $role,
);
LessonThreadReply::create([...$replyData, 'thread_id' => $thread->id, ...]);

출처

academy.how 의 LessonThread + LessonThreadReply Model + LessonThreadController + LessonThreadPolicy + LessonThreadReplyPolicy 에서 추출. Eloquent Model 의존을 제거하고 Pure validator + Pure policy 로 분리.

의존성

  • AnnotationAuthorRole enum 재사용

다음 단계 (Phase 3+)

  • 표준 Thread + ThreadReply Model + Migration (선택 활성)
  • Filament Resource (관리자 thread 관리, 답변 채택)
  • DocumentProcessor 의 RichEditorSanitizer 통합 (HTML body)
  • Webbook plugin 과 cross-link (lesson 페이지에 thread 위젯 임베드)

라이선스

MIT

의존성

데모


🛒 Plugin Store에서 보기: store.codebase.how/plugins/lesson-thread