LessonThread
레슨별 Q&A 포럼 — thread/reply 검증 + 1단계 reply depth + 권한 정�
상태
| 항목 | 값 |
|---|---|
| Layer | domain |
| Tier | L1 |
| Status | wip |
| Version | 0.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_lockedreject - 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
InvalidThreadPayloadException — reason 필드로 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 로 분리.
의존성
- Annotation —
AuthorRoleenum 재사용
다음 단계 (Phase 3+)
- 표준
Thread+ThreadReplyModel + Migration (선택 활성) - Filament Resource (관리자 thread 관리, 답변 채택)
- DocumentProcessor 의
RichEditorSanitizer통합 (HTML body) - Webbook plugin 과 cross-link (lesson 페이지에 thread 위젯 임베드)