From 08a60d2605291b0b24742db4e7ccd07a56a550d5 Mon Sep 17 00:00:00 2001 From: NohDoyeon241104 Date: Tue, 30 Jun 2026 14:28:31 +0900 Subject: [PATCH] feat: ubisam-ppt-make and ubisam-deliverables PPTX integration (Step 1/2) - master background + docs --- .claude/skills/ubisam-deliverables/SKILL.md | 71 ++ .../references/04-stage-deliverables.md | 39 + .../references/06-procedure.md | 39 + .../references/09-pptx-integration.md | 96 +++ .claude/skills/ubisam-ppt-make/SKILL.md | 134 ++++ .claude/skills/ubisam-ppt-make/build_deck.py | 739 ++++++++++++++++++ .../reference/layout_selection.md | 87 +++ .gitignore | 8 +- docs/작업일지/2026_06_25.md | 23 + docs/작업일지/2026_06_29.md | 71 ++ docs/작업일지/2026_06_30.md | 48 ++ 11 files changed, 1354 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/ubisam-deliverables/SKILL.md create mode 100644 .claude/skills/ubisam-deliverables/references/04-stage-deliverables.md create mode 100644 .claude/skills/ubisam-deliverables/references/06-procedure.md create mode 100644 .claude/skills/ubisam-deliverables/references/09-pptx-integration.md create mode 100644 .claude/skills/ubisam-ppt-make/SKILL.md create mode 100644 .claude/skills/ubisam-ppt-make/build_deck.py create mode 100644 .claude/skills/ubisam-ppt-make/reference/layout_selection.md create mode 100644 docs/작업일지/2026_06_25.md create mode 100644 docs/작업일지/2026_06_29.md create mode 100644 docs/작업일지/2026_06_30.md diff --git a/.claude/skills/ubisam-deliverables/SKILL.md b/.claude/skills/ubisam-deliverables/SKILL.md new file mode 100644 index 0000000..cc08f1a --- /dev/null +++ b/.claude/skills/ubisam-deliverables/SKILL.md @@ -0,0 +1,71 @@ +--- +name: ubisam-deliverables +description: >- + Ubisam 협업관리(칸반) 구조에 맞춰 프로젝트 산출물을 docs/[Ubisam산출물]/ 트리로 + 구성·관리한다. "산출물 구조 만들기", "Ubisam 양식으로 채우기", "설계/셋업/양산 산출물 + 정리", "산출물 트리 스캐폴딩" 같은 요청에 사용. 6단계(분석·설계·제작·셋업·양산·CS) + 폴더·_meta enum·일감 카드·LGPRI 양식 채움 절차를 제공. +--- + +# Ubisam 산출물 관리 SKILL + +UBISAM 협업관리는 *프로젝트(칸반 보드 1개) → 일감(6단계) → 단계별 산출물* 로 운영된다. +이 스킬은 그 구조를 **소스 레포의 `docs/[Ubisam산출물]/` 하위 파일 트리로 미러링**하고, +LGPRI 제공 양식을 프로젝트 내용으로 채워 관리한다. + +## 언제 쓰나 +- 새 프로젝트에 산출물 트리를 깔 때(스캐폴딩). +- `산출물문서/양식/`에 들어온 빈 양식을 프로젝트 내용으로 채울 때. +- 단계별 일감 상태/진행률을 갱신할 때. + +## 참조 맵 (모두 200줄 이내, 필요한 것만 읽기) +| # | 파일 | 용도 | +|---|---|---| +| 0 | [references/00-harness.md](references/00-harness.md) | **하네스**: 호스트 레포 맞물림·비침습 계약 (먼저 읽기) | +| 1 | [references/01-tree.md](references/01-tree.md) | 목표 트리·배치 규칙·우선순위 | +| 2 | [references/02-enums.md](references/02-enums.md) | 상태 4종·유형 9종·티켓코드 규칙 | +| 3 | [references/03-schema.md](references/03-schema.md) | 일감 필드·프로젝트 메타 스키마 | +| 4 | [references/04-stage-deliverables.md](references/04-stage-deliverables.md) | 6단계↔산출물 슬롯 매핑 | +| 5 | [references/05-form-schemas.md](references/05-form-schemas.md) | 설계 양식 시트·컬럼 스키마 | +| 6 | [references/06-procedure.md](references/06-procedure.md) | 분석→채움 절차·조건서식 보존 | +| 7 | [references/07-fill-mapping.md](references/07-fill-mapping.md) | 프로젝트 분석→산출물 필드 자동 채움 매핑 | +| **8** | [**references/08-design-system.md**](references/08-design-system.md) | **색상 팔레트·배너·뱃지·진행률 HTML 템플릿** | +| **9** | [**references/09-pptx-integration.md**](references/09-pptx-integration.md) | **PPTX 생성 — ubisam-ppt-make 연동 절차·슬라이드 매핑** | +| T | [templates/](templates/) | `_task` 카드·`project.yaml` 템플릿 | + +## 실행 순서 (오케스트레이션) + +### 0. 하네스 진입 (항상 먼저) +[00-harness.md](references/00-harness.md)의 비침습 계약 확인 → 호스트 `CLAUDE.md`·소스에서 프로젝트 사실 읽기 → `project_code`/`project_name` 자동 추정(실패 시 질문). + +### A. 스캐폴딩 (트리 생성) +1. 소스 레포 루트의 `.claude/`·`CLAUDE.md`·`RULES.md`는 **건드리지 않는다**. +2. `docs/[Ubisam산출물]/` 아래에 [01-tree.md](references/01-tree.md)의 트리를 생성: + - `_meta/`(enum·스키마) ← [02](references/02-enums.md)·[03](references/03-schema.md) 내용으로 채움. + - `01.분석협의 ~ 06.CS/` 6단계 폴더 + 각 `_task.md`(템플릿: [templates/_task.template.md](templates/_task.template.md)). + - 각 단계 산출물 슬롯 하위폴더 + `.gitkeep` ← [04](references/04-stage-deliverables.md). + - `산출물문서/`(+`양식/` 드롭존), `ref/`, `[분석]/`, `[작업일지]/`. +3. `RULES.MD`(산출물 영역 규칙) + `README.md`(트리 한눈에 + 단계별 현황 뱃지 표)를 루트에 작성. +4. `_meta/project.yaml`의 `{...}` 토큰은 프로젝트 정보로 교체. 모르면 그대로 두고 사용자에게 질문. +5. **디자인 적용**: 모든 `_task.md`와 `README.md`에 [08-design-system.md](references/08-design-system.md) § 7 템플릿 적용. + +### B. 양식 채움 (산출물 생성) +[06-procedure.md](references/06-procedure.md) 절차를 따른다: 분석 → 타설비 잔재 식별 → 프로젝트 내용 채움(출처 명시) → 슬롯 배치 → `_task.md` 진행률 갱신 → `[작업일지]` 기록. +- **자동 채움**: 호스트 분석 사실을 [07-fill-mapping.md](references/07-fill-mapping.md) 매핑으로 산출물 필드에 기입. +- **디자인 적용**: 생성하는 모든 MD에 [08-design-system.md](references/08-design-system.md) 배너·뱃지·섹션헤더 삽입. +- 양식의 시트·컬럼 골격은 [05-form-schemas.md](references/05-form-schemas.md) 참조(ref 불필요). +- 상태/유형 값은 [02-enums.md](references/02-enums.md) enum만 사용(임의 신규값 금지). + +### C. PPTX 생성 (ubisam-ppt-make 연동) +[09-pptx-integration.md](references/09-pptx-integration.md) 절차를 따른다. +- **2.설계 채움 완료** → `SW_설계서.pptx` (선택): MD 표/시퀀스 → JSON spec → `build_deck.py` +- **5.양산 채움 or 명시 요청** → `<설비명>_HMI_매뉴얼.pptx` (필수): View별 기능·절차·알람 → JSON spec → `build_deck.py` +- 마스터 배경(#F4F7FA + accent 바)은 `build_deck.py`가 자동 적용. + +## 불변 규칙 (항상 지킴) +- **MD 200줄 상한**: 새 문서가 길어지면 분할하고 상호 링크(`[[...]]`/상대경로)로 매핑. +- **enum 고정**: 상태·유형은 회사 공통 마스터. 새 값 발견 시 `_meta/*.enum.md` 먼저 갱신. +- **출처 명시**: 채운 내용은 원본 파일·코드 경로를 문서에 남긴다. +- **디자인 의무**: 생성하는 모든 산출물 MD에 [08-design-system.md](references/08-design-system.md) 배너·뱃지 적용. 텍스트 단독 표기 금지. +- **xlsx 재생산**: `references/build_02설계_xlsx.py` + `xlsxfill.py`로 완전 독립 생성(ref 불필요). 스타일 상수는 xlsxfill.py에 하드코딩. `set PYTHONUTF8=1 && py build_02설계_xlsx.py` 한 줄 실행. +- **루트 보호**: 레포 루트 `RULES.md` > 본 산출물 `RULES.MD`(스코프 하위). diff --git a/.claude/skills/ubisam-deliverables/references/04-stage-deliverables.md b/.claude/skills/ubisam-deliverables/references/04-stage-deliverables.md new file mode 100644 index 0000000..cefe138 --- /dev/null +++ b/.claude/skills/ubisam-deliverables/references/04-stage-deliverables.md @@ -0,0 +1,39 @@ +# 04 · 6단계 ↔ 산출물 슬롯 매핑 + +> 각 단계 폴더 = 협업관리 일감 1개. 슬롯 = 산출물 보관 하위폴더. 비면 `.gitkeep`. + +| # | 폴더 | 유형 | 슬롯(하위폴더) | 산출물 | +|---|---|---|---|---| +| 1 | `01.분석협의/` | 미팅 | `사양서/` `회의록_CR/` `마스터플랜/` | 사양서(URS)·CR회의록·마스터플랜 | +| 2 | `02.설계/` | 설계 | `UIUX_기능설계/` `시퀀스_인터록/` `IO_DB설계/` `설계리뷰_DR/` | 기능명세서·시퀀스/인터록·통신/IO맵·DB스키마·D/R회의록 | +| 3 | `03.제작/` | 제작 | `테스트결과_CheckList/` | 단위/통합 테스트 결과 보고서 | +| 4 | `04.셋업/` | 입고셋업 | `HW_IO모션점검/` `파라미터_레시피백업/` `이슈로그_IssueLog/` `SAT_인수검사보고서/` | IO/모션 점검·파라미터/레시피 백업·이슈로그·SAT 결과서 | +| 5 | `05.양산/` | 적용 및 모니터링 | `운영매뉴얼_HMI/` `최종SW_마스터백업/` `프로젝트완료보고서/` | 운영매뉴얼(HMI/알람)·최종SW 백업·완료 보고서 / **`*.pptx`** (ubisam-ppt-make Step C) | +| 6 | `06.CS/` | 요청대응 | (슬롯 없음, 건별 누적) | CS 대응 이력 | + +## 단계별 세부 산출물 정의(채움 기준) + +### 1.분석/협의 +- 요구사항 분석: 고객 사양서(URS) 분석 / 제어 시나리오 검토 → CR회의록 / 마스터플랜. + +### 2.설계 +- UI/UX·기능: HMI 화면 정의서·UI 스토리보드·기능 명세서. +- 시퀀스·인터록: 제어 시퀀스 다이어그램 & 인터록 정의서. +- I/O·DB: 통신 맵(Memory/IO/Register)·DB 스키마 정의서. +- 설계 리뷰: D/R 회의록. +- 양식 시트 골격 → [05-form-schemas.md](05-form-schemas.md). + +### 3.제작 +- 개발 환경 구축 / 핵심 모듈 / UI·메인 시퀀스 / 시뮬레이션 검증 / 단위·통합 테스트(Check-List). + +### 4.셋업 +- HW IO/모션 점검 시트 · 파라미터/레시피 백업 · 이슈 로그(Issue Log) · 호스트(MES) 연동 · SAT 결과서. + +### 5.양산 +- Ramp-up 교육 · 안정화 모니터링(운영 매뉴얼·알람 조치서) · S/W 최적화 · 최종 버전 동결(마스터 백업) · 프로젝트 완료 보고서. + +### 6.CS +- 운영 단계 요청 대응 이력(요청/조치/결과) 누적. + +## 타사 산출물 이관 가이드(예) +- 제작사양서·시퀀스 → `01`, IO/PLC 인터페이스·기능명세서·FlowChart → `02`, HMI Manual·알람조치 → `05`. diff --git a/.claude/skills/ubisam-deliverables/references/06-procedure.md b/.claude/skills/ubisam-deliverables/references/06-procedure.md new file mode 100644 index 0000000..7ca6a4e --- /dev/null +++ b/.claude/skills/ubisam-deliverables/references/06-procedure.md @@ -0,0 +1,39 @@ +# 06 · 분석 → 채움 절차 + +> 입력: `산출물문서/양식/`(빈 양식) 또는 타설비 산출물. 출력: 단계 슬롯에 채운 산출물 + 갱신된 `_task.md` + 로그. + +## 절차 +1. **분석**: 원본 구조 파악(시트/슬라이드/컬럼). 결과를 `[분석]/_<주제>-분석.md`에 기록. + - 엑셀 구조 추출(파이썬·Office 없이): `unzip -p file.xlsx xl/workbook.xml`(시트명), `xl/sharedStrings.xml`(라벨). +2. **양식 식별**: 타설비 템플릿 잔재 분리. 채울 슬롯 결정 → [04-stage-deliverables.md](04-stage-deliverables.md). + - 시트·컬럼 골격은 [05-form-schemas.md](05-form-schemas.md) 기준(ref 없어도 재현 가능). +3. **채움**: 이 프로젝트 사실로 값 작성. **출처(원본 파일·코드 경로) 명시**. + - 미확정 디바이스주소·축범위는 `(제안)` 표기. +4. **디자인 적용**: [08-design-system.md](08-design-system.md) 배너·뱃지·섹션헤더를 삽입한다. + - MD 최상단: 배너(§1) → 부제 바 순. + - 상태·유형 필드: 텍스트 대신 뱃지(§2·3) 사용. + - 진행률: `` 바(§4) 병기. + - 산출물 시트 구분 `##` 헤더: `` 적용(§5). +5. **배치**: 해당 `NN.단계명/<슬롯>/`에 저장. +6. **PPTX 생성**: [09-pptx-integration.md](09-pptx-integration.md) 참고. + - 2.설계 채움 완료 → `SW_설계서.pptx` (선택) + - 5.양산 채움 or 명시 요청 → `<설비명>_HMI_매뉴얼.pptx` (필수) + - MD 내용 → JSON spec → `ubisam-ppt-make/build_deck.py` → 슬롯에 저장 + - 임시 spec JSON 생성 후 pptx 완성 즉시 삭제 +7. **카드 갱신**: `_task.md`의 상태·진행률·첨부 매핑 갱신(PPTX 파일명 포함, [02-enums.md](02-enums.md)). +8. **기록**: `[작업일지]/<오늘>.md`에 한 일·다음 TODO. + +## xlsx 재생산 (from scratch · ref 불필요) +- `references/xlsxfill.py` — LG PRI 스타일 상수 하드코딩 + scratch-build API. +- `references/build_02설계_xlsx.py` — PtoP 데이터 내장, 4종 xlsx 자동 생성. +- 실행: cwd=repo root, `set PYTHONUTF8=1 && py .claude/skills/ubisam-deliverables/references/build_02설계_xlsx.py` +- ref xlsx 없어도 동일 외형 재현. 스타일 상수 변경 시 xlsxfill.py만 수정. + +## MD 분할 규칙 +- 모든 산출/분석 MD는 **200줄 이내**. 초과 시 주제별 분할 + 상호 링크(상대경로)로 매핑. +- 인덱스 문서(예: [분석] 폴더 README)에 1줄 포인터로 목록 유지. + +## 자기완결 보장 (ref 없이 동작) +- 이 스킬은 양식 골격을 [05-form-schemas.md](05-form-schemas.md)에 내장한다. +- `ref/`가 비어 있어도 트리·enum·양식 골격을 동일하게 생성한다. +- 새 양식 입수 시: 골격을 추출해 05 파일에 추가 → 이후 ref 없이 재현 가능. diff --git a/.claude/skills/ubisam-deliverables/references/09-pptx-integration.md b/.claude/skills/ubisam-deliverables/references/09-pptx-integration.md new file mode 100644 index 0000000..5da9ca3 --- /dev/null +++ b/.claude/skills/ubisam-deliverables/references/09-pptx-integration.md @@ -0,0 +1,96 @@ +# 09 · PPTX 통합 — ubisam-ppt-make 연동 + +> `ubisam-deliverables` Step C에서 호출. MD 산출물 데이터를 JSON spec으로 변환해 +> `ubisam-ppt-make/build_deck.py`로 PPTX를 생성한다. +> 디자인 시스템은 build_deck.py 마스터 배경 포함 자동 적용. + +--- + +## 생성 타이밍 + +| 트리거 | 생성 PPTX | 저장 슬롯 | +|---|---|---| +| 2.설계 채움 완료 | SW 설계서 (선택) | `02.설계/설계리뷰_DR/SW_설계서.pptx` | +| 5.양산 채움 or 명시 요청 | 운영매뉴얼_HMI | `05.양산/운영매뉴얼_HMI/<설비명>_HMI_매뉴얼.pptx` | +| 명시 요청 ("pptx 만들어줘") | 해당 단계 산출물 | 해당 슬롯 | + +--- + +## 실행 절차 + +``` +1. 해당 단계 MD 파일 읽기 +2. 아래 매핑표로 slide 오브젝트 목록 작성 +3. JSON spec 생성 → 스크래치 파일(임시)로 저장 +4. python 실행: + py .claude/skills/ubisam-ppt-make/build_deck.py \ + --input .json --output <슬롯경로>.pptx +5. 임시 spec 파일 삭제 +6. _task.md 첨부 목록 갱신 +``` + +--- + +## A. 2.설계 → SW 설계서 (선택) + +소스 MD → slide 타입 매핑: + +| MD 소스 | slide type | 비고 | +|---|---|---| +| — | `cover` | doc_tag="SW DESIGN DOCUMENT" | +| — | `history` | 버전/변경이력 (사용자 입력 or 빈 행) | +| 각 산출물 목록 | `agenda` | 최대 6항목 | +| 기능명세서 §메뉴구조 | `table` | headers: 메인메뉴/권한/서브뷰/기능 | +| 기능명세서 §View별기능 | `table` | View별 1슬라이드씩 | +| 시퀀스_인터록_정의서 §mermaid 노드 | `flow` | steps: 노드명+코드+kind | +| PLC신호정의서 §PC→PLC | `table` | headers: No/신호명/디바이스/Type/의미 | +| PLC신호정의서 §PLC→PC | `table` | 동일 구조 | +| 모션축_IO맵 §통신구성 | `table` | | +| 모션축_IO맵 §I/O맵 DI/DO | `table` | | +| TactTime §PC TactTime | `table` | | +| — | `closing` | | + +--- + +## B. 5.양산 → 운영매뉴얼_HMI (필수) + +| MD/소스 | slide type | 비고 | +|---|---|---| +| — | `cover` | doc_tag="HMI OPERATION MANUAL" | +| — | `history` | | +| View 목록 | `agenda` | AutoRun/Recipe/Manual/Calibration/Monitoring/Alarm | +| 각 View | `divider` | View명 섹션 구분 | +| 각 View §기능목록 | `screen_guide` | image 없으면 caption만 | +| 각 View §조작절차 | `procedure` | steps: title+desc | +| 알람 목록 | `alarm` | rows: 코드/알람명/원인/조치 | +| — | `closing` | | + +--- + +## spec JSON 최소 골격 + +```json +{ + "doc_title": "{설비명} SW 설계서", + "slides": [ + {"type": "cover", "tag": "SW DESIGN DOCUMENT", + "title": "{설비명} SW 설계서", + "subtitles": ["{설비 설명}"], + "team": "로봇자동화사업부", + "contacts": ["{담당자}"]}, + {"type": "history", "rows": []}, + {"type": "agenda", "items": []}, + {"type": "closing", "title": "Thank You", + "team": "로봇자동화사업부", "contacts": []} + ] +} +``` + +> `background_color` 키 생략 → build_deck.py 기본값 `#F4F7FA` 자동 적용. + +--- + +## 불변 규칙 +- spec JSON은 임시파일로 생성 후 pptx 완성 즉시 삭제 (작업 완료 시 정리). +- PPTX 저장 후 `_task.md` 첨부 목록에 파일명 추가. +- `ubisam-ppt-make/SKILL.md` §Verification 조건부 검증 권장 (PowerPoint 설치 시). diff --git a/.claude/skills/ubisam-ppt-make/SKILL.md b/.claude/skills/ubisam-ppt-make/SKILL.md new file mode 100644 index 0000000..5c0eba8 --- /dev/null +++ b/.claude/skills/ubisam-ppt-make/SKILL.md @@ -0,0 +1,134 @@ +--- +name: ubisam-ppt-make +description: Build a .pptx deck in the Ubisam SW 설계서 design system (extracted from P-PRJ_BFHB_UBISAM). UbiSam logo on every page, fixed fonts (한글=굴림, 영문=Arial), blue #4A8DCB accent. Use when the user wants a Ubisam-style design document deck or runs /ubisam-ppt-make. +--- + +# ubisam-ppt-make — Ubisam SW 설계서 deck builder + +Deterministic PPT builder following the design system extracted from +`P-PRJ_BFHB_UBISAM_Ver1.4.pptx` and the user's edited layout showcase +(UbiSam logo placed on every page). Takes a JSON spec and emits a `.pptx` +via python-pptx. + +## When to use + +- The user types `/ubisam-ppt-make` +- Requests like "Ubisam 설계서 PPT 만들어줘" / "유비삼 스타일 덱" / "this format으로" +- New design-document presentations that must keep Ubisam branding + +## Design system (fixed) + +- Slide: 16:9 (13.333 × 7.5 in) +- **슬라이드 마스터 배경** (모든 pptx 자동 적용): + - 캔버스: `BG_SOFT` `#F4F7FA` (연회색) — `_apply_master_background()` 에서 마스터+레이아웃 전체 설정 + - 하단 accent 바: `#4A8DCB` h=0.15in @ y=7.35in (마스터 공통) + - cover·divider·closing은 자체 full rect로 덮음; 나머지 레이아웃은 상속 + - spec JSON에서 `"background_color": "RRGGBB"` 로 재정의 가능 +- Accent: `#4A8DCB` (blue), dark `#2F5E8C`; warn `#E53E3E` +- Ink `#262A2E`, sub-gray `#6B7278`, soft panel `#F4F7FA` +- **Font rule (do not change): 한글=굴림, 영문=Arial** — applied to every run +- **UbiSam logo on every page**: cover = bottom-right, all others = top-right +- Header pattern: `0X.` badge + title + KO subtitle + split accent bar +- Footer: `Ubisam | ` + page number (content pages only) + +## 12 layout types (`slide.type`) + +Document layouts: + +| type | purpose | key fields | +|------|---------|-----------| +| `cover` | 표지 | `tag, title, subtitles[], team, contacts[]` | +| `history` | 변경 이력 표 | `rows[] = [No,Version,날짜,변경자,내용]` | +| `agenda` | 목차 그리드(≤6) | `items[] = {no,title,sub}` | +| `divider` | 섹션 간지(큰 숫자) | `no, title, subtitle` | +| `content` | 4분할 패널 | `no, title, subtitle, panels[] = {title,body}` | +| `flow` | 시퀀스 플로우차트 | `no, title, subtitle, steps[]={title,code,kind}, branch{title,sub}` | +| `table` | 스펙/IO 표 | `no, title, headers[], widths[], center_cols[], rows[][]` | +| `closing` | 마무리 | `title, message, team, contacts[]` | + +Manual layouts (HMI Manual / 27번): + +| type | purpose | key fields | +|------|---------|-----------| +| `screen_guide` | 화면 설명(캡처+번호 콜아웃) | `no, title, caption, image(PNG경로), items[]={title,desc,pos:[x,y]}` | +| `procedure` | 조작 절차(번호 단계) | `no, title, steps[]={title,desc}` | +| `icon_ref` | 버튼/아이콘 설명(3열 카드, ≤6) | `no, title, items[]={icon,name,func}` | +| `alarm` | 알람·조치(주의 배너+표) | `no, title, notice, rows[]=[코드,알람명,원인,조치]` | + +`kind` for flow steps: `"proc"` (process box) or `"dec"` (decision, blue outline). +`pos` in `screen_guide` items = callout marker offset (inch) inside the capture box. +Page numbers auto-increment over content/manual pages; cover/divider/closing excluded. +`divider`/`closing` use a white background with a `#0070C0` / `#2F5E8C` block. + +## Choosing a layout for each piece of data + +Match every chunk of source/analysis data to the layout whose intent fits it — +do not force everything into `table`. Full rules + disambiguation in +[`reference/layout_selection.md`](reference/layout_selection.md). Cheat sheet: + +- ordered **human** steps → `procedure` · machine sequence **with branches** → `flow` +- 2–4 facets, one line each → `content` · repeating rows/cols → `table` +- annotate ONE screenshot → `screen_guide` · button/icon legend → `icon_ref` +- fault → cause → action (+safety) → `alarm` · revision log → `history` + +## Workflow + +1. **Gather info** — topic, doc title, sections, slide contents, contacts. + If the user gives no content, build the sample showcase (all layouts). +2. **Classify each data chunk → layout** using the guide above, then + **write the spec JSON** — see `examples/showcase.json` and the type tables. + Reuse the original BFHB sample text only when the user wants the showcase. +3. **Run the builder**: + ```bash + cd + python build_deck.py --input spec.json --output "생성파일/.pptx" + # or, sample showcase: + python build_deck.py --output "생성파일/Ubisam_layout_showcase.pptx" + ``` +4. **Verify** — render to PNG and eyeball every slide (see Verification). +5. **Report** — output path + slide summary. + +## Spec schema (minimal) + +```json +{ + "doc_title": "P-PRJ BFHB SW 설계서", + "slides": [ + { "type": "cover", "title": "...", "subtitles": ["..."], + "team": "시스템 사업팀", "contacts": ["이름 직급 email"] }, + { "type": "divider", "no": "01", "title": "...", "subtitle": "..." }, + { "type": "content", "no": "02.", "title": "...", "subtitle": "...", + "panels": [ {"title": "...", "body": "..."} ] } + ] +} +``` + +A full working spec is in [`examples/showcase.json`](examples/showcase.json). + +## Verification (required, project rule 13) + +Export every slide to PNG and inspect; check no overflow/overlap, logo on +every page, and 한글=굴림 / 영문=Arial on all runs. + +```powershell +# PowerPoint COM export (Windows, PowerPoint installed) +$ppt = New-Object -ComObject PowerPoint.Application +$pres = $ppt.Presentations.Open($pptx, $true, $false, $false) +$i=1; foreach ($s in $pres.Slides) { $s.Export("$dir/slide$i.png","PNG",1280,720); $i++ } +$pres.Close(); $ppt.Quit() +``` + +Font audit (every `rPr` must show `ea=굴림`, `latin=Arial`): +```bash +python -c "import zipfile,re;from collections import Counter;z=zipfile.ZipFile('deck.pptx');\ +[print(Counter(re.findall(r'`/`` so Korean renders in 굴림. Always go through it. +- Logo asset: `assets/Ubisam_logo_transparent.png`. On dark divider/closing the + colored logo has lower contrast but matches the user's original placement. +- Keep `.md` files under 200 lines (project rule 4); split if it grows. diff --git a/.claude/skills/ubisam-ppt-make/build_deck.py b/.claude/skills/ubisam-ppt-make/build_deck.py new file mode 100644 index 0000000..13a0dae --- /dev/null +++ b/.claude/skills/ubisam-ppt-make/build_deck.py @@ -0,0 +1,739 @@ +# -*- coding: utf-8 -*- +""" +ubisam-ppt-make — Ubisam SW 설계서 deck builder. + +Design system extracted from P-PRJ_BFHB_UBISAM_Ver1.4.pptx and the user's +edited layout_showcase (UbiSam logo on every page). + +Font rule (fixed): 한글=굴림, 영문=Arial. + +Usage: + python build_deck.py # build sample showcase + python build_deck.py --input spec.json --output deck.pptx +""" +import os +import json +import argparse +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE +from pptx.oxml.ns import qn +from pptx.parts.image import ImagePart +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.packuri import PackURI +from lxml import etree + +HERE = os.path.dirname(os.path.abspath(__file__)) +LOGO = os.path.join(HERE, "assets", "Ubisam_logo_transparent.png") + +# ---- design tokens ---- +ACCENT = RGBColor(0x4A, 0x8D, 0xCB) +ACCENT_D = RGBColor(0x2F, 0x5E, 0x8C) +INK = RGBColor(0x26, 0x2A, 0x2E) +SUB = RGBColor(0x6B, 0x72, 0x78) +LINE = RGBColor(0xD9, 0xDD, 0xE1) +BG_SOFT = RGBColor(0xF4, 0xF7, 0xFA) +WHITE = RGBColor(0xFF, 0xFF, 0xFF) +WARN = RGBColor(0xE5, 0x3E, 0x3E) +PALE = RGBColor(0xDD, 0xEA, 0xF6) +BLUE2 = RGBColor(0x00, 0x70, 0xC0) # divider/closing block blue + +KFONT = "굴림" +EFONT = "Arial" + +EMU_W = Inches(13.333) +EMU_H = Inches(7.5) + +_NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main" +_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main" +_NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + + +# ---------- master background ---------- +def _apply_master_background(prs): + """슬라이드 마스터에 흰 배경 + Ubisam 로고 워터마크 + 하단 accent 바 적용. + 모든 슬라이드가 상속. cover·divider·closing은 자체 rect로 덮음.""" + master = prs.slide_master + spTree = master.shapes._spTree + + # 1. 배경: WHITE + try: + master.background.fill.solid() + master.background.fill.fore_color.rgb = WHITE + except Exception: + pass + for layout in prs.slide_layouts: + try: + layout.background.fill.solid() + layout.background.fill.fore_color.rgb = WHITE + except Exception: + pass + + # 2. Ubisam 로고 워터마크 (하단 전체 폭, 15% 투명도) + if os.path.exists(LOGO): + try: + with open(LOGO, "rb") as f: + blob = f.read() + pkg = master.part._package + img_part = ImagePart( + PackURI("/ppt/media/ubisam_master_wm.png"), "image/png", pkg, blob + ) + rId = master.part.relate_to(img_part, RT.IMAGE) + W, H = int(EMU_W), int(Inches(2.5)) + T = int(EMU_H) - H + wm_xml = ( + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + ) + spTree.append(etree.fromstring(wm_xml.encode("utf-8"))) + except Exception: + pass + + # 3. 하단 accent 바 + hex_c = f"{ACCENT[0]:02X}{ACCENT[1]:02X}{ACCENT[2]:02X}" + bar_xml = ( + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + ) + try: + spTree.append(etree.fromstring(bar_xml.encode("utf-8"))) + except Exception: + pass + + +# ---------- text / shape helpers ---------- +def _set_font(run, size=None, bold=None, color=None, italic=None): + f = run.font + if size is not None: + f.size = Pt(size) + if bold is not None: + f.bold = bold + if italic is not None: + f.italic = italic + if color is not None: + f.color.rgb = color + f.name = EFONT + rPr = run._r.get_or_add_rPr() + for tag in ("a:ea", "a:cs"): + el = rPr.find(qn(tag)) + if el is None: + el = rPr.makeelement(qn(tag), {}) + rPr.append(el) + el.set("typeface", KFONT if tag == "a:ea" else EFONT) + + +def add_text(slide, l, t, w, h, lines, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP): + tb = slide.shapes.add_textbox(l, t, w, h) + tf = tb.text_frame + tf.word_wrap = True + tf.vertical_anchor = anchor + for m in ("margin_left", "margin_right", "margin_top", "margin_bottom"): + setattr(tf, m, 0) + for i, spec in enumerate(lines): + text, size, bold, color = spec[0], spec[1], spec[2], spec[3] + a = spec[4] if len(spec) > 4 else align + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + p.alignment = a + r = p.add_run() + r.text = text + _set_font(r, size=size, bold=bold, color=color) + return tb + + +def rect(slide, l, t, w, h, fill=None, line_color=None, line_w=None, + shape=MSO_SHAPE.RECTANGLE): + sp = slide.shapes.add_shape(shape, l, t, w, h) + sp.shadow.inherit = False + if fill is None: + sp.fill.background() + else: + sp.fill.solid() + sp.fill.fore_color.rgb = fill + if line_color is None: + sp.line.fill.background() + else: + sp.line.color.rgb = line_color + sp.line.width = Pt(line_w or 0.75) + if sp.has_text_frame: + sp.text_frame.word_wrap = True + return sp + + +def blank_slide(prs): + return prs.slides.add_slide(prs.slide_layouts[6]) + + +def add_logo(slide, where="top", left=None, top=None, height=None): + """UbiSam logo on every page. cover=bottom-right, others=top-right. + left/top/height override the top-right default (in inches).""" + if not os.path.exists(LOGO): + return + if where == "bottom": + slide.shapes.add_picture(LOGO, Inches(10.55), Inches(6.35), height=Inches(0.62)) + else: + slide.shapes.add_picture(LOGO, Inches(10.55 if left is None else left), + Inches(0.5 if top is None else top), + height=Inches(0.55 if height is None else height)) + + +def header(slide, no, title_en, title_ko=None): + if no: + rect(slide, Inches(0.55), Inches(0.42), Inches(0.62), Inches(0.46), fill=ACCENT) + add_text(slide, Inches(0.55), Inches(0.42), Inches(0.62), Inches(0.46), + [(no, 16, True, WHITE, PP_ALIGN.CENTER)], anchor=MSO_ANCHOR.MIDDLE) + tx = 1.32 if no else 0.55 + add_text(slide, Inches(tx), Inches(0.40), Inches(8.6), Inches(0.5), + [(title_en, 22, True, INK)], anchor=MSO_ANCHOR.MIDDLE) + if title_ko: + add_text(slide, Inches(tx + 0.02), Inches(0.86), Inches(8.6), Inches(0.32), + [(title_ko, 12, False, SUB)]) + rect(slide, Inches(0.55), Inches(1.22), Inches(11.4), Inches(0.035), fill=ACCENT) + rect(slide, Inches(0.55), Inches(1.22), Inches(2.2), Inches(0.035), fill=ACCENT_D) + + +def footer(slide, page, doc_title): + add_text(slide, Inches(0.55), Inches(7.05), Inches(7.0), Inches(0.3), + [("Ubisam | " + doc_title, 9, False, SUB)]) + if page is not None: + add_text(slide, Inches(11.0), Inches(7.05), Inches(1.78), Inches(0.3), + [(str(page), 9, False, SUB, PP_ALIGN.RIGHT)]) + + +def num_badge(slide, l, t, dia, n, fill=ACCENT, color=WHITE, size=14): + rect(slide, Inches(l), Inches(t), Inches(dia), Inches(dia), fill=fill, shape=MSO_SHAPE.OVAL) + add_text(slide, Inches(l), Inches(t), Inches(dia), Inches(dia), + [(str(n), size, True, color, PP_ALIGN.CENTER)], anchor=MSO_ANCHOR.MIDDLE) + + +# ---------- layouts ---------- +def L_cover(prs, d): + s = blank_slide(prs) + rect(s, 0, 0, EMU_W, EMU_H, fill=WHITE) + rect(s, 0, 0, Inches(0.28), EMU_H, fill=ACCENT) + rect(s, Inches(0.28), 0, Inches(0.06), EMU_H, fill=ACCENT_D) + rect(s, Inches(0.9), Inches(1.7), Inches(2.4), Inches(0.42), fill=BG_SOFT) + add_text(s, Inches(0.9), Inches(1.7), Inches(2.4), Inches(0.42), + [(d.get("tag", "SW DESIGN DOCUMENT"), 11, True, ACCENT, PP_ALIGN.CENTER)], + anchor=MSO_ANCHOR.MIDDLE) + add_text(s, Inches(0.9), Inches(2.35), Inches(10.0), Inches(1.2), + [(d["title"], 40, True, INK)]) + subs = d.get("subtitles", []) + add_text(s, Inches(0.92), Inches(3.55), Inches(10.5), Inches(0.9), + [(t, 15 if i == 0 else 12, False, SUB) for i, t in enumerate(subs)]) + rect(s, Inches(0.92), Inches(4.7), Inches(3.2), Inches(0.03), fill=LINE) + contacts = d.get("contacts", []) + cl = [(d.get("team", "시스템 사업팀"), 13, True, INK)] + cl += [(c, 11, False, SUB) for c in contacts] + add_text(s, Inches(0.92), Inches(4.95), Inches(8.0), Inches(1.4), cl) + add_logo(s, "bottom") + return s + + +def L_history(prs, d): + s = blank_slide(prs) + header(s, d.get("no", "00."), d.get("title", "변경 이력"), d.get("subtitle", "Revision History")) + cols = [("No.", 0.55, 0.55), ("Version", 1.4, 1.1), ("변경일자", 2.5, 1.5), + ("변경자", 4.0, 1.3), ("변경 내용", 5.3, 6.65)] + rows = d.get("rows", []) + top, rh = 1.7, 0.5 + rect(s, Inches(0.55), Inches(top), Inches(11.4), Inches(rh), fill=ACCENT) + for label, x, w in cols: + add_text(s, Inches(x), Inches(top), Inches(w), Inches(rh), + [(label, 12, True, WHITE, PP_ALIGN.CENTER)], anchor=MSO_ANCHOR.MIDDLE) + for i, row in enumerate(rows): + y = top + rh + i * rh + if i % 2 == 1: + rect(s, Inches(0.55), Inches(y), Inches(11.4), Inches(rh), fill=BG_SOFT) + for (label, x, w), val in zip(cols, row): + al = PP_ALIGN.LEFT if label == "변경 내용" else PP_ALIGN.CENTER + xx = x + 0.1 if label == "변경 내용" else x + add_text(s, Inches(xx), Inches(y), Inches(w), Inches(rh), + [(str(val), 11, False, INK, al)], anchor=MSO_ANCHOR.MIDDLE) + rect(s, Inches(0.55), Inches(y + rh), Inches(11.4), Inches(0.012), fill=LINE) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_agenda(prs, d): + s = blank_slide(prs) + header(s, "", d.get("title", "Contents"), d.get("subtitle", "목차")) + items = d.get("items", []) + cw, ch = 5.55, 1.45 + gx, gy = 0.55, 1.75 + for i, it in enumerate(items[:6]): + r, c = divmod(i, 2) + x = gx + c * (cw + 0.3) + y = gy + r * (ch + 0.25) + rect(s, Inches(x), Inches(y), Inches(cw), Inches(ch), fill=BG_SOFT) + rect(s, Inches(x), Inches(y), Inches(0.08), Inches(ch), fill=ACCENT) + add_text(s, Inches(x + 0.25), Inches(y + 0.18), Inches(1.2), Inches(1.0), + [(it.get("no", ""), 34, True, ACCENT)], anchor=MSO_ANCHOR.MIDDLE) + add_text(s, Inches(x + 1.55), Inches(y + 0.3), Inches(cw - 1.7), Inches(0.9), + [(it.get("title", ""), 17, True, INK), (it.get("sub", ""), 11, False, SUB)], + anchor=MSO_ANCHOR.MIDDLE) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_divider(prs, d): + s = blank_slide(prs) + rect(s, 0, 0, EMU_W, EMU_H, fill=WHITE) + rect(s, 0, Inches(3.88), EMU_W, Inches(3.62), fill=BLUE2) + add_text(s, Inches(0.9), Inches(1.3), Inches(6.0), Inches(2.2), + [(d.get("no", "01"), 130, True, BLUE2)], anchor=MSO_ANCHOR.MIDDLE) + rect(s, Inches(0.95), Inches(4.05), Inches(2.4), Inches(0.06), fill=WHITE) + add_text(s, Inches(0.95), Inches(4.25), Inches(10.0), Inches(0.84), + [(d.get("title", ""), 30, True, WHITE), (d.get("subtitle", ""), 20, False, PALE)]) + add_logo(s, left=11.3, top=0.55, height=0.5) + return s + + +def L_content(prs, d): + s = blank_slide(prs) + header(s, d.get("no", "02."), d.get("title", ""), d.get("subtitle")) + panels = d.get("panels", []) + pw, ph = 5.55, 2.35 + gx, gy = 0.55, 1.65 + for i, p in enumerate(panels[:4]): + r, c = divmod(i, 2) + x = gx + c * (pw + 0.3) + y = gy + r * (ph + 0.25) + rect(s, Inches(x), Inches(y), Inches(pw), Inches(ph), fill=WHITE, line_color=LINE, line_w=1.0) + rect(s, Inches(x), Inches(y), Inches(pw), Inches(0.42), fill=BG_SOFT) + add_text(s, Inches(x + 0.2), Inches(y), Inches(pw - 0.4), Inches(0.42), + [(p.get("title", ""), 13, True, ACCENT_D)], anchor=MSO_ANCHOR.MIDDLE) + add_text(s, Inches(x + 0.2), Inches(y + 0.42), Inches(pw - 0.4), Inches(ph - 0.42), + [("〔 " + p.get("body", "") + " 〕", 11, False, SUB, PP_ALIGN.CENTER)], + anchor=MSO_ANCHOR.MIDDLE) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_flow(prs, d): + s = blank_slide(prs) + header(s, d.get("no", "03."), d.get("title", "Sequence Flowchart"), d.get("subtitle")) + steps = d.get("steps", []) + branch = d.get("branch") + cx, bw = 4.55, 3.0 + y, gap = 1.6, 0.46 + prev_bottom, dec_y = None, None + for title, code, kind in [(s2.get("title", ""), s2.get("code", ""), s2.get("kind", "proc")) for s2 in steps]: + h = 0.58 if kind == "dec" else 0.52 + if kind == "dec" and dec_y is None: + dec_y = y + lc = ACCENT if kind == "dec" else SUB + rect(s, Inches(cx), Inches(y), Inches(bw), Inches(h), fill=WHITE, + line_color=lc, line_w=1.5 if kind == "dec" else 1.0, + shape=MSO_SHAPE.ROUNDED_RECTANGLE) + add_text(s, Inches(cx), Inches(y + 0.04), Inches(bw), Inches(h - 0.04), + [(title, 12, True, INK, PP_ALIGN.CENTER), (code, 9, False, ACCENT_D, PP_ALIGN.CENTER)], + anchor=MSO_ANCHOR.MIDDLE) + if prev_bottom is not None: + ln = s.shapes.add_connector(2, Inches(cx + bw / 2), Inches(prev_bottom), + Inches(cx + bw / 2), Inches(y)) + ln.line.color.rgb = ACCENT + ln.line.width = Pt(1.5) + prev_bottom = y + h + y += h + gap + if branch and dec_y is not None: + bh = 0.58 + by = dec_y + rect(s, Inches(1.7), Inches(by), Inches(1.9), Inches(bh), fill=BG_SOFT, + line_color=SUB, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE) + add_text(s, Inches(1.7), Inches(by), Inches(1.9), Inches(bh), + [(branch.get("title", ""), 11, True, WARN, PP_ALIGN.CENTER), + (branch.get("sub", ""), 9, False, SUB, PP_ALIGN.CENTER)], anchor=MSO_ANCHOR.MIDDLE) + ln = s.shapes.add_connector(2, Inches(cx), Inches(by + bh / 2), Inches(3.6), Inches(by + bh / 2)) + ln.line.color.rgb = WARN + ln.line.width = Pt(1.25) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def _fill_cell(cell, text, size, bold, color, center=False): + cell.vertical_anchor = MSO_ANCHOR.MIDDLE + cell.margin_left = Inches(0.08) + cell.margin_right = Inches(0.08) + cell.margin_top = Inches(0.02) + cell.margin_bottom = Inches(0.02) + tf = cell.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.alignment = PP_ALIGN.CENTER if center else PP_ALIGN.LEFT + r = p.add_run() + r.text = str(text) + _set_font(r, size=size, bold=bold, color=color) + + +def L_table(prs, d): + s = blank_slide(prs) + header(s, d.get("no", "06."), d.get("title", ""), d.get("subtitle")) + headers = d.get("headers", []) + widths = d.get("widths") or [11.4 / len(headers)] * len(headers) + data = d.get("rows", []) + center_cols = set(d.get("center_cols", [])) + rows = len(data) + 1 + tbl = s.shapes.add_table(rows, len(headers), Inches(0.55), Inches(1.7), + Inches(11.4), Inches(0.5 * rows)).table + for c, w in enumerate(widths): + tbl.columns[c].width = Inches(w) + for c, hd in enumerate(headers): + cell = tbl.cell(0, c) + cell.fill.solid() + cell.fill.fore_color.rgb = ACCENT + _fill_cell(cell, hd, 12, True, WHITE, center=True) + for r, row in enumerate(data, start=1): + for c, val in enumerate(row): + cell = tbl.cell(r, c) + cell.fill.solid() + cell.fill.fore_color.rgb = BG_SOFT if r % 2 == 0 else WHITE + _fill_cell(cell, val, 11, False, INK, center=(c in center_cols)) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_closing(prs, d): + s = blank_slide(prs) + rect(s, 0, 0, EMU_W, EMU_H, fill=WHITE) + rect(s, 0, Inches(5.0), EMU_W, Inches(2.5), fill=ACCENT_D) + add_text(s, Inches(0.83), Inches(1.8), Inches(11.0), Inches(1.62), + [(d.get("title", "Thank You"), 96, True, BLUE2)]) + if d.get("message"): + add_text(s, Inches(0.95), Inches(3.5), Inches(11.0), Inches(0.5), + [(d["message"], 14, False, INK)]) + cl = [(d.get("team", "시스템 사업팀"), 14, True, WHITE)] + cl += [(c, 12, False, PALE) for c in d.get("contacts", [])] + add_text(s, Inches(0.95), Inches(5.35), Inches(11.0), Inches(1.4), cl) + add_logo(s, left=10.55, top=0.55, height=0.6) + return s + + +# ---------- manual layouts ---------- +REGION = RGBColor(0xFF, 0x00, 0x00) # red region-highlight border (matches manual edit) + + +def _img_px_size(path): + try: + from PIL import Image + with Image.open(path) as im: + return im.size + except Exception: + return None + + +def _fit_image(slide, path, bx, by, bw, bh): + """Add picture fitted (aspect-preserved, centered) inside box (inches). + Returns the placed rect (left, top, w, h) in inches.""" + pic = slide.shapes.add_picture(path, Inches(bx), Inches(by), width=Inches(bw)) + if pic.height > Inches(bh): + sp = pic._element + sp.getparent().remove(sp) + pic = slide.shapes.add_picture(path, Inches(bx), Inches(by), height=Inches(bh)) + pic.left = Inches(bx) + (Inches(bw) - pic.width) // 2 + pic.top = Inches(by) + (Inches(bh) - pic.height) // 2 + return (pic.left / 914400.0, pic.top / 914400.0, + pic.width / 914400.0, pic.height / 914400.0) + + +def _norm_boxes(it): + """Return list of [x,y,w,h] pixel boxes from item's box/boxes field.""" + b = it.get("boxes") or it.get("box") + if not b: + return [] + return b if isinstance(b[0], (list, tuple)) else [b] + + +def L_screen_guide(prs, d): + """화면 설명: 캡처 영역(좌) + 번호 콜아웃(우). 이미지 위 영역 박스+번호 마커 지원.""" + s = blank_slide(prs) + header(s, d.get("no", "07."), d.get("title", "화면 설명"), d.get("subtitle")) + sx, sy, sw, sh_ = 0.55, 1.7, 7.3, 5.0 + rect(s, Inches(sx), Inches(sy), Inches(sw), Inches(sh_), fill=BG_SOFT, line_color=LINE, line_w=1.0) + items = d.get("items", []) + img = d.get("image") + if img and os.path.exists(img): + il, it_, iw, ih = _fit_image(s, img, sx + 0.05, sy + 0.05, sw - 0.1, sh_ - 0.1) + px = _img_px_size(img) + if px: + scale = iw / px[0] # inches per pixel (aspect preserved) + mdia = 0.30 + for i, item in enumerate(items): + for j, bx in enumerate(_norm_boxes(item)): + rl, rt = il + bx[0] * scale, it_ + bx[1] * scale + rw, rh = bx[2] * scale, bx[3] * scale + rect(s, Inches(rl), Inches(rt), Inches(rw), Inches(rh), + fill=None, line_color=REGION, line_w=1.5) + if j == 0: # number marker at first box's top-left corner + num_badge(s, rl, rt, mdia, i + 1, size=12) + else: + add_text(s, Inches(sx), Inches(sy), Inches(sw), Inches(sh_), + [("〔 " + d.get("caption", "화면 캡처 영역") + " 〕", 12, False, SUB, PP_ALIGN.CENTER)], + anchor=MSO_ANCHOR.MIDDLE) + rx, rw = 8.15, 4.65 + n = max(len(items), 1) + rh = min(1.0, (sh_ + 0.0) / n) + for i, it in enumerate(items): + pos = it.get("pos") + if pos: + num_badge(s, sx + pos[0], sy + pos[1], 0.34, i + 1, size=12) + ly = sy + i * rh + num_badge(s, rx, ly + 0.03, 0.34, i + 1, size=12) + add_text(s, Inches(rx + 0.5), Inches(ly - 0.02), Inches(rw - 0.5), Inches(rh), + [(it.get("title", ""), 13, True, INK), (it.get("desc", ""), 11, False, SUB)]) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_procedure(prs, d): + """조작 절차: 세로 번호 단계.""" + s = blank_slide(prs) + header(s, d.get("no", "07."), d.get("title", "조작 절차"), d.get("subtitle")) + steps = d.get("steps", []) + y0, y1 = 1.75, 6.8 + n = max(len(steps), 1) + rh = min(1.1, (y1 - y0) / n) + for i, st in enumerate(steps): + y = y0 + i * rh + if i % 2 == 1: + rect(s, Inches(0.55), Inches(y), Inches(11.4), Inches(rh - 0.12), fill=BG_SOFT) + num_badge(s, 0.75, y + (rh - 0.12 - 0.5) / 2, 0.5, i + 1, size=18) + add_text(s, Inches(1.55), Inches(y), Inches(10.3), Inches(rh - 0.12), + [(st.get("title", ""), 15, True, INK), (st.get("desc", ""), 12, False, SUB)], + anchor=MSO_ANCHOR.MIDDLE) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_icon_ref(prs, d): + """버튼/아이콘 설명: 카드 그리드(3열).""" + s = blank_slide(prs) + header(s, d.get("no", "07."), d.get("title", "버튼 설명"), d.get("subtitle")) + items = d.get("items", []) + cw, ch, gx, gy, gp = 3.6, 2.35, 0.55, 1.75, 0.3 + for i, it in enumerate(items[:6]): + r, c = divmod(i, 3) + x = gx + c * (cw + gp) + y = gy + r * (ch + 0.3) + rect(s, Inches(x), Inches(y), Inches(cw), Inches(ch), fill=WHITE, line_color=LINE, line_w=1.0) + rect(s, Inches(x + (cw - 1.1) / 2), Inches(y + 0.3), Inches(1.1), Inches(1.1), + fill=BG_SOFT, line_color=ACCENT, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE) + add_text(s, Inches(x + (cw - 1.1) / 2), Inches(y + 0.3), Inches(1.1), Inches(1.1), + [(it.get("icon", "ICON"), 11, True, ACCENT, PP_ALIGN.CENTER)], anchor=MSO_ANCHOR.MIDDLE) + add_text(s, Inches(x + 0.15), Inches(y + 1.5), Inches(cw - 0.3), Inches(0.35), + [(it.get("name", ""), 13, True, INK, PP_ALIGN.CENTER)]) + add_text(s, Inches(x + 0.15), Inches(y + 1.85), Inches(cw - 0.3), Inches(0.45), + [(it.get("func", ""), 10, False, SUB, PP_ALIGN.CENTER)]) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +def L_alarm(prs, d): + """알람·조치: 주의 배너 + 표.""" + s = blank_slide(prs) + header(s, d.get("no", "07."), d.get("title", "알람 및 조치 방법"), d.get("subtitle")) + rect(s, Inches(0.55), Inches(1.65), Inches(11.4), Inches(0.55), + fill=RGBColor(0xFF, 0xF4, 0xD6), line_color=YEL if False else RGBColor(0xF0, 0xB4, 0x29), line_w=1.0) + rect(s, Inches(0.7), Inches(1.78), Inches(0.95), Inches(0.3), fill=WARN, shape=MSO_SHAPE.ROUNDED_RECTANGLE) + add_text(s, Inches(0.7), Inches(1.78), Inches(0.95), Inches(0.3), + [("주의", 11, True, WHITE, PP_ALIGN.CENTER)], anchor=MSO_ANCHOR.MIDDLE) + add_text(s, Inches(1.8), Inches(1.65), Inches(10.0), Inches(0.55), + [(d.get("notice", "알람 발생 시 아래 조치 방법을 순서대로 수행하십시오."), 12, True, INK)], + anchor=MSO_ANCHOR.MIDDLE) + headers = d.get("headers", ["코드", "알람명", "발생 원인", "조치 방법"]) + widths = d.get("widths") or [1.4, 3.0, 3.5, 3.5] + data = d.get("rows", []) + center_cols = set(d.get("center_cols", [0])) + rows = len(data) + 1 + tbl = s.shapes.add_table(rows, len(headers), Inches(0.55), Inches(2.45), + Inches(11.4), Inches(0.5 * rows)).table + for c, w in enumerate(widths): + tbl.columns[c].width = Inches(w) + for c, hd in enumerate(headers): + cell = tbl.cell(0, c) + cell.fill.solid() + cell.fill.fore_color.rgb = ACCENT + _fill_cell(cell, hd, 12, True, WHITE, center=True) + for r, row in enumerate(data, start=1): + for c, val in enumerate(row): + cell = tbl.cell(r, c) + cell.fill.solid() + cell.fill.fore_color.rgb = BG_SOFT if r % 2 == 0 else WHITE + _fill_cell(cell, val, 11, False, INK, center=(c in center_cols)) + add_logo(s) + footer(s, d.get("page"), d.get("doc_title", "")) + return s + + +BUILDERS = { + "cover": L_cover, "history": L_history, "agenda": L_agenda, "divider": L_divider, + "content": L_content, "flow": L_flow, "table": L_table, "closing": L_closing, + "screen_guide": L_screen_guide, "procedure": L_procedure, + "icon_ref": L_icon_ref, "alarm": L_alarm, +} + + +def build(spec, out_path): + prs = Presentation() + prs.slide_width = EMU_W + prs.slide_height = EMU_H + # 슬라이드 마스터 배경 적용 (흰 배경 + Ubisam 워터마크 + accent 바) + _apply_master_background(prs) + doc_title = spec.get("doc_title", "") + page = 0 + for sl in spec["slides"]: + t = sl["type"] + if t not in BUILDERS: + raise ValueError("unknown slide type: " + t) + sl.setdefault("doc_title", doc_title) + if t in ("history", "agenda", "content", "flow", "table", + "screen_guide", "procedure", "icon_ref", "alarm"): + page += 1 + sl.setdefault("page", page) + BUILDERS[t](prs, sl) + prs.save(out_path) + return len(prs.slides._sldIdLst) + + +def sample_spec(): + return { + "doc_title": "P-PRJ BFHB SW 설계서", + "slides": [ + {"type": "cover", "tag": "SW DESIGN DOCUMENT", "title": "P-PRJ BFHB SW 설계서", + "subtitles": ["인도향 P-Project · HFHB (Button Flex Hot Bar 열압착 본딩기)", + "A device that compresses a button flex by applying pressure and temperature to a point"], + "team": "시스템 사업팀", + "contacts": ["조정묵 과장 jojeongmuk@ubisam.com", "남재근 대리 namjaegeun@ubisam.com"]}, + {"type": "history", "rows": [ + ["1", "1.0", "2025.04.28", "조정묵", "초안 작성"], + ["2", "1.1", "2025.04.29", "조정묵", "UI 안, Class 내용 추가"], + ["3", "1.2", "2025.05.09", "조정묵", "250425_엑추에이터 정리.xlsx 내용 반영"], + ["4", "1.3", "2025.05.15", "조정묵", "[DFM]_LG PRI_V57_BFHB_1.2 내용 반영"], + ["5", "1.4", "2025.05.21", "조정묵", "[DFM]_LG PRI_V57_BFHB_1.7 내용 반영"], + ]}, + {"type": "agenda", "items": [ + {"no": "01", "title": "System Overview", "sub": "시스템 구성도"}, + {"no": "02", "title": "UI Concept", "sub": "Main / Teaching UI 구성"}, + {"no": "03", "title": "Sequence Flowchart", "sub": "Loading · Bonding · Unloading · PDCA"}, + {"no": "04", "title": "Class 관계도", "sub": "Class Diagram"}, + {"no": "05", "title": "축 구성", "sub": "Axis Configuration"}, + {"no": "06", "title": "IO 구성", "sub": "I/O Map"}, + ]}, + {"type": "divider", "no": "01", "title": "System Overview", "subtitle": "시스템 구성도"}, + {"type": "content", "no": "02.", "title": "UI Concept", "subtitle": "Main UI 구성 1", "panels": [ + {"title": "HotBar 측정 값", "body": "선형 그래프 (최근 생산 100개 표시)"}, + {"title": "IO Monitoring", "body": "조작 버튼 / 상태 표시"}, + {"title": "CPK", "body": "막대형 그래프"}, + {"title": "OK 판정률", "body": "도넛 그래프 (Bali 수신 + 현재공정 판정)"}, + ]}, + {"type": "flow", "no": "03.", "title": "Sequence Flowchart", "subtitle": "[Loading]", + "steps": [ + {"title": "Wait (Is Loader Empty)", "code": "SendMachine_Ready", "kind": "proc"}, + {"title": "Is UpStream Ready", "code": "CHECK_UPSTREAM", "kind": "dec"}, + {"title": "Wait (Is Board Available)", "code": "BoardAvailable", "kind": "proc"}, + {"title": "Loader Conveyor On", "code": "SendStartTransport", "kind": "proc"}, + {"title": "Is TransportError", "code": "TransportError", "kind": "dec"}, + ], + "branch": {"title": "Conveyor Stop", "sub": "Sequence Stop"}}, + {"type": "table", "no": "06.", "title": "IO 구성", "subtitle": "I/O Map", + "headers": ["No.", "Type", "Address", "Signal Name", "Description"], + "widths": [0.8, 1.4, 1.7, 3.3, 4.2], "center_cols": [0, 1, 2], + "rows": [ + ["1", "DI", "X00.0", "Start_Button", "사이클 시작 입력"], + ["2", "DI", "X00.1", "Stop_Button", "사이클 정지 입력"], + ["3", "DI", "X00.2", "EMG_Stop", "비상 정지"], + ["4", "DO", "Y10.0", "Tower_Lamp_R", "경광등 적색"], + ["5", "DO", "Y10.1", "Conveyor_Run", "컨베이어 구동"], + ["6", "DO", "Y10.2", "Stopper1_Up", "스토퍼1 상승"], + ]}, + {"type": "divider", "no": "07", "title": "Operation Manual", "subtitle": "조작 매뉴얼"}, + {"type": "screen_guide", "no": "07.", "title": "화면 설명", "subtitle": "Main 운전 화면", + "caption": "Main 운전 화면 캡처", + "items": [ + {"title": "운전 상태 표시", "desc": "현재 사이클/공정 상태 램프", "pos": [0.5, 0.5]}, + {"title": "측정값 그래프", "desc": "Load Cell · 온도 실시간 추이", "pos": [3.0, 1.5]}, + {"title": "판정 결과", "desc": "OK/NG 도넛 그래프", "pos": [5.5, 2.5]}, + {"title": "조작 버튼", "desc": "Start / Stop / Reset", "pos": [1.5, 3.8]}, + {"title": "알람 표시줄", "desc": "최근 발생 알람 메시지", "pos": [4.0, 4.3]}, + ]}, + {"type": "procedure", "no": "07.", "title": "조작 절차", "subtitle": "운전 시작 순서", + "steps": [ + {"title": "전원 ON 및 초기화", "desc": "메인 전원 인가 후 [Reset] 버튼으로 장비 초기화"}, + {"title": "원점 복귀", "desc": "[Home] 버튼 선택 → 전 축 원점 복귀 완료 확인"}, + {"title": "레시피 선택", "desc": "생산 모델에 맞는 레시피 로드 및 파라미터 확인"}, + {"title": "자동 운전 시작", "desc": "[Auto] 모드 전환 후 [Start] 버튼으로 사이클 시작"}, + {"title": "운전 종료", "desc": "[Stop] 후 사이클 완료 대기 → 전원 OFF"}, + ]}, + {"type": "icon_ref", "no": "07.", "title": "버튼 설명", "subtitle": "Main UI 조작 버튼", + "items": [ + {"icon": "▶", "name": "Start", "func": "자동 운전 시작"}, + {"icon": "■", "name": "Stop", "func": "사이클 정지"}, + {"icon": "↺", "name": "Reset", "func": "알람 해제 / 초기화"}, + {"icon": "⌂", "name": "Home", "func": "전 축 원점 복귀"}, + {"icon": "≡", "name": "Recipe", "func": "레시피 관리 화면"}, + {"icon": "⚙", "name": "Setup", "func": "설정 / 파라미터"}, + ]}, + {"type": "alarm", "no": "07.", "title": "알람 및 조치 방법", "subtitle": "주요 알람 코드", + "notice": "알람 발생 시 원인 확인 후 아래 조치 방법을 순서대로 수행하십시오.", + "rows": [ + ["AL-001", "비상정지 입력", "EMG 버튼 눌림", "안전 확인 후 EMG 해제 → Reset"], + ["AL-012", "원점 복귀 실패", "센서 미감지 / 간섭", "축 위치 확인 후 수동 원점 재시도"], + ["AL-024", "Load Cell 통신 이상", "케이블 단선 / 노이즈", "커넥터 체결 확인 후 전원 재투입"], + ["AL-031", "온도 상한 초과", "Heater 과열", "설정 온도 확인 → 냉각 후 재가동"], + ]}, + {"type": "closing", "title": "Thank You", "message": "문의 사항은 아래 담당자에게 연락 바랍니다.", + "team": "시스템 사업팀", + "contacts": ["조정묵 과장 jojeongmuk@ubisam.com", "남재근 대리 namjaegeun@ubisam.com"]}, + ], + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--input", help="spec JSON path (omit for sample showcase)") + ap.add_argument("--output", default="Ubisam_layout_showcase.pptx") + args = ap.parse_args() + if args.input: + with open(args.input, encoding="utf-8") as f: + spec = json.load(f) + else: + spec = sample_spec() + n = build(spec, args.output) + print("saved:", os.path.abspath(args.output), "slides:", n) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/ubisam-ppt-make/reference/layout_selection.md b/.claude/skills/ubisam-ppt-make/reference/layout_selection.md new file mode 100644 index 0000000..8d3922d --- /dev/null +++ b/.claude/skills/ubisam-ppt-make/reference/layout_selection.md @@ -0,0 +1,87 @@ +# Layout selection guide + +How to pick the right layout for a given piece of source/analysis data when +building a Ubisam SW 설계서 / HMI Manual deck. Read this before writing the spec. + +## Slide master background (유비샘 산출물 고정) + +`build()` 호출 시 자동으로 슬라이드 마스터에 배경을 적용한다. 모든 pptx에 공통 적용. + +| 요소 | 값 | 적용 범위 | +|---|---|---| +| 캔버스 배경색 | `BG_SOFT` #F4F7FA (연회색) | 전체 슬라이드 마스터 + 모든 레이아웃 | +| 하단 accent 바 | `ACCENT` #4A8DCB (blue), h=0.15in | 마스터 공통 장식 (y=7.35in) | + +**상속 규칙**: 개별 레이아웃에서 full-slide `rect(fill=WHITE)` 를 그리면 마스터 배경을 덮는다. +- 배경 **덮음** (자체 흰색 rect): `cover` · `divider` · `closing` +- 배경 **상속** (BG_SOFT 노출): `history` · `agenda` · `content` · `flow` · `table` · `screen_guide` · `procedure` · `icon_ref` · `alarm` + +spec JSON에서 `"background_color": "RRGGBB"` 키로 배경색 재정의 가능 (기본 = F4F7FA). + + +## Quick decision table + +| If the data is… | use | not | +|------------------|-----|-----| +| Document identity (title, 장비명, 담당자) | `cover` | — | +| Revision log (버전·날짜·작성자·변경내용) | `history` | `table` | +| List of sections / TOC | `agenda` | — | +| A chapter / section break | `divider` | — | +| 2–4 parallel sub-areas, each 1 line | `content` | `table` | +| Machine/SW execution sequence **with conditions/branches** | `flow` | `procedure` | +| Reference data in rows × columns (IO, 축, 스펙, 파라미터) | `table` | `content` | +| One screenshot to annotate by region | `screen_guide` | `icon_ref` | +| Ordered **human actions**, no branching | `procedure` | `flow` | +| Set of discrete buttons/icons (name + function, ≤6) | `icon_ref` | `table` | +| Faults with cause → action (+ safety) | `alarm` | `table` | +| Closing / contact | `closing` | — | + +## Per-layout: what data belongs + +### Document layouts + +- **cover** — The deliverable's identity. Fields: doc tag, 설계서/매뉴얼 title, + 장비 한/영 설명, team, 담당자 contacts. One per deck, first slide. +- **history** — Revision rows `[No, Version, 날짜, 변경자, 변경내용]`. Use only + for the change log; any other 5-column data → `table`. +- **agenda** — Up to 6 top-level sections `{no, title(EN), sub(KO)}`. Mirrors the + `divider` numbers used later. +- **divider** — Section break: big `no` + EN title + KO subtitle. No body data. +- **content** — 2–4 panels `{title, body}`, each panel one parallel facet + (e.g. UI 화면 영역, 기능 블록). Body is a one-line description, NOT a list of + rows. If you have ≥5 items or columnar data, switch to `table`. +- **flow** — Sequence steps `{title, code, kind}` where `code` is the + method/condition name and `kind` is `proc` or `dec` (decision). Add `branch` + for the fail/stop path. Use for control sequences, state machines, 공정 흐름. + If there are no conditions/branches and the actor is a person → `procedure`. +- **table** — Any structured reference data: IO map, 축 구성, 통신 스펙, 레시피 + 파라미터. Define `headers`, `widths`, `center_cols`. Keep cells short. +- **closing** — Thank-you + contacts. Last slide. + +### Manual layouts (HMI Manual / 27번) + +- **screen_guide** — Exactly ONE screen, annotated. `caption` = what the capture + shows; `items[]` = numbered callouts `{title, desc, pos:[x,y]}` where `pos` is + the marker offset (inch) inside the capture box. Use when regions of a single + image need labels. Many separate images → multiple `screen_guide` slides. +- **procedure** — Ordered operator steps `{title, desc}`. Each step is an action + the user performs in order (전원 ON → 원점 복귀 → …). No machine codes, no + branches (those belong in `flow`). Aim for 3–6 steps. +- **icon_ref** — ≤6 discrete controls `{icon, name, func}`. One card per button/ + icon with its function. For long control lists with extra columns → `table`. +- **alarm** — Troubleshooting rows `[코드, 알람명, 발생원인, 조치방법]` plus a + `notice` safety banner. Use for alarm/fault handling; pairs each fault with its + remedy. Generic specs without cause→action → `table`. + +## Disambiguation rules (the common mistakes) + +1. **procedure vs flow** — Person doing steps in order = `procedure`. Machine/SW + executing with `if`/branches/return-to = `flow`. +2. **content vs table** — ≤4 facets each described in a sentence = `content`. + Repeating records with the same columns = `table`. +3. **icon_ref vs table** — Each item is a single control with one function = + `icon_ref`. Items have multiple attributes/columns = `table`. +4. **alarm vs table** — Rows pair a problem with its corrective action (needs the + safety banner) = `alarm`. Pure reference rows = `table`. +5. **screen_guide vs icon_ref** — Annotating positions on one image = + `screen_guide`. Listing controls independent of an image = `icon_ref`. diff --git a/.gitignore b/.gitignore index 0c26fb2..4c1fc89 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,14 @@ bin/ /.git.zip /.gitignore /UnTrackedLocalFiles -.claude/ +.claude/* +!.claude/skills/ .serena/ +__pycache__/ +*.pyc # 로컬 전용 Aurora 에이전트 레퍼런스 (커밋 제외) /docs/Aurora에이전트레퍼런스모음/ + +# Ubisam 산출물 — 로컬 전용, GIT 추적 제외 +/docs/\[Ubisam산출물\]/ diff --git a/docs/작업일지/2026_06_25.md b/docs/작업일지/2026_06_25.md new file mode 100644 index 0000000..867544c --- /dev/null +++ b/docs/작업일지/2026_06_25.md @@ -0,0 +1,23 @@ +# 작업일지 — 2026-06-25 + +## 1. 레시피별 Front/Down 레이저 인터락 추가 (`041c155`) + +- **목적**: Use Interlock ON 시 ① 안전구역 게이트(기존)는 유지하면서, ② **레시피별 방향(Front/Down) 차단**(신규)을 추가. +- **저장**: Recipe 폴더의 `Recipe_Laser_Interlock.json`(Front/Down = 0/1)에 보관. 경로는 `FieldHelper.json`과 동일 소스(`DB.Dao.SystemRecipe.WorkRecipeFile`) 사용. +- **적용 범위**: Auto/Manual 양쪽. Manual은 ①+② 모두 신규 적용. +- **stale 방지**: VM 체크박스는 레시피 전환 시 Background 우선순위로 재로드하여 이전 레시피 값 잔존 방지. +- **변경 파일**: + - `WeldingCommon/Settings/RecipeLaserInterlock.cs`(신규): Front/Down 인터락 모델/입출력 + - `Sequences/SeqAutoRunPTOP.cs` / `Sequences/Manual/SeqComponentTeachingRun.cs`: 방향 차단 게이트 추가 + - `Views/Recipes/UCScannerObjectTeachingControlBox.xaml` + `…ViewModel.cs`: Front/Down 체크박스 UI + 레시피 전환 재로드 + - `WeldingCommon/WeldingCommon.csproj`: 신규 파일 Compile Include 등록 + +## 2. 용어정리(가접 용접) 별도 파일 분리 (`8bbac47`) + +- 200줄 규칙 준수를 위해 `architecture.md §8` 용어정리 본문을 `docs/용어정리.md`로 분리하고 `@import`로 연결. + +## 3. SW 점검 결과 — SW Version 시트 최신 Source 변경분 반영 (`48befb6`) + +- `d131ad9` 이후 소스(.cs/.xaml/.csv) 커밋 6항목 추가 반영: + 용접 전/후 Scout 핸드셰이크, Auto 발진 보정테이블/셀 offset, 레시피 스캐너 토글 창·Field 버튼, 카메라 각도 기반 Laser offset. +- 헤더 요약 갱신: 총 14→20, 정상 9→12, 확인중 5→8, 점검일자 06-24. diff --git a/docs/작업일지/2026_06_29.md b/docs/작업일지/2026_06_29.md new file mode 100644 index 0000000..ffe7d53 --- /dev/null +++ b/docs/작업일지/2026_06_29.md @@ -0,0 +1,71 @@ +# 작업일지 — 2026-06-29 + +## 1. 02.설계 xlsx 4종 — 완전 독립 빌드로 전환 + +### 배경 +이전 방식(ref xlsx 클론 후 덮어쓰기)은 `D:\Work\Processing\...` ref 파일이 없는 PC에서 실행 불가. ubisam-qa 검증에서 API 불일치(import 단계 즉시 실패) + ref 의존 2건 Critical 판정. + +### 수행 내용 +- **`xlsxfill.py` 재작성**: openpyxl 완전 독립. 스타일 상수 하드코딩. + - 배너: 맑은 고딕 13pt Bold White / `#1F4E78` (진네이비) / medium border + - 헤더: 맑은 고딕 11pt Bold White / `#2E75B6` (파랑) + - 데이터: 맑은 고딕 10pt / 홀수=`#DDEBF7`·짝수=흰색 교대 + - API: `make_workbook / write_banner / write_header_row / write_data_rows / group_merge` +- **`build_02설계_xlsx.py` 전면 재작성**: `load_workbook(REF_DIR)` 제거. 4종 xlsx를 xlsxfill scratch-build API로 처음부터 생성. + - PLC-PC 신호정의서(4탭) / 모션축·IO맵(3탭) / 기능명세서(8탭) / TactTime(2탭) + - 실행: `set PYTHONUTF8=1 && py build_02설계_xlsx.py` (cwd=repo root) +- **`05-form-schemas.md` 갱신**: "ref 클론" → "완전 독립" 방식, 스타일 상수표(색상·폰트·테두리·행높이) 추가. + +### 검증 결과 +xlsx 4종 전 항목 통과: 시트 구성, 필수 키워드(M70000/M70500/PAGE_REQ/ACS/Ajin/Aurora/SeqAutoRunPTOP), 잔재 0건, 테두리/배경색 정의 정상. + +--- + +## 2. 05.양산 운영매뉴얼_HMI PPTX 3종 생산 + +### 소스 파일 (Doc/산출물문서/ — 값 확정 원본 보존) +| 파일 | 슬라이드 | +|---|---| +| `25~26. 셋팅 기준서(SOP)_IP Setting_Setting_...` | 6 | +| `27. HMI Manual_UBISAM_260622_PTOP.pptx` | 15 | +| `27. HMI Manual_UBISAM_가접용접_매뉴얼_추가_260626_PTOP.pptx` | 8 | + +### 방법 +1. PowerShell COM으로 소스 슬라이드 PNG 렌더링(1920×1080) +2. `/ubisam-ppt-make` 스킬 + `screen_guide` 레이아웃으로 Ubisam 양식 래핑 +3. 표지(PtoP Welder 브랜딩) + 변경이력 + 섹션 간지 + 슬라이드별 screen_guide + 마무리 + +### 출력 (`docs/[Ubisam산출물]/05.양산/운영매뉴얼_HMI/`) +| 파일 | 슬라이드 | 크기 | +|---|---|---| +| `셋팅기준서_SOP.pptx` | 10 | 1.2 MB | +| `HMI_운영매뉴얼_기본.pptx` | 19 | 2.9 MB | +| `HMI_운영매뉴얼_가접용접.pptx` | 12 | 2.1 MB | + +- 담당자: 노도연 / otteryeonny@ubisam.com / 로봇자동화사업부 +- 폰트: 굴림(한글)/Arial(영문) 준수, Ubisam 로고 임베딩 확인 +- `05.양산/_task.md` 진행률 0% → 40% + +--- + +## 3. 스킬 업데이트 — ubisam-deliverables + +- `xlsxfill.py`, `build_02설계_xlsx.py`, `05-form-schemas.md` 개정. ref 파일 없이 어느 PC에서든 재생산 가능. +- ubisam-qa 2회 검증(02.설계 xlsx, 05.양산 PPTX) 실시. + +--- + +## 4. 사용자 정보 업데이트 + +- 이메일: `otteryeonny@ubisam.com` (수정, 구 `ubisam.dy241104@gmail.com`) +- 소속: 로봇자동화사업부 +- PPTX 3종 연락처 반영 완료. + +--- + +## 잔여·후속 작업 + +- `HMI_운영매뉴얼_기본.pptx`: 소스 PPTX 19슬라이드 중 COM 렌더링 15장(4장 누락 가능성). 실물 확인 후 필요시 보완. +- 가접 용접 시나리오 추가분(가접 용접 후속 매뉴얼) — 현장 추가 시나리오 발생 시 소스 PPTX 교체 후 재빌드. +- 02.설계: 모션축 행정/속도/Home 파라미터 미확정 → PLC/모션 축 확정 후 `build_02설계_xlsx.py` 내 `(제안)` 값 갱신. +- 04.셋업 산출물(HW_IO모션점검, 파라미터_레시피백업) 미작성. diff --git a/docs/작업일지/2026_06_30.md b/docs/작업일지/2026_06_30.md new file mode 100644 index 0000000..87f335e --- /dev/null +++ b/docs/작업일지/2026_06_30.md @@ -0,0 +1,48 @@ +# 작업일지 — 2026-06-30 + +## 1. 스킬 최종 검토 및 개선 + +### 배경 +커밋 전 스킬 파일 전체 재검토 요청. `xlsxfill.py`·`build_02설계_xlsx.py` 코드 품질 개선 및 `.gitignore` 구조 수정. + +### xlsxfill.py 개선 +- **`fill=None` sentinel 처리 버그 수정**: `fill or HEADER_FILL` 패턴이 `fill=None` 을 "파랑 기본값"으로 처리하던 문제. + - `_UNSET = object()` sentinel 추가. + - `write_header_row(fill=_UNSET)` → 생략 시 `HEADER_FILL`(파랑), `fill=None` 명시 시 배경 없음. + - 기능명세서·TactTime 계열의 헤더 덮어쓰기 패턴 제거. + +### build_02설계_xlsx.py 개선 +- `TODAY = datetime.date.today()` — 하드코딩(2026-06-29) → 실행일 자동 반영. +- `build_funcspec`: 중복 `from openpyxl.styles import Border, Side` + `thin()/tb()` 로컬 정의 제거 → `tb = lambda: FUNC_DATA_BORDER` 단순화. +- `build_funcspec` OverView: `write_header_row` 후 덮어쓰기 패턴 제거(`fill=None` 직접 전달). +- `build_tact`: 중복 로컬 함수 제거, `TACT_BORDER` 직접 사용, 헤더 덮어쓰기 패턴 제거. + +### 05-form-schemas.md 개선 +- 스타일 상수 표에 "기능명세서 헤더", "TactTime 헤더" 행 추가(배경 없음 명시). +- `write_header_row(fill=None)` 동작·`TODAY` 자동 반영 설명 추가. + +### ubisam-ppt-make/SKILL.md 개선 +- `screen_guide` 레이아웃 필드에 `image(PNG경로)` 추가 — 실 이미지 삽입 필드가 문서에 누락돼 있었음. + +### .gitignore 구조 수정 +| 항목 | 변경 전 | 변경 후 | +|---|---|---| +| 스킬 추적 | `.claude/` 전체 무시 | `.claude/*` + `!.claude/skills/` 예외 | +| Python 캐시 | 미정의 | `__pycache__/`, `*.pyc` 추가 | + +결과: `.claude/skills/` 하위만 git 추적, settings·memory 등 나머지는 여전히 무시. + +### 검증 (build 재실행) +- xlsx 4종 재빌드 성공 (오류 없음). +- 시트 구성·배너 색상·헤더 fill 검증 스크립트 All PASS. + - PLC-PC·모션축IO: banner=FF1F4E78, header=FF2E75B6 ✅ + - 기능명세서·TactTime: banner·header 배경 없음(00000000) ✅ + +### 기타 +- `_meta/project.yaml` 담당자 확정: 노도연 / otteryeonny@ubisam.com / 로봇자동화사업부 +- `기능명세서.md` xlsx 8탭↔md 코드기반 탭 구조 차이 명시 + +## 잔여·후속 작업 +- 커밋 전 최종 사용자 확인 대기. +- 모션축 행정/속도/Home 파라미터 미확정 → PLC/모션 확정 후 `(제안)` 값 갱신. +- 04.셋업 산출물(HW_IO모션점검, 파라미터_레시피백업) 미작성.