본문으로 건너뛰기

내장 게시판 운영자 가이드

내장 게시판(공지 / FAQ / 1:1 문의)의 운영 절차를 다룹니다. 개념과 접속 경로는 시작하기를 먼저 확인하세요.

권한 전제

게시판 컨테이너 생성/삭제는 L1 SaaS · L2 Tenant 운영자만 가능합니다. L3 조직 운영자는 본인 서브트리 게시판의 글 관리와 문의 응대만 할 수 있습니다. 서브관리자는 boards / inquiries feature 권한이 부여되어야 메뉴가 보입니다.

1. 게시판 컨테이너 관리

SaaS · Tenant · Org 패널의 사이트 관리 → 게시판(BoardResource, 슬러그 site-boards)에서 게시판 컨테이너를 관리합니다.

새 게시판 만들기 (L1/L2)

게시판 폼은 두 섹션으로 구성됩니다.

기본 정보 섹션

필드설명비고
kind종류 (공지/FAQ/문의/범용)is_system 게시판은 변경 불가
name게시판 표시 이름최대 150자, 필수
key내부 식별 키최대 64자, is_system 게시판은 변경 불가
audience_scope노출 범위 (SaaS/Tenant/Org)기본값 tenant
audience_org_id대상 조직audience_scope = org 일 때만 표시
site_key독립 사이트 키비우면 공통(null), 특정 독립 사이트에만 노출 시 지정

노출/배치 섹션

필드설명기본값
placement.app_nav회원 App 패널 메뉴 노출 토글true
placement.sortApp 네비 내 정렬 순서0
is_active게시판 활성화true
sort관리 목록 정렬 순서0
  • 한 테넌트 안에서 (site_key, key) 조합은 유일합니다(부분 유니크 인덱스, site_key NULL 은 '' 로 정규화).
  • audience_scopeorg 이면 지정한 조직과 그 하위 서브트리 전체(depth 무관)에 노출됩니다.

목록과 액션

게시판 목록은 kind(배지), name, audience_scope, 글 수(posts_count), is_active, is_system 컬럼을 보여 줍니다. 정렬 기본값은 sort 오름차순입니다.

  • 수정: 행 단위 EditAction 으로 모든 운영자(열람 권한 기준) 수행.
  • 일괄 삭제: DeleteBulkActionlevel <= 2(L1/L2)에게만 표시됩니다.

is_system 보호

CoreSeeder 가 시드한 기본 게시판(공지/FAQ/문의)은 is_system=true 로 보호됩니다.

  • 폼에서 kindkey 필드가 비활성(disabled)됩니다 — 종류와 식별 키를 바꿀 수 없습니다.
  • name, audience_scope, 노출 설정 등은 수정할 수 있습니다.
  • 삭제는 L1/L2 의 일괄 삭제로만 가능하며, 시스템 게시판은 운영 연속성을 위해 보존하는 것을 권장합니다.

2. 공지 / FAQ / 범용 글 작성

글 관리는 게시판 상세의 Posts RelationManager(PostsRelationManager)에서 BoardPost 단위로 수행합니다. BoardPost 의 주요 필드는 다음과 같습니다.

필드설명
title / body제목 / 본문
excerpt요약
category분류 (FAQ 카테고리 그룹에 사용)
statusdraft / published
is_pinned상단 고정 (공지 목록 상단 노출)
published_at발행 시각
starts_at / ends_at노출 시작/종료 기간

발행과 노출 기간

회원에게 보이는 글은 BoardPost::scopePublished() 가 결정합니다. 다음 조건을 모두 만족해야 노출됩니다.

  • status = published
  • published_at 이 비었거나 현재 시각 이하
  • starts_at 이 비었거나 현재 시각 이하
  • ends_at 이 비었거나 현재 시각 이상

starts_at / ends_at 로 기간 한정 공지를 예약할 수 있고, is_pinned 로 공지 목록 상단에 고정할 수 있습니다.

3. 1:1 문의 운영

문의 응대는 SaaS · Tenant · Org 패널의 고객 문의 → 1:1 문의(InquiryResource)에서 수행합니다. 문의는 회원이 작성하므로 운영자는 새로 만들 수 없습니다(canCreate() = false).

계층 열람 (상위 조직이 하위 문의를 본다)

어떤 문의를 볼 수 있는지는 BoardInquiry::scopeVisibleTo() 가 뷰어의 레벨로 결정합니다.

뷰어 레벨열람 범위
L0 / L1 / L2Tenant · SaaS 전체 문의 (글로벌 스코프)
L3~L5 조직 운영자본인 작성 문의 + 자기 조직 서브트리(ViewerScopeResolver::orgSubtreeIds())에 origin(organization_id) 또는 assigned(assigned_org_id)된 문의
L6 회원본인 문의만 (같은 조직 동료 문의도 비노출)

핵심은 상위 조직 운영자가 하위 조직의 문의를 열람한다는 점입니다. 문의에는 두 개의 조직 필드가 있습니다.

  • organization_id — 작성 시점 회원의 소속 조직 (열람 스코프 기준)
  • assigned_org_id — 현재 처리 주체 조직 (에스컬레이션에 따라 이동)

문의 목록은 제목, 작성자(author.name), 소속 조직, 담당 조직(assignedOrg.name), 상태(배지), 최근 답글 시각을 보여 주며, 상태 필터(open/answered/closed)를 제공합니다.

답글 작성 (운영자)

문의 응대 화면(EditInquiry)의 Replies RelationManager(RepliesRelationManager)에서 답글을 답니다.

  • 답글 작성 시 문의 상태가 자동으로 answered 로 바뀌고 last_reply_at 이 갱신됩니다.
  • is_internal 토글을 켜면 내부 메모가 됩니다. 내부 메모는 회원에게 노출되지 않습니다(BoardInquiryReply::scopePublic()is_internal=false 만 반환).
  • 운영자는 상단 폼에서 문의 상태(open / answered / closed)를 직접 변경할 수 있습니다. 제목·분류는 읽기 전용입니다.

에스컬레이션

응대 화면(EditInquiry) 헤더의 에스컬레이션 액션으로 미해결 문의를 상위 조직으로 한 단계 올립니다. 이 액션은 config('board.inquiry.escalation_enabled', true)(CORE_BOARD_INQUIRY_ESCALATION) 가 true 일 때만 보입니다.

동작은 BoardInquiry::escalate() 가 처리합니다.

  1. 현재 처리 주체(assigned_org_id)의 부모 조직(organizations.parent_id)으로 assigned_org_id 를 이동합니다.
  2. 부모가 없으면(parent_id = null) assigned_org_idnull 로 설정 — 이는 Tenant/SaaS 큐(최상위)에 도달했음을 의미합니다.
  3. 이동 후 상태를 다시 open 으로 되돌립니다.
  4. 이미 최상위(assigned_org_id = null)이면 이동하지 않고 "최상위 도달" 알림을 보냅니다(organization_id 폴백 없음 — 재에스컬레이션 방지).

에스컬레이션 액션은 공통 로깅 모듈(->audited('board.inquiry.escalate'))로 계층 컨텍스트가 감사 로그에 자동 기록됩니다.

4. 회원 입장에서의 사용

회원은 App 패널(/app)에서 다음 페이지를 사용합니다(게시판이 활성화된 경우에만 노출).

공지사항 (NoticesPage)

자기 계층(SaaS 전체 + 소속 Tenant + 조직 조상 체인)에 노출된 notice 게시판의 발행 글을 봅니다. 고정 글(is_pinned)이 상단에 오고, 글을 클릭하면 모달로 본문을 봅니다. 본문은 XSS 방지를 위해 escape 후 줄바꿈만 허용합니다(nl2br(e($body))).

FAQ (FaqPage)

노출된 faq 게시판의 글을 카테고리(category)별로 그룹지어 보여 줍니다. 글을 클릭하면 모달로 본문을 봅니다.

내 문의 (MyInquiriesPage)

본인이 작성한 문의만 조회합니다(user_id = auth()->id()).

  • 새 문의 작성: 노출된 inquiry 게시판을 선택하고 제목·내용을 입력해 작성합니다. 작성 시 organization_idassigned_org_id 가 모두 회원의 소속 조직으로 설정되어, 해당 조직(및 상위)이 응대 진입점이 됩니다. 작성 트랜잭션은 문의 1건 + 첫 답글(is_internal=false) 1건을 함께 생성합니다.
  • 답글 달기: 문의를 열어 답글을 추가하면 상태가 open 으로 돌아가고 last_reply_at 이 갱신됩니다. 회원에게는 내부 메모를 제외한 답글(is_internal=false)만 보입니다.

5. 다국어 (i18n)

내장 게시판의 모든 라벨·메시지는 lang/{locale}/board.php 로 4개 언어를 제공합니다.

locale언어
ko한국어 (기본)
en영어
ja일본어
zh-CN중국어(간체)

Filament Resource·회원 페이지·알림은 모두 __('board.*') 번역 키를 사용하므로, 사용자의 로케일에 따라 메뉴명(예: board.navigation_group_site → "사이트 관리", board.navigation_group_inquiry → "고객 문의")과 안내 문구가 자동 전환됩니다.

참고: 핵심 클래스 요약

구성요소클래스 / 위치
Filament 플러그인App\Filament\Board\BuiltinBoardFilamentPlugin (id builtin-board)
게시판 컨테이너 ResourceApp\Filament\Board\Resources\BoardResource (slug site-boards)
글 RelationManager...\BoardResource\RelationManagers\PostsRelationManager
문의 ResourceApp\Filament\Board\Resources\InquiryResource
문의 응대 페이지...\InquiryResource\Pages\EditInquiry (에스컬레이션 액션)
답글 RelationManager...\InquiryResource\RelationManagers\RepliesRelationManager
회원 페이지App\Filament\App\Pages\{NoticesPage, FaqPage, MyInquiriesPage}
모델App\Models\{Board, BoardPost, BoardInquiry, BoardInquiryReply}