안동민 개발노트

본문 시작

저장 구조와 AI 콘텐츠 검수

관계형·JSON·외부 보관의 책임을 확정하고 AI가 제안한 게시글을 자동 게시하지 않는 후보·검사·승인·릴리스 증거 모델을 완성합니다.

회원 게시판의 마지막 설계 결정은 모든 데이터를 한 저장 형식에 넣는 일이 아닙니다.

회원, 게시글, 작성자 관계, 상태처럼 트랜잭션과 FK가 묶는 핵심 사실은 관계형 테이블에 둡니다.

게시글마다 선택적으로 달라지는 작은 메타데이터는 검증된 JSON에 둘 수 있습니다.

대형 원본 파일과 모델 응답 전문은 접근 통제와 보존 정책을 갖춘 객체 저장소가 맡습니다.

또 하나의 경계는 AI가 만든 제목과 본문을 게시판에 반영하는 과정입니다.

모델 출력은 문법이 자연스러워도 사실 오류, 개인정보, 저작권 침해, 유해 링크, 중복 문장을 포함할 수 있습니다.

후보를 곧바로 posts에 INSERT하거나 기존 게시글에 UPDATE하면 검토 전 내용이 사용자에게 노출됩니다.

이 문서는 AI 출력을 신뢰 경계 밖 입력으로 다룹니다.

ai_content_candidate는 제안 원문과 출처, 모델 식별 정보, 해시를 고정합니다.

ai_review_rule과 ai_review_result는 필수 검사의 결과와 근거를 보존합니다.

모든 필수 검사가 통과하고 제출자와 다른 검토자가 승인한 후보만 content_release에 포함할 수 있습니다.

릴리스에 포함되었다는 사실도 자동 게시 명령은 아닙니다.


검수 없는 자동 게시의 위험

단순한 자동화는 모델 응답에 제목과 본문이 있으면 곧바로 PUBLISHED 게시글을 만들 수 있습니다.

이 경로에는 사실 근거, 개인정보, 링크 안전성, 게시판 범위, 중복 여부를 확인하는 지점이 없습니다.

AI 초안을 즉시 게시하는 잘못된 경로
DROP TABLE IF EXISTS ai_content_draft_bad;
CREATE TABLE ai_content_draft_bad(
 draft_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
 author_id BIGINT UNSIGNED NOT NULL,
 title VARCHAR(200) NOT NULL,
 content LONGTEXT NOT NULL,
 model_name VARCHAR(80) NOT NULL
) ENGINE=InnoDB;

INSERT INTO members(email,password_hash,name,status,created_at)
VALUES(
 '[email protected]','not-a-real-hash',
 '자동 게시 위험 예제 작성자','ACTIVE',CURRENT_TIMESTAMP(6)
)
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id);
SET @member_id=LAST_INSERT_ID();

DELETE FROM posts
WHERE author_id=@member_id
 AND request_key LIKE 'ai-draft-%';

INSERT INTO ai_content_draft_bad(author_id,title,content,model_name)
VALUES(@member_id,'검증되지 않은 성능 수치',
 '홍길동의 이메일은 [email protected]이며 이 설정은 항상 10배 빠르다.',
 'example-model');

INSERT INTO posts
 (author_id,title,content,status,view_count,created_at,updated_at,published_at,version,request_key)
SELECT author_id,title,content,'PUBLISHED',0,CURRENT_TIMESTAMP(6),CURRENT_TIMESTAMP(6),
 CURRENT_TIMESTAMP(6),0,CONCAT('ai-draft-',draft_id)
FROM ai_content_draft_bad;

코드는 성공하지만 개인정보와 근거 없는 수치가 이미 공개 상태로 저장됩니다.

원문 조건에 따른 예상 결과
candidate review records=0
personal-data check=missing
fact-source check=missing
automatic published posts=1
human approval=0

문제는 모델의 품질 점수보다 쓰기 경계에 있습니다.

AI가 언제나 정확하다는 가정도, 사람이 나중에 발견할 것이라는 가정도 게시 전 무결성을 만들지 못합니다.

후보 저장과 게시글 반영을 분리하면 검토 실패가 공개 데이터로 번지지 않습니다.

후보는 거절되어도 감사와 개선 근거를 위해 일정 기간 보존할 수 있습니다.

게시글 반영은 승인된 해시와 릴리스 항목을 다시 대조한 별도 편집 절차가 수행합니다.


후보·검사·승인의 경계

후보를 검토하는 동안 원본 요청 ID·모델·제안 제목·본문을 고정한다는 쓰기 정책이 필요합니다. 아래 트리거는 승인 이후의 해당 필드 변경만 막습니다.

제목과 본문 해시는 둘 사이에 NUL 구분자를 넣어 계산하므로 입력에서 NUL 문자를 거절한다는 전제도 둡니다. DB 예제에는 그 입력 검사까지 구현하지 않습니다.

필수 규칙은 사실 근거, 개인정보, 저작권, 링크 안전성, 유해 표현, 게시판 범위, 중복, 문체입니다.

검사 결과는 PASS, FAIL, MISSING 가운데 하나이며 PASS에는 사람이 확인할 수 있는 근거가 필요합니다.

자동 분류기 결과는 근거 하나일 뿐 최종 승인을 대신하지 않습니다.

제출자와 검토자를 분리하면 자기 승인을 막을 수 있습니다.

검토자는 후보 내용을 수정하지 않고 거절 이유를 남기거나 새 후보 제출을 요구합니다.

수정된 내용은 해시가 다른 새 후보로 등록해 모든 필수 검사를 다시 수행합니다.

승인과 게시를 분리하는 이유

승인은 검토가 끝났다는 상태 전환입니다.

게시글 생성·수정은 충돌 확인, 대상 게시글 버전, 예약 시각, 롤백 계획을 확인하는 별도 편집 작업입니다.

이 예제에는 후보를 읽어 posts에 자동 반영하는 프로시저를 의도적으로 만들지 않습니다.

운영 안전 규칙: AI 후보 테이블에 접근할 수 있는 계정에 posts 게시 권한을 자동으로 함께 주지 않습니다. 승인된 해시와 사람이 검토한 릴리스 항목만 제한된 편집 절차로 전달합니다.

검수 기록과 내용 해시가 확인하는 범위

후보 내용, 검사 근거, 승인 상태와 릴리스 해시는 서로 다른 사실을 저장합니다.

검수 기록과 내용 해시가 확인하는 범위
저장 대상현재 기록자동 보장하지 않는 것
후보 내용제목·본문으로 생성한 SHA-256검사 전에 읽은 내용과 같다는 사실
검사 결과후보 ID·규칙 코드·결과·검토자·근거검사한 내용 해시·규칙 버전과의 결합
승인 상태PENDING에서 분리 검토자로 APPROVEDPASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목승인 상태에서 복사한 내용 해시실제 posts 반영과 버전·충돌 검사
후보 내용
현재 기록: 제목·본문으로 생성한 SHA-256
자동 보장하지 않는 것: 검사 전에 읽은 내용과 같다는 사실
검사 결과
현재 기록: 후보 ID·규칙 코드·결과·검토자·근거
자동 보장하지 않는 것: 검사한 내용 해시·규칙 버전과의 결합
승인 상태
현재 기록: PENDING에서 분리 검토자로 APPROVED
자동 보장하지 않는 것: PASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목
현재 기록: 승인 상태에서 복사한 내용 해시
자동 보장하지 않는 것: 실제 posts 반영과 버전·충돌 검사

후보 승인 트리거가 검사 결과·규칙·릴리스의 직접 변경까지 일괄 동결하지는 않습니다.


AI 콘텐츠 검수 스키마

스키마는 후보, 규칙, 결과, 릴리스, 릴리스 항목을 분리합니다.

승인 프로시저는 검토자 분리와 모든 필수 규칙의 PASS를 확인합니다.

승인된 후보의 핵심 내용을 동결하는 트리거를 함께 둡니다.

AI 콘텐츠 후보와 해시 고정 릴리스 매니페스트
DROP PROCEDURE IF EXISTS approve_ai_content_candidate;
DROP TRIGGER IF EXISTS freeze_approved_ai_candidate;
DROP TABLE IF EXISTS content_release_item;
DROP TABLE IF EXISTS content_release;
DROP TABLE IF EXISTS ai_review_result;
DROP TABLE IF EXISTS ai_review_rule;
DROP TABLE IF EXISTS ai_content_candidate;

CREATE TABLE ai_content_candidate(
 candidate_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
 target_post_id BIGINT UNSIGNED NULL,
 source_request_id VARCHAR(120) NOT NULL,
 model_reference VARCHAR(120) NOT NULL,
 prompt_summary VARCHAR(500) NOT NULL,
 proposed_title VARCHAR(200) NOT NULL,
 proposed_content LONGTEXT NOT NULL,
 content_sha256 BINARY(32) GENERATED ALWAYS AS(
  UNHEX(SHA2(CONCAT(proposed_title,CHAR(0),proposed_content),256))
 ) STORED,
 risk_level VARCHAR(16) NOT NULL,
 review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
 submitter_id BIGINT UNSIGNED NOT NULL,
 reviewer_id BIGINT UNSIGNED NULL,
 review_note VARCHAR(500) NULL,
 created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
 reviewed_at DATETIME(6) NULL,
 UNIQUE KEY uq_ai_candidate_source(source_request_id),
 FOREIGN KEY(target_post_id) REFERENCES posts(id) ON DELETE SET NULL,
 FOREIGN KEY(submitter_id) REFERENCES members(id) ON DELETE RESTRICT,
 FOREIGN KEY(reviewer_id) REFERENCES members(id) ON DELETE RESTRICT,
 CHECK(risk_level IN('LOW','MEDIUM','HIGH')),
 CHECK(review_status IN('PENDING','APPROVED','REJECTED'))
) ENGINE=InnoDB;

CREATE TABLE ai_review_rule(
 rule_code VARCHAR(40) PRIMARY KEY,
 description VARCHAR(240) NOT NULL,
 is_required BOOLEAN NOT NULL DEFAULT TRUE,
 CHECK(is_required IN(0,1))
) ENGINE=InnoDB;

INSERT INTO ai_review_rule VALUES
 ('FACT_SOURCE','수치와 사실의 근거를 확인',TRUE),
 ('PERSONAL_DATA','개인정보와 비밀값 노출을 확인',TRUE),
 ('COPYRIGHT','과도한 인용과 권리 침해를 확인',TRUE),
 ('LINK_SAFETY','외부 링크의 목적과 안전성을 확인',TRUE),
 ('HARMFUL_CONTENT','유해하거나 차별적인 표현을 확인',TRUE),
 ('BOARD_SCOPE','게시판 목적과 카테고리 범위를 확인',TRUE),
 ('DUPLICATION','기존 게시글과 실질 중복을 확인',TRUE),
 ('EDITORIAL_STYLE','교재형 문체와 표기 규칙을 확인',TRUE);

CREATE TABLE ai_review_result(
 candidate_id BIGINT UNSIGNED NOT NULL,
 rule_code VARCHAR(40) NOT NULL,
 review_result VARCHAR(16) NOT NULL,
 evidence VARCHAR(1000) NOT NULL,
 reviewed_by BIGINT UNSIGNED NOT NULL,
 reviewed_at DATETIME(6) NOT NULL,
 PRIMARY KEY(candidate_id,rule_code),
 FOREIGN KEY(candidate_id) REFERENCES ai_content_candidate(candidate_id) ON DELETE CASCADE,
 FOREIGN KEY(rule_code) REFERENCES ai_review_rule(rule_code) ON DELETE RESTRICT,
 FOREIGN KEY(reviewed_by) REFERENCES members(id) ON DELETE RESTRICT,
 CHECK(review_result IN('PASS','FAIL','MISSING'))
) ENGINE=InnoDB;

CREATE TABLE content_release(
 release_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
 release_name VARCHAR(120) NOT NULL,
 release_status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
 approved_by BIGINT UNSIGNED NOT NULL,
 approved_at DATETIME(6) NOT NULL,
 UNIQUE KEY uq_content_release_name(release_name),
 FOREIGN KEY(approved_by) REFERENCES members(id) ON DELETE RESTRICT,
 CHECK(release_status IN('DRAFT','READY','APPLIED','WITHDRAWN'))
) ENGINE=InnoDB;

CREATE TABLE content_release_item(
 release_id BIGINT UNSIGNED NOT NULL,
 sequence_no INT UNSIGNED NOT NULL,
 candidate_id BIGINT UNSIGNED NOT NULL,
 approved_content_sha256 BINARY(32) NOT NULL,
 PRIMARY KEY(release_id,sequence_no),
 UNIQUE KEY uq_release_candidate(release_id,candidate_id),
 FOREIGN KEY(release_id) REFERENCES content_release(release_id) ON DELETE RESTRICT,
 FOREIGN KEY(candidate_id) REFERENCES ai_content_candidate(candidate_id) ON DELETE RESTRICT
) ENGINE=InnoDB;

DELIMITER //
CREATE TRIGGER freeze_approved_ai_candidate
BEFORE UPDATE ON ai_content_candidate FOR EACH ROW
BEGIN
 IF OLD.review_status='APPROVED' AND(
   NEW.review_status<>'APPROVED'
   OR NOT(CAST(NEW.proposed_title AS BINARY)<=>CAST(OLD.proposed_title AS BINARY))
   OR NOT(CAST(NEW.proposed_content AS BINARY)<=>CAST(OLD.proposed_content AS BINARY))
   OR NOT(CAST(NEW.source_request_id AS BINARY)<=>CAST(OLD.source_request_id AS BINARY))
  OR NOT(CAST(NEW.model_reference AS BINARY)<=>CAST(OLD.model_reference AS BINARY))
 ) THEN
  SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='approved AI content is immutable';
 END IF;
END//

CREATE PROCEDURE approve_ai_content_candidate(
 IN p_candidate BIGINT UNSIGNED,
 IN p_reviewer BIGINT UNSIGNED,
 IN p_note VARCHAR(500))
BEGIN
 DECLARE v_submitter BIGINT UNSIGNED;
 DECLARE v_status VARCHAR(16);
 DECLARE v_missing INT;
 DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END;
 START TRANSACTION;
 SELECT submitter_id,review_status INTO v_submitter,v_status
 FROM ai_content_candidate WHERE candidate_id=p_candidate FOR UPDATE;
 IF p_reviewer IS NULL OR v_submitter IS NULL OR v_status<>'PENDING' OR v_submitter=p_reviewer THEN
  SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='reviewer separation or state invalid';
 END IF;
 SELECT COUNT(*) INTO v_missing
 FROM ai_review_rule r
 LEFT JOIN ai_review_result x
  ON x.candidate_id=p_candidate
  AND x.rule_code=r.rule_code
  AND x.review_result='PASS'
  AND x.reviewed_by<>v_submitter
 WHERE r.is_required=TRUE AND x.rule_code IS NULL;
 IF v_missing<>0 THEN
  SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='required content checks not passed';
 END IF;
 UPDATE ai_content_candidate
 SET review_status='APPROVED',reviewer_id=p_reviewer,review_note=p_note,
     reviewed_at=CURRENT_TIMESTAMP(6)
 WHERE candidate_id=p_candidate;
 COMMIT;
END//
DELIMITER ;

이 프로시저는 필수 규칙의 PASS 행과 검토자 ID 분리를 확인합니다. PASS 행에는 검사한 후보 해시나 규칙 버전이 없으므로, 검사 시작부터 내용과 규칙을 고정하고 직접 DML 권한을 제한한다는 전제가 필요합니다. 후보 잠금 하나가 검사 결과·규칙의 모든 동시 변경을 막지는 않습니다.

예제의 조건을 위반한 승인 호출은 롤백됩니다. 승인 SQL에는 posts 쓰기가 없으므로 그 호출로 게시글이 바뀌지는 않습니다.

수정한 SQL의 예상 결과
required rules=8
passed rules=8
reviewer separation=yes
PASS reviewers separated=yes
approved hash pinned=yes
automatic post publishing=not implemented

승인 트리거는 제목·본문·요청 ID·모델 식별 정보를 바이트 단위로 비교하고 APPROVED 상태의 되돌리기를 막습니다. prompt_summary·위험도·대상 게시글 등 모든 열을 동결하는 트리거는 아닙니다.

따라서 APPROVED를 REJECTED로 바꾼 뒤 본문을 수정하는 우회도 허용되지 않습니다.

검사 결과와 규칙은 이 트리거의 대상이 아닙니다. 검토 이후 변경하지 못하게 하는 권한·쓰기 경계는 별도로 운영해야 합니다.

릴리스 항목의 해시 대사

content_release_item.approved_content_sha256는 검토자가 승인한 시점의 해시를 복사합니다.

편집 절차는 반영 직전에 후보의 현재 해시와 다시 비교합니다.

다르면 반영을 중단하고 새 검토를 요구합니다.


누락·자가 승인·해시 검증

아래 fixture는 필수 규칙마다 PASS 행을 INSERT해 승인 경로를 설명합니다. 실제 사실·개인정보·저작권 검사를 실행하거나 콘텐츠의 안전성을 입증한 결과가 아닙니다. 운영에서는 사람이 확인한 구체적인 근거를 기록해야 합니다.

아래 릴리스 감사는 승인 상태와 내용 해시 일치를 검사합니다. 릴리스 내 순서 중복은 PK가 제한합니다. 해시는 제목과 본문을 묶을 뿐 PASS 근거의 정확성을 증명하지 않습니다.

AI 콘텐츠 검수 인수와 릴리스 무결성 대사
INSERT INTO members(email,password_hash,name,status,created_at)
VALUES('[email protected]','not-a-real-hash','AI 제출자','ACTIVE',CURRENT_TIMESTAMP(6))
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id);
SET @submitter_id=LAST_INSERT_ID();

INSERT INTO members(email,password_hash,name,status,created_at)
VALUES('[email protected]','not-a-real-hash','AI 검토자','ACTIVE',CURRENT_TIMESTAMP(6))
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id);
SET @reviewer_id=LAST_INSERT_ID();

INSERT INTO ai_content_candidate
 (source_request_id,model_reference,prompt_summary,proposed_title,proposed_content,
  risk_level,submitter_id)
VALUES
 ('request-20260718-001','reviewed-model-v1','B+Tree 입문 게시글 초안',
  'B+Tree가 범위 검색에 적합한 이유',
  'B+Tree는 정렬된 키와 균형 높이를 이용해 범위 검색 경로를 제한합니다.',
  'LOW',@submitter_id);
SET @candidate=LAST_INSERT_ID();

INSERT INTO ai_review_result
SELECT @candidate,rule_code,'PASS',
 CONCAT('검토 근거: ',description),@reviewer_id,CURRENT_TIMESTAMP(6)
FROM ai_review_rule;

CALL approve_ai_content_candidate(
 @candidate,@reviewer_id,'출처·개인정보·문체 검수 완료');

INSERT INTO content_release(release_name,release_status,approved_by,approved_at)
VALUES('database-posts-2026-07-18','READY',@reviewer_id,CURRENT_TIMESTAMP(6));
SET @release=LAST_INSERT_ID();
INSERT INTO content_release_item
SELECT @release,1,candidate_id,content_sha256
FROM ai_content_candidate WHERE candidate_id=@candidate AND review_status='APPROVED';

SELECT c.candidate_id,r.rule_code
FROM ai_content_candidate c CROSS JOIN ai_review_rule r
LEFT JOIN ai_review_result x
 ON x.candidate_id=c.candidate_id
 AND x.rule_code=r.rule_code
 AND x.review_result='PASS'
WHERE c.candidate_id=@candidate AND r.is_required=TRUE AND x.rule_code IS NULL;

SELECT x.candidate_id,x.rule_code
FROM ai_review_result x
JOIN ai_content_candidate c ON c.candidate_id=x.candidate_id
WHERE x.candidate_id=@candidate
 AND x.reviewed_by=c.submitter_id;

SELECT i.release_id,i.sequence_no
FROM content_release_item i
JOIN ai_content_candidate c ON c.candidate_id=i.candidate_id
WHERE c.review_status<>'APPROVED'
 OR i.approved_content_sha256<>c.content_sha256;

세 감사 쿼리는 필수 PASS 누락, 제출자의 자기 검수, 승인 상태·해시 불일치를 각각 확인하며 모두 0행입니다.

READY 릴리스에는 승인된 후보와 같은 해시가 한 번만 들어갑니다.

posts 반영은 이 SQL에 포함하지 않았으므로 승인과 게시의 경계가 유지됩니다.


저장·검수 선택 기준

판단 축확인할 질문
관계형FK, UNIQUE, 상태 전이, 트랜잭션이 핵심인가?
JSON한 게시글과 함께 읽는 작은 선택 문서이며 스키마 검증이 가능한가?
외부 보관대형 원본이나 장기 증거로 OLTP 조회와 분리해야 하는가?
AI 후보모델 출력이 공개 데이터에 닿기 전에 독립 검수 경계를 통과하는가?
승인필수 규칙, 근거, 검토자 분리, 해시 고정이 있는가?
반영승인과 실제 게시 사이에 버전·충돌·롤백 확인이 있는가?

핵심 회원 게시판에는 관계형 모델이 기준입니다.

JSON과 외부 보관은 구체적인 선택성과 크기 요구가 있을 때만 추가합니다.

AI 후보도 같은 원칙으로 최소 권한과 명시적 증거를 갖춥니다.


연습 문제

개인정보와 출처 없는 성능 수치를 포함한 AI 후보를 등록하세요.

PERSONAL_DATA와 FACT_SOURCE를 FAIL로 남기고 승인 프로시저가 거절하는지 확인해야 합니다.

수정된 안전한 문장은 기존 후보를 덮지 말고 새 후보로 등록합니다.

해설과 예시 답안

실패 근거를 후보별 검사 결과로 남깁니다.

필수 PASS가 누락되어 승인 호출은 오류를 발생시킵니다.

후보 상태를 REJECTED로 바꾸되 제안 본문과 해시는 감사 증거로 보존합니다.

INSERT INTO ai_content_candidate
 (source_request_id,model_reference,prompt_summary,proposed_title,proposed_content,
  risk_level,submitter_id)
VALUES
 ('request-20260718-unsafe','reviewed-model-v1','성능 홍보 문구',
  '항상 10배 빠른 설정',
  '[email protected] 사용자의 환경에서 이 설정은 항상 10배 빠르다.',
  'HIGH',@submitter_id);
SET @unsafe_candidate=LAST_INSERT_ID();

INSERT INTO ai_review_result VALUES
 (@unsafe_candidate,'PERSONAL_DATA','FAIL','이메일 주소가 본문에 포함됨',
  @reviewer_id,CURRENT_TIMESTAMP(6)),
 (@unsafe_candidate,'FACT_SOURCE','FAIL','10배 수치의 측정 조건과 출처가 없음',
  @reviewer_id,CURRENT_TIMESTAMP(6));

CALL approve_ai_content_candidate(
 @unsafe_candidate,@reviewer_id,'승인되면 안 되는 호출');
-- ERROR: required content checks not passed

UPDATE ai_content_candidate
SET review_status='REJECTED',reviewer_id=@reviewer_id,
    review_note='개인정보 제거와 근거 보강 뒤 새 후보 제출',
    reviewed_at=CURRENT_TIMESTAMP(6)
WHERE candidate_id=@unsafe_candidate;

SELECT candidate_id,review_status,HEX(content_sha256),review_note
FROM ai_content_candidate WHERE candidate_id=@unsafe_candidate;

안전하지 않은 후보는 REJECTED 상태이며 본문과 해시가 증거로 남습니다.

수정본은 새 source_request_id를 가진 후보로 등록하고 여덟 규칙을 처음부터 검수합니다.


핵심 정리

  • 회원·게시글·작성자 관계와 상태는 관계형 스키마가 맡습니다.
  • 게시글별 작은 선택 메타데이터는 스키마로 검증한 JSON에 제한합니다.
  • AI 콘텐츠는 출처와 해시, 필수 규칙, 분리된 검토자를 통과해도 자동 게시하지 않습니다.
  • 릴리스 항목은 승인된 해시를 고정하고 실제 반영은 충돌·버전·롤백을 확인하는 별도 절차가 수행합니다.

이로써 회원 게시판의 핵심 관계, 변경 이력, 집계, 유연한 문서 데이터와 안전한 AI 콘텐츠 검수까지 연결했습니다.