# 문서 스냅샷 아키텍처 계획

> **작성일**: 2026-03-06
> **목적**: 문서 보기/인쇄 시 HTML 스냅샷 기반 출력으로 전환 (B안 + 구조화 데이터 병행)
> **상태**: ✅ 코드 완료 (검증 대기)
> **영향 범위**: API(저장), React(캡처/전송), MNG(출력)

---

## 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 2 전면 보정: API 누락 수정, 오프스크린 렌더링 적용, readOnly 자동 캡처 제거 |
| **다음 작업** | Phase 4: 브라우저 검증 + 기존 partial 정리 |
| **진행률** | 13/13 (100% 코드 완료, 검증 대기) |
| **마지막 업데이트** | 2026-03-06 |

---

## 1. 개요

### 1.1 배경

현재 MNG 문서 보기(`show.blade.php`)는 문서 양식별로 전용 blade partial을 만들어 렌더링한다:
- `bending-inspection-data.blade.php` (절곡 중간검사)
- `bending-worklog.blade.php` (절곡 작업일지)

이 방식의 문제:
1. **확장 불가**: 회사마다 다양한 양식이 존재 → 양식마다 blade 파일 생성 불가
2. **스냅샷 미보장**: 하드코딩된 제품 목록/도면치수가 정책 변경 시 과거 문서를 깨뜨림
3. **이중 렌더링**: React와 MNG에서 동일 문서를 각각 렌더링 → 불일치 발생

### 1.2 목표 아키텍처

```
[React] 문서 저장 시
├── 구조화 데이터 저장 (기존 유지)
│   ├── document_data (EAV 플랫)
│   └── work_order_items.options.inspection_data (JSON 스냅샷)
└── rendered_html 저장 (신규)
    └── React가 렌더링한 HTML을 캡처 → documents.rendered_html에 저장

[MNG] 문서 보기 시
├── rendered_html 있으면 → 그대로 출력 (렌더링 로직 0)
└── rendered_html 없으면 → 기존 동적 렌더링 fallback
```

### 1.3 핵심 원칙

```
1. 하나의 view 파일로 모든 문서를 보기 (문서 양식별 blade 파일 금지)
2. rendered_html이 있으면 무조건 그것을 사용 (완전한 스냅샷)
3. 구조화 데이터는 편집/검색/통계용으로 병행 유지
4. React에서만 문서 렌더링 책임 → MNG는 출력만 담당
5. Lazy Snapshot: 조회 시 rendered_html 없으면 자동 캡처 → 저장 (점진적 스냅샷 전환)
```

### 1.4 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| 즉시 가능 | blade 템플릿 수정, 기존 partial 정리 | 불필요 |
| 컨펌 필요 | API 저장 로직 변경, React 저장 흐름 변경 | **필수** |
| 금지 | documents 테이블 구조 변경 (이미 rendered_html 존재) | 불필요 |

---

## 2. 현황 분석

### 2.1 DB 현황

`documents` 테이블에 이미 `rendered_html` (LONGTEXT, nullable) 컬럼이 존재:
- 마이그레이션: `api/database/migrations/2026_02_28_100001_add_block_data_to_documents.php`
- 현재 값: 모든 문서에서 NULL (사용 안 됨)
- **DB 변경 불필요**

### 2.2 React 현황 (구현 완료)

#### 캡처 원칙 A: 입력 시 저장 (Active Capture)

입력 화면에서 저장할 때 해당 데이터의 "문서 뷰"를 캡처. 보기(readOnly)에서는 캡처하지 않음.

| Save Path | 파일 | 방식 | 캡처 대상 |
|-----------|------|------|----------|
| 작업일지 저장 | `WorkLogModal.tsx` | contentWrapperRef.innerHTML | 작업일지 문서 뷰 |
| 검사성적서 저장 (edit) | `InspectionReportModal.tsx` | contentWrapperRef.innerHTML | 검사 성적서 문서 뷰 |
| 수입검사 저장 | `ImportInspectionInputModal.tsx` | 오프스크린 렌더링 (`captureRenderedHtml`) | 수입검사 성적서 문서 (`ImportInspectionDocument`) |
| WorkerScreen 인라인 검사 저장 | `index.tsx` | 미캡처 (데이터만 저장) | 성적서 모달에서 저장 시 캡처 |

> **WorkerScreen 인라인 저장**: 검사 입력 시점에 성적서 문서가 렌더링되지 않으므로 rendered_html 미포함.
> 이후 InspectionReportModal을 edit 모드로 열어 저장하면 캡처됨.
> 향후 오프스크린 렌더링으로 확장 가능 (템플릿 로딩 등 async 의존성 해결 필요).

#### 캡처 원칙 B: 조회 시 자동 캡처 (Lazy Snapshot)

문서 조회(view/readOnly) 시 `rendered_html`이 없으면 자동 캡처하여 백그라운드 저장.

```
문서 View 시
├── rendered_html 있음 → 그대로 표시 (기존)
└── rendered_html 없음 → 동적 렌더링 완료 후 캡처 → API로 rendered_html 저장
                          (다음 조회부터는 스냅샷 사용)
```

**적용 대상**:
- readonly 문서 (제품검사 요청서 등 — 입력 없이 자동 생성되는 문서)
- 마이그레이션 이전 기존 데이터 (rendered_html이 NULL인 과거 문서)
- WorkerScreen 인라인 저장 후 아직 모달에서 저장하지 않은 문서

**구현 방식**:
```typescript
// 문서 표시 컴포넌트에서 (DocumentViewer, Modal 등)
useEffect(() => {
  if (document && !document.rendered_html && isContentRendered) {
    const html = contentWrapperRef.current?.innerHTML
              || await captureRenderedHtml(DocumentComponent, props);
    patchDocumentRenderedHtml(document.id, html); // 백그라운드 저장
  }
}, [document, isContentRendered]);
```

**고려사항**:
- 사용자 UX 영향 없음 (백그라운드 비동기 저장)
- 조회 권한만 있는 사용자도 트리거 가능해야 함
- 동시 접속 시 중복 저장 가능 → 같은 HTML이므로 실질적 문제 없음
- 캡처 타이밍: template 로드 + 데이터 바인딩 완료 후 (isContentRendered 판단 필요)

### 2.3 API 현황 (구현 완료)

- Document 모델 `$fillable`에 `rendered_html` 포함 ✅
- `DocumentService` store/update에서 `rendered_html` 저장 ✅
- `DocumentService` upsert에서 `rendered_html` 전달 ✅ (수입검사 경로)
- `StoreRequest`/`UpdateRequest`에 `rendered_html` nullable string 검증 ✅
- `UpsertRequest`에 `rendered_html` nullable string 검증 ✅

### 2.4 MNG 현황 (구현 완료)

- `show.blade.php`: rendered_html 우선 출력, 없으면 기존 동적 렌더링 fallback ✅
- `print.blade.php`: 동일 패턴 적용 ✅
- 전용 partial 파일 (삭제 대기):
  - `partials/bending-inspection-data.blade.php`
  - `partials/bending-worklog.blade.php`

---

## 3. 작업 범위

### Phase 0: 사전 정리

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 0.1 | API Document 모델 $fillable 확인 및 rendered_html 추가 | ✅ | |
| 0.2 | 기존 절곡 전용 partial 파일 정리 방침 결정 | ✅ | rendered_html 전환 후 삭제 |

### Phase 1: API - rendered_html 저장 지원

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | Document 모델 $fillable에 rendered_html 추가 | ✅ | |
| 1.2 | DocumentService store/update에서 rendered_html 저장 | ✅ | |
| 1.3 | StoreRequest/UpdateRequest에 rendered_html 검증 추가 | ✅ | nullable, string |
| 1.4 | WorkOrderService inspection/worklog에 rendered_html 전달 | ✅ | create + update 모두 |

### Phase 2: React - HTML 캡처 및 전송

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | 오프스크린 렌더링 유틸리티 생성 | ✅ | `captureRenderedHtml()` — `flushSync` + `createRoot` |
| 2.2 | InspectionReportModal 저장 시 rendered_html 포함 전송 | ✅ | contentWrapperRef.innerHTML 캡처 |
| 2.3 | 작업일지 저장 시 rendered_html 포함 전송 | ✅ | contentWrapperRef.innerHTML 캡처 |
| 2.4 | ImportInspectionInputModal 수입검사 저장 시 rendered_html | ✅ | 오프스크린 성적서 문서 렌더링 |
| 2.5 | ReceivingManagement/actions saveInspectionData 파라미터 추가 | ✅ | rendered_html → /documents/upsert 전달 |
| 2.6 | API UpsertRequest에 rendered_html 검증 추가 | ✅ | nullable string |

### Phase 3: MNG - 스냅샷 출력

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | show.blade.php에 rendered_html 우선 출력 로직 추가 | ✅ | |
| 3.2 | 기존 전용 partial 파일 fallback으로 유지 (과도기) | ✅ | |
| 3.3 | print.blade.php에도 rendered_html 출력 적용 | ✅ | 스냅샷 우선, 레거시 fallback |

### Phase 4: 검증 및 정리

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 4.1 | 브라우저 검증 (MNG 보기/인쇄) | ⏳ | |
| 4.2 | 기존 전용 partial 파일 삭제 | ⏳ | rendered_html 전환 완료 후 |

---

## 4. 상세 작업 내용

### 4.1 HTML 캡처 방식 (Phase 2.1)

React에서 문서 컨텐츠 영역의 DOM을 캡처할 때 고려사항:

**방법 A: innerHTML 직접 추출 + CSS 인라인화**
```typescript
// 문서 컨텐츠 영역에 ref 부여
const contentRef = useRef<HTMLDivElement>(null);

// 저장 시 HTML 추출
const captureHtml = () => {
  const el = contentRef.current;
  if (!el) return '';

  // Tailwind 클래스 → 인라인 스타일 변환 (또는 스타일시트 포함)
  // 방법 1: 계산된 스타일을 인라인으로
  // 방법 2: 필요한 Tailwind CSS를 <style> 태그로 포함
  return el.innerHTML;
};
```

**방법 B: 자체 CSS 포함 완전한 HTML 조각**
```html
<div class="document-snapshot">
  <style>/* 필요한 스타일만 추출 */</style>
  <!-- 문서 컨텐츠 -->
</div>
```

**권장: 방법 B** — MNG에서 Tailwind가 로드되어 있으므로, Tailwind 클래스를 그대로 사용하되 MNG에 없는 커스텀 스타일만 `<style>` 태그로 포함. MNG도 Tailwind를 사용하므로 대부분의 클래스가 호환됨.

### 4.2 MNG 출력 구조 (Phase 3.1)

```blade
{{-- show.blade.php --}}
@if($document->rendered_html)
    {{-- 스냅샷 모드: React가 생성한 HTML 그대로 출력 --}}
    <div class="document-snapshot-container">
        {!! $document->rendered_html !!}
    </div>
@else
    {{-- 동적 모드: 기존 템플릿 기반 렌더링 (fallback) --}}
    @include('documents.partials.dynamic-render')
@endif
```

### 4.3 스타일 호환성 전략

| 항목 | React (Tailwind) | MNG (Tailwind) | 호환성 |
|------|------------------|----------------|--------|
| 기본 클래스 | px-2, py-1, border 등 | 동일 | 완전 호환 |
| 반응형 | sm:, md:, lg: | inline style 정책 | 주의 필요 |
| 커스텀 컴포넌트 | shadcn/ui | 없음 | `<style>` 포함 필요 |

**결론**: React에서 문서 영역은 순수 HTML+Tailwind로 렌더링 (shadcn 컴포넌트 미사용) → MNG 호환성 높음.
단, `@media` 쿼리나 MNG에서 빌드되지 않은 Tailwind 클래스가 있을 수 있으므로 필요 시 인라인 스타일 변환.

### 4.4 XSS 보안 고려

`{!! !!}` (unescaped output) 사용 시 XSS 위험:
- `rendered_html`은 **자체 시스템(React)에서만 생성** → 외부 입력 아님
- API 저장 시 **sanitize** 처리 권장 (script 태그, on* 이벤트 제거)
- 또는 CSP(Content Security Policy)로 인라인 스크립트 차단

---

## 5. 참고 파일

### React (수정 대상)
- `react/src/components/production/WorkOrders/documents/InspectionReportModal.tsx` — 저장 흐름 (contentWrapperRef)
- `react/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx` — 검사 문서 렌더링
- `react/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx` — 작업일지 렌더링
- `react/src/components/production/WorkOrders/documents/bending/` — 절곡 섹션 컴포넌트들
- `react/src/components/production/WorkerScreen/WorkLogModal.tsx` — 작업일지 저장 시 캡처
- `react/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx` — 수입검사 저장 시 오프스크린 캡처
- `react/src/components/material/ReceivingManagement/actions.ts` — saveInspectionData rendered_html 전달
- `react/src/lib/utils/capture-rendered-html.tsx` — 오프스크린 렌더링 유틸리티 (신규)

### API (수정 대상)
- `api/app/Models/Documents/Document.php` — $fillable
- `api/app/Services/DocumentService.php` — store/update/upsert
- `api/app/Http/Requests/Documents/StoreRequest.php` — 검증
- `api/app/Http/Requests/Documents/UpdateRequest.php` — 검증
- `api/app/Http/Requests/Document/UpsertRequest.php` — 검증 (수입검사 경로)

### MNG (수정 대상)
- `mng/resources/views/documents/show.blade.php` — 메인 보기
- `mng/resources/views/documents/print.blade.php` — 인쇄
- `mng/resources/views/documents/partials/bending-inspection-data.blade.php` — 삭제 예정
- `mng/resources/views/documents/partials/bending-worklog.blade.php` — 삭제 예정
- `mng/app/Http/Controllers/DocumentController.php` — show()

### DB
- `api/database/migrations/2026_02_28_100001_add_block_data_to_documents.php` — rendered_html 컬럼 (이미 존재)

### 문서
- `docs/features/documents/README.md` — 문서관리 시스템
- `docs/system/database/documents.md` — DB 스키마

---

## 6. 의존성 및 순서

```
Phase 0 (사전 정리)
    ↓
Phase 1 (API: rendered_html 저장)
    ↓
Phase 2 (React: HTML 캡처 + 전송)  ← Phase 1 완료 필요
    ↓
Phase 3 (MNG: 스냅샷 출력)  ← Phase 2 완료 후 데이터 존재
    ↓
Phase 4 (검증 + 정리)  ← 모든 Phase 완료 후
```

Phase 1과 Phase 3의 MNG 코드 수정은 병렬 가능 (fallback 유지).
단, 실제 데이터가 있어야 검증 가능하므로 Phase 2 완료 후 통합 검증.

---

## 7. 리스크 및 대응

| 리스크 | 영향 | 대응 |
|--------|------|------|
| HTML 용량 | 문서당 50-200KB, 수만 건 시 GB | LONGTEXT 이미 사용, 필요 시 gzip 압축 |
| Tailwind 클래스 불일치 | MNG에서 일부 스타일 깨짐 | 인라인 스타일 변환 또는 MNG Tailwind 빌드에 포함 |
| XSS | rendered_html에 악성 코드 | API에서 sanitize, CSP 정책 |
| 과도기 fallback | rendered_html 없는 기존 문서 | 기존 동적 렌더링 유지 |
| React 미경유 문서 | MNG에서만 생성된 문서 | MNG 저장 시에도 rendered_html 생성 검토 |

---

## 8. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-03-06 | - | 계획 문서 작성 | - | - |
| 2026-03-06 | Phase 0~3 | Phase 0~3 전체 구현 완료 | API/React/MNG 다수 | ✅ |
| 2026-03-06 | Phase 2.4~2.6 | 모든 저장 경로에 rendered_html 추가 | InspectionReportModal, ImportInspectionInputModal, actions.ts | ✅ |
| 2026-03-06 | Phase 2 보정 | API UpsertRequest rendered_html 누락 수정, DocumentService upsert() rendered_html 전달 추가 | UpsertRequest.php, DocumentService.php | ✅ |
| 2026-03-06 | Phase 2 보정 | ImportInspection: 입력폼 캡처 → 오프스크린 성적서 렌더링으로 변경 | ImportInspectionInputModal.tsx, capture-rendered-html.tsx | ✅ |
| 2026-03-06 | Phase 2 보정 | InspectionReportModal readOnly 자동 캡처 useEffect 제거 | InspectionReportModal.tsx | ✅ |
| 2026-03-06 | 원칙 확장 | Lazy Snapshot 패턴 추가 — 조회 시 rendered_html 없으면 자동 캡처/저장 | 아키텍처 원칙 | - |

---

## 9. 검증 결과

> 작업 완료 후 이 섹션에 검증 결과 추가

### 9.1 성공 기준

| 기준 | 달성 | 비고 |
|------|------|------|
| React 저장 시 rendered_html이 documents 테이블에 저장 | ⏳ | |
| mng.sam.kr/documents/36 에서 rendered_html로 문서 출력 | ⏳ | |
| mng.sam.kr/documents/39 에서 rendered_html로 문서 출력 | ⏳ | |
| rendered_html 없는 기존 문서가 기존대로 렌더링 | ⏳ | |
| 인쇄 시에도 스냅샷 기반 출력 | ⏳ | |
| 양식별 전용 blade 파일 없이 동작 | ⏳ | |

---

## 10. 자기완결성 점검 결과

### 10.1 체크리스트 검증

| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 스냅샷 기반 문서 출력 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.1 참조 |
| 3 | 작업 범위가 구체적인가? | ✅ | 5 Phase, 15개 작업 |
| 4 | 의존성이 명시되어 있는가? | ✅ | 6. 의존성 참조 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 5. 참고 파일 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3. 작업 범위 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 성공 기준 |
| 8 | 모호한 표현이 없는가? | ✅ | |

### 10.2 새 세션 시뮬레이션 테스트

| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | Phase 0 → 1 → 2 → 3 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5. 참고 파일 |
| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 성공 기준 |
| Q5. 막혔을 때 참고 문서는? | ✅ | 5. 참고 파일, docs/ |

---

*이 문서는 /plan 스킬로 생성되었습니다.*