본문 바로가기

저장 구조와 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 게시 권한을 자동으로 함께 주지 않습니다. 승인된 해시와 사람이 검토한 릴리스 항목만 제한된 편집 절차로 전달합니다.

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

현재 제목·본문 해시를 보관한다. 승인 해시를 복사한다. 각 단계의 값과 전환 이유를 비교합니다.

CONTENT REVIEW · CANDIDATE / EVIDENCE / RELEASE

PASS 기록과 같은 내용 해시는 사실 검수의 증명을 대신하지 않는다

승인은 제출자와 다른 검토자·필수 8규칙 PASS를 확인하고 릴리스는 승인 해시를 복사합니다. 실제 사실·개인정보 검사의 근거와 검사 중 내용 고정은 별도 쓰기 정책이며 posts 반영 절차도 별도입니다.

승인된 해시를 전달해도 게시글 반영은 별도 작업이다일곱노드여섯간선. 후보와필수PASS근거뒤승인조건을판단하고실패는SIGNAL로승인호출만거절된다. REJECTED상태기록은본문의별도UPDATE다. 성공은APPROVED의일부열동결뒤릴리스항목으로해시를복사한다. 마지막점선은구현밖별도posts반영절차이며현재SQL의자동게시아니다. PASS의사실성/검사한해시/규칙버전결합은이그림으로새로보장되지않고운영정책전제다. 모바일예경로는호출오류노드를통과하지않는다.후보 제목 · 본문현재 내용 해시필수 8규칙의 PASS 근거검토자 분리승인 조건모두 통과?승인 호출 오류상태 자동 거절 아님APPROVED일부 내용 열 동결릴리스 항목승인 해시 복사별도 posts 반영버전 · 충돌 · 해시 대조아니요예점선: 별도 편집 절차
승인된 해시를 전달해도 게시글 반영은 별도 작업이다일곱노드여섯간선. 후보와필수PASS근거뒤승인조건을판단하고실패는SIGNAL로승인호출만거절된다. REJECTED상태기록은본문의별도UPDATE다. 성공은APPROVED의일부열동결뒤릴리스항목으로해시를복사한다. 마지막점선은구현밖별도posts반영절차이며현재SQL의자동게시아니다. PASS의사실성/검사한해시/규칙버전결합은이그림으로새로보장되지않고운영정책전제다. 모바일예경로는호출오류노드를통과하지않는다.후보 제목 · 본문현재 내용 해시필수 8규칙 PASS 근거검토자 분리승인 조건모두 통과?승인 호출 오류자동 REJECTED 아님APPROVED일부 내용 열 동결릴리스 항목승인 해시 복사별도 posts 반영버전 · 충돌해시 대조아니요예점선: 별도 편집 절차
검수 기록과 내용 해시가 확인하는 범위
저장 대상현재 기록자동 보장하지 않는 것
후보 내용제목·본문으로 생성한 SHA-256검사 전에 읽은 내용과 같다는 사실
검사 결과후보 ID·규칙 코드·결과·검토자·근거검사한 내용 해시·규칙 버전과의 결합
승인 상태PENDING에서 분리 검토자로 APPROVEDPASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목승인 상태에서 복사한 내용 해시실제 posts 반영과 버전·충돌 검사
후보 내용
현재 기록: 제목·본문으로 생성한 SHA-256
자동 보장하지 않는 것: 검사 전에 읽은 내용과 같다는 사실
검사 결과
현재 기록: 후보 ID·규칙 코드·결과·검토자·근거
자동 보장하지 않는 것: 검사한 내용 해시·규칙 버전과의 결합
승인 상태
현재 기록: PENDING에서 분리 검토자로 APPROVED
자동 보장하지 않는 것: PASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목
현재 기록: 승인 상태에서 복사한 내용 해시
자동 보장하지 않는 것: 실제 posts 반영과 버전·충돌 검사

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

두 FAIL이면 승인 거절 · 수정본은 새 후보로 검토

PERSONAL_DATA FAIL + FACT_SOURCE FAIL → 필수 PASS 누락

승인 호출 오류 → REJECTED로 기록 · 원문과 해시 보존

수정본 새 요청 ID → 8규칙 다시 검수 → 승인·릴리스 대사

정상 fixture의 PASS 8행 INSERT는 실제 검수 실행이나 콘텐츠 안전성 증거가 아닙니다. 내용 해시는 제목·NUL·본문을 묶고 입력 NUL 거절은 구현 밖 전제입니다. 승인 뒤 트리거도 검사 결과·규칙·릴리스 전체를 동결하지 않으며 자동 게시 SQL은 없습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 후보 대기부터 릴리스까지 진행합니다.
검수 기록과 내용 해시가 확인하는 범위
저장 대상현재 기록자동으로 보장하지 않는 것
후보 내용제목·본문으로 생성한 SHA-256검사 전에 읽은 내용과 같다는 사실
검사 결과후보 ID·규칙 코드·결과·검토자·근거검사한 내용 해시·규칙 버전과의 결합
승인 상태PENDING에서 분리 검토자로 APPROVEDPASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목승인 상태에서 복사한 내용 해시실제 posts 반영과 버전·충돌 검사
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명일곱 노드, 여섯 간선. 후보와 필수 PASS 근거 뒤 승인 조건을 판단하고 실패하면 SIGNAL로 승인 호출만 거절된다. REJECTED 상태 기록은 본문의 별도 UPDATE다. 성공하면 APPROVED의 일부 열을 동결한 뒤 릴리스 항목으로 해시를 복사한다. 마지막 점선은 구현 밖 별도 posts 반영 절차이며 현재 SQL의 자동 게시가 아니다. PASS의 사실성/검사한 해시/규칙 버전 결합은 이 그림으로 새로 보장되지 않고 운영 정책의 전제다. 모바일의 예 경로는 호출 오류 노드를 통과하지 않는다.
표의 열자동으로 보장하지 않는 것
표의 값후보 내용. 현재 기록: 제목·본문으로 생성한 SHA-256. 자동으로 보장하지 않는 것: 검사 전에 읽은 내용과 같다는 사실. 검사 결과. 현재 기록: 후보 ID·규칙 코드·결과·검토자·근거. 자동으로 보장하지 않는 것: 검사한 내용 해시·규칙 버전과의 결합. 승인 상태. 현재 기록: PENDING에서 분리 검토자로 APPROVED. 자동으로 보장하지 않는 것: PASS 근거의 사실성·규칙의 동시 변경 방지. 릴리스 항목. 현재 기록: 승인 상태에서 복사한 내용 해시. 자동으로 보장하지 않는 것: 실제 posts 반영과 버전·충돌 검사.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
후보 대기

현재 제목·본문 해시를 보관한다.

  • 해시·검수 기록 1: PENDING
  • 해시·검수 기록 2: 현재 SHA-256
  • 필수 8규칙 근거 수집
승인 검사

모든 PASS를 확인하고 검토자가 제출자와 다른지 확인한다.

  • 해시·검수 기록 1: 8 PASS
  • 해시·검수 기록 2: submitter≠reviewer
  • 실패면 SIGNAL; REJECTED 기록은 별도 UPDATE
승인

조건을 통과한 후보를 승인한다.

  • 해시·검수 기록 1: APPROVED
  • 해시·검수 기록 2: 일부 내용 열 동결
  • 검사한 해시·규칙 버전 결합/사실성은 별도 정책
릴리스

승인 해시를 복사한다.

  • 해시·검수 기록 1: release hash=approved hash
  • 해시·검수 기록 2: posts 미반영
  • 별도 편집 절차로 전달; PASS fixture는 실제 검사 실행 아님

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

현재 제목·본문 해시를 보관한다. 승인 해시를 복사한다. 각 단계의 값과 전환 이유를 비교합니다.

CONTENT REVIEW · CANDIDATE / EVIDENCE / RELEASE

PASS 기록과 같은 내용 해시는 사실 검수의 증명을 대신하지 않는다

승인은 제출자와 다른 검토자·필수 8규칙 PASS를 확인하고 릴리스는 승인 해시를 복사합니다. 실제 사실·개인정보 검사의 근거와 검사 중 내용 고정은 별도 쓰기 정책이며 posts 반영 절차도 별도입니다.

승인된 해시를 전달해도 게시글 반영은 별도 작업이다일곱노드여섯간선. 후보와필수PASS근거뒤승인조건을판단하고실패는SIGNAL로승인호출만거절된다. REJECTED상태기록은본문의별도UPDATE다. 성공은APPROVED의일부열동결뒤릴리스항목으로해시를복사한다. 마지막점선은구현밖별도posts반영절차이며현재SQL의자동게시아니다. PASS의사실성/검사한해시/규칙버전결합은이그림으로새로보장되지않고운영정책전제다. 모바일예경로는호출오류노드를통과하지않는다.후보 제목 · 본문현재 내용 해시필수 8규칙의 PASS 근거검토자 분리승인 조건모두 통과?승인 호출 오류상태 자동 거절 아님APPROVED일부 내용 열 동결릴리스 항목승인 해시 복사별도 posts 반영버전 · 충돌 · 해시 대조아니요예점선: 별도 편집 절차
승인된 해시를 전달해도 게시글 반영은 별도 작업이다일곱노드여섯간선. 후보와필수PASS근거뒤승인조건을판단하고실패는SIGNAL로승인호출만거절된다. REJECTED상태기록은본문의별도UPDATE다. 성공은APPROVED의일부열동결뒤릴리스항목으로해시를복사한다. 마지막점선은구현밖별도posts반영절차이며현재SQL의자동게시아니다. PASS의사실성/검사한해시/규칙버전결합은이그림으로새로보장되지않고운영정책전제다. 모바일예경로는호출오류노드를통과하지않는다.후보 제목 · 본문현재 내용 해시필수 8규칙 PASS 근거검토자 분리승인 조건모두 통과?승인 호출 오류자동 REJECTED 아님APPROVED일부 내용 열 동결릴리스 항목승인 해시 복사별도 posts 반영버전 · 충돌해시 대조아니요예점선: 별도 편집 절차
검수 기록과 내용 해시가 확인하는 범위
저장 대상현재 기록자동 보장하지 않는 것
후보 내용제목·본문으로 생성한 SHA-256검사 전에 읽은 내용과 같다는 사실
검사 결과후보 ID·규칙 코드·결과·검토자·근거검사한 내용 해시·규칙 버전과의 결합
승인 상태PENDING에서 분리 검토자로 APPROVEDPASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목승인 상태에서 복사한 내용 해시실제 posts 반영과 버전·충돌 검사
후보 내용
현재 기록: 제목·본문으로 생성한 SHA-256
자동 보장하지 않는 것: 검사 전에 읽은 내용과 같다는 사실
검사 결과
현재 기록: 후보 ID·규칙 코드·결과·검토자·근거
자동 보장하지 않는 것: 검사한 내용 해시·규칙 버전과의 결합
승인 상태
현재 기록: PENDING에서 분리 검토자로 APPROVED
자동 보장하지 않는 것: PASS 근거의 사실성·규칙의 동시 변경 방지
릴리스 항목
현재 기록: 승인 상태에서 복사한 내용 해시
자동 보장하지 않는 것: 실제 posts 반영과 버전·충돌 검사

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

두 FAIL이면 승인 거절 · 수정본은 새 후보로 검토

PERSONAL_DATA FAIL + FACT_SOURCE FAIL → 필수 PASS 누락

승인 호출 오류 → REJECTED로 기록 · 원문과 해시 보존

수정본 새 요청 ID → 8규칙 다시 검수 → 승인·릴리스 대사

정상 fixture의 PASS 8행 INSERT는 실제 검수 실행이나 콘텐츠 안전성 증거가 아닙니다. 내용 해시는 제목·NUL·본문을 묶고 입력 NUL 거절은 구현 밖 전제입니다. 승인 뒤 트리거도 검사 결과·규칙·릴리스 전체를 동결하지 않으며 자동 게시 SQL은 없습니다.

이 예의 조건

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

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명일곱 노드, 여섯 간선. 후보와 필수 PASS 근거 뒤 승인 조건을 판단하고 실패하면 SIGNAL로 승인 호출만 거절된다. REJECTED 상태 기록은 본문의 별도 UPDATE다. 성공하면 APPROVED의 일부 열을 동결한 뒤 릴리스 항목으로 해시를 복사한다. 마지막 점선은 구현 밖 별도 posts 반영 절차이며 현재 SQL의 자동 게시가 아니다. PASS의 사실성/검사한 해시/규칙 버전 결합은 이 그림으로 새로 보장되지 않고 운영 정책의 전제다. 모바일의 예 경로는 호출 오류 노드를 통과하지 않는다.
표의 열자동으로 보장하지 않는 것
표의 값후보 내용. 현재 기록: 제목·본문으로 생성한 SHA-256. 자동으로 보장하지 않는 것: 검사 전에 읽은 내용과 같다는 사실. 검사 결과. 현재 기록: 후보 ID·규칙 코드·결과·검토자·근거. 자동으로 보장하지 않는 것: 검사한 내용 해시·규칙 버전과의 결합. 승인 상태. 현재 기록: PENDING에서 분리 검토자로 APPROVED. 자동으로 보장하지 않는 것: PASS 근거의 사실성·규칙의 동시 변경 방지. 릴리스 항목. 현재 기록: 승인 상태에서 복사한 내용 해시. 자동으로 보장하지 않는 것: 실제 posts 반영과 버전·충돌 검사.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
후보 대기

현재 제목·본문 해시를 보관한다.

  • 해시·검수 기록 1: PENDING
  • 해시·검수 기록 2: 현재 SHA-256
  • 필수 8규칙 근거 수집
승인 검사

모든 PASS를 확인하고 검토자가 제출자와 다른지 확인한다.

  • 해시·검수 기록 1: 8 PASS
  • 해시·검수 기록 2: submitter≠reviewer
  • 실패면 SIGNAL; REJECTED 기록은 별도 UPDATE
승인

조건을 통과한 후보를 승인한다.

  • 해시·검수 기록 1: APPROVED
  • 해시·검수 기록 2: 일부 내용 열 동결
  • 검사한 해시·규칙 버전 결합/사실성은 별도 정책
릴리스

승인 해시를 복사한다.

  • 해시·검수 기록 1: release hash=approved hash
  • 해시·검수 기록 2: posts 미반영
  • 별도 편집 절차로 전달; PASS fixture는 실제 검사 실행 아님

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 콘텐츠 검수까지 연결했습니다.