@qualisoft/ai-skills 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/README.md +26 -1
- package/package.json +1 -1
- package/skills/project-init/SKILL.md +92 -0
- package/skills/project-init/build.mjs +668 -8
- package/skills/project-init/templates/script/_style.md +47 -0
- package/skills/project-init/templates/script/csv.md +49 -0
- package/skills/project-init/templates/script/docx.md +35 -0
- package/skills/project-init/templates/script/excel.md +38 -0
- package/skills/project-init/templates/script/pdf.md +35 -0
- package/skills/project-init/templates/script/pptx.md +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
# 변경 이력
|
|
2
2
|
|
|
3
|
+
## 1.3.0 — 2026-09-14
|
|
4
|
+
|
|
5
|
+
### 파일 스크립트를 **대본**으로 바꿨습니다
|
|
6
|
+
|
|
7
|
+
1.2.0 은 파이썬 코드를 내줬습니다. 실제로 필요한 건 코드가 아니라 **AI 에게 줄
|
|
8
|
+
지시문**이었습니다. 이제 `파일 스크립트` 버튼을 누르면 복사해서 에이전트에 붙여
|
|
9
|
+
넣을 대본이 나옵니다. 파이썬도, `pip install` 도 필요 없습니다.
|
|
10
|
+
|
|
11
|
+
형식은 PDF · Excel · PowerPoint · Word · CSV 다섯입니다.
|
|
12
|
+
|
|
13
|
+
**서식 규격이 대본 안에 박혀 있습니다.** 색 여섯 개, 글자 위계 네 단계, 표 스타일,
|
|
14
|
+
하지 말 것 목록이 고정되어 모든 대본에 들어갑니다. 착수 때 만들든 중간에 만들든,
|
|
15
|
+
누가 어떤 도구로 만들든 **같은 모양이 나옵니다.** 매번 "모던하게 만들어 줘" 라고
|
|
16
|
+
말하면 매번 다른 것이 나오기 때문입니다.
|
|
17
|
+
|
|
18
|
+
대본은 원본 `.md` 경로만 가리키고 "이 파일을 먼저 읽어라" 라고 지시합니다. 문서를
|
|
19
|
+
고친 뒤 같은 대본을 다시 써도 최신 내용으로 만들어집니다.
|
|
20
|
+
|
|
21
|
+
서식을 바꾸려면 `skills/project-init/templates/script/*.md` 를 고칩니다.
|
|
22
|
+
|
|
23
|
+
### 안 쓴 메뉴를 검정 점으로 죽여 둡니다
|
|
24
|
+
|
|
25
|
+
기획안을 근거로 메뉴는 만들어졌지만 끝까지 손대지 않는 문서가 생깁니다. 그런 문서를
|
|
26
|
+
"작성 중"으로 두면 진행률이 영원히 낮게 깔리고, 무엇이 남은 일이고 무엇이 애초에
|
|
27
|
+
필요 없던 항목인지 구분되지 않습니다.
|
|
28
|
+
|
|
29
|
+
한 번도 쓰이지 않은 문서는 좌측 메뉴에서 **노란 점이 검정 점으로** 바뀌고 이름이
|
|
30
|
+
흐려집니다. 판정 기준:
|
|
31
|
+
|
|
32
|
+
| 신호 | 판정 |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| 템플릿 골격을 뺀 실질 글자 수 ≥ 400 | 쓰임 |
|
|
35
|
+
| `status` 가 `draft` 가 아님 (`log` 템플릿 제외) | 쓰임 |
|
|
36
|
+
| 추적 ID 를 본문에 정의 (코드 블록 예시 제외) | 쓰임 |
|
|
37
|
+
| 위 어느 것도 아님 | **안 쓰임 — 검정 점** |
|
|
38
|
+
|
|
39
|
+
임계값 400 은 실측으로 잡았습니다 — 갓 만든 문서 37~265, 한 번이라도 채운 문서
|
|
40
|
+
996 이상.
|
|
41
|
+
|
|
42
|
+
### 사이드바 상단에서 비활성을 직접 관리합니다
|
|
43
|
+
|
|
44
|
+
**`비활성 관리`** 를 켜면 메뉴마다 체크박스가 생깁니다. 자동 판정을 사람이 덮어쓸
|
|
45
|
+
수 있습니다. 덮어쓴 값은 문서 id 로 `localStorage` 에 남아 **다시 빌드해도
|
|
46
|
+
유지됩니다.** 자동 판정과 같아지면 저장을 지워 쓸데없는 기록을 남기지 않습니다.
|
|
47
|
+
|
|
48
|
+
### 검증
|
|
49
|
+
|
|
50
|
+
브라우저에서 확인했습니다.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
갓 만든 프로젝트 15개 중 13개 검정 점 · "비활성 13 / 15"
|
|
54
|
+
실제 쓰는 프로젝트 14개 전부 정상 (오탐 0)
|
|
55
|
+
검정 점 rgb(0,0,0) + 테두리 (다크 모드에서도 보임)
|
|
56
|
+
체크 클릭 문서 이동 없음 · 저장 · 새로고침 후 유지
|
|
57
|
+
대본 치환 잔여 0건 · 서식 규격 포함 확인
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## 1.2.0 — 2026-09-14
|
|
62
|
+
|
|
63
|
+
문서를 **산출물 파일로 뽑는 스크립트**를 붙였습니다.
|
|
64
|
+
|
|
65
|
+
빌드한 HTML 의 각 문서 우측 상단에 `파일 스크립트` 버튼이 생깁니다. 누르면 모달이
|
|
66
|
+
열리고, 형식을 고르면 그 형식으로 문서를 만드는 파이썬 스크립트가 나옵니다.
|
|
67
|
+
복사해서 그대로 실행하면 됩니다.
|
|
68
|
+
|
|
69
|
+
| 형식 | 필요한 것 | 결과 |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| PDF | `pip install reportlab` | 표지·머리말·쪽번호, 줄무늬 표. 한글은 내장 CID 폰트라 폰트 파일이 필요 없습니다 |
|
|
72
|
+
| Excel (XLSX) | `pip install openpyxl` | 본문 시트 + 표마다 별도 시트 (정렬·필터 켜짐) |
|
|
73
|
+
| PowerPoint (PPTX) | `pip install python-pptx` | 표지 + `##` 하나당 슬라이드 하나 |
|
|
74
|
+
| Word (DOCX) | `pip install python-docx` | 한글 서체·표 음영 적용 |
|
|
75
|
+
| CSV | 없음 — 표준 라이브러리 | 개요 1개 + 표마다 1개 (엑셀용 BOM) |
|
|
76
|
+
|
|
77
|
+
**스크립트는 내용을 품지 않습니다.** 원본 `.md` 경로만 박혀 있고 실행할 때 그 파일을
|
|
78
|
+
다시 읽습니다. 그래서 문서를 고친 뒤 **같은 스크립트를 다시 돌리면 최신 내용**이
|
|
79
|
+
나옵니다. 계속 최신화되는 문서를 다루는 데 필요한 성질입니다.
|
|
80
|
+
|
|
81
|
+
HTML 크기도 문서 수와 무관합니다 — 스크립트는 한 벌만 들어가고 파일명은 모달을 열 때
|
|
82
|
+
브라우저에서 치환합니다.
|
|
83
|
+
|
|
84
|
+
서식을 바꾸려면 `skills/project-init/templates/export/*.py` 를 고칩니다.
|
|
85
|
+
`build.mjs` 는 그 파일을 읽어 나르기만 합니다.
|
|
86
|
+
|
|
87
|
+
### 검증
|
|
88
|
+
|
|
89
|
+
5종 모두 실제 문서(`07-screen-spec.md`, 183줄·표 20개)로 생성해 확인했습니다.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
CSV 7개 파일 PDF 8쪽 · 한글 정상
|
|
93
|
+
Excel 본문 + 표 20시트 DOCX 42KB
|
|
94
|
+
PPTX 59KB
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
브라우저에서도 확인했습니다 — 문서 14개 전부에 버튼, 형식 5종, 치환 잔여 0건,
|
|
98
|
+
모달 가운데 정렬, 183줄 스크롤, 다크 모드.
|
|
99
|
+
|
|
100
|
+
|
|
3
101
|
## 1.1.0 — 2026-09-11
|
|
4
102
|
|
|
5
103
|
설치 위치와 소유 판정을 고쳤습니다. **1.0.0 사용자는 전원 재설치가 필요합니다.**
|
|
@@ -10,6 +108,17 @@ npx @qualisoft/ai-skills@latest install --force
|
|
|
10
108
|
|
|
11
109
|
> 직접 고친 스킬이 있으면 `--force` 가 그 디렉터리를 지웁니다. 먼저 백업하세요.
|
|
12
110
|
|
|
111
|
+
> **`npx` 가 옛 버전을 실행하는 경우** — 과거에 `npm i -g @qualisoft/ai-skills` 로
|
|
112
|
+
> 전역 설치를 한 적이 있으면, `npx` 는 레지스트리에서 최신을 받지 않고 **그 전역
|
|
113
|
+
> 설치본을 그대로 실행합니다.** 버전을 확인하고, 낡았으면 전역 설치를 갱신하세요.
|
|
114
|
+
>
|
|
115
|
+
> ```bash
|
|
116
|
+
> npm ls -g --depth=0 | grep qualisoft # 전역 설치 버전 확인
|
|
117
|
+
> npm i -g @qualisoft/ai-skills@latest # 갱신
|
|
118
|
+
> ```
|
|
119
|
+
>
|
|
120
|
+
> 전역 설치를 쓰지 않는다면 버전을 명시하면 확실합니다 — `npx @qualisoft/ai-skills@latest install`
|
|
121
|
+
|
|
13
122
|
### 고친 것
|
|
14
123
|
|
|
15
124
|
**① npx 캐시를 직접 가리키던 문제** — 모든 플랫폼 해당
|
package/README.md
CHANGED
|
@@ -165,7 +165,21 @@ large 19~21개 다인원, 6개월 이상
|
|
|
165
165
|
**비유하자면** — 택배 배송 알림과 같습니다.
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
### 5.
|
|
168
|
+
### 5. 산출물은 매번 같은 모양으로 나온다
|
|
169
|
+
|
|
170
|
+
각 문서 우측 상단 **`파일 스크립트`** 버튼 → 형식(PDF · Excel · PPT · Word · CSV)을 고르면 **AI 에게 줄 대본**이 나옵니다. 복사해서 에이전트에 붙여 넣으면 됩니다. 코드가 아니라 지시문이라 설치할 것이 없습니다.
|
|
171
|
+
|
|
172
|
+
서식 규격 — 색 여섯 개, 글자 위계 네 단계, 표 스타일, 하지 말 것 — 이 대본 안에 박혀 있습니다. **착수 때 만들든 중간에 만들든 같은 모양이 나옵니다.** 매번 "모던하게 만들어 줘" 라고 말하면 매번 다른 것이 나오니까요.
|
|
173
|
+
|
|
174
|
+
대본은 원본 `.md` 를 가리킬 뿐 내용을 품지 않습니다. 문서를 고친 뒤 같은 대본을 다시 써도 최신 내용으로 만들어집니다.
|
|
175
|
+
|
|
176
|
+
### 6. 안 쓴 메뉴는 스스로 물러난다
|
|
177
|
+
|
|
178
|
+
기획안으로 메뉴는 만들어졌지만 끝까지 손대지 않는 문서가 생깁니다. 한 번도 쓰이지 않은 문서는 좌측 메뉴에서 **검정 점**으로 바뀌고 이름이 흐려집니다. 남은 일과 애초에 필요 없던 항목이 섞이지 않습니다.
|
|
179
|
+
|
|
180
|
+
사이드바 상단 **`비활성 관리`** 로 자동 판정을 직접 덮어쓸 수 있고, 그 값은 다시 빌드해도 유지됩니다.
|
|
181
|
+
|
|
182
|
+
### 7. 상용 라이브러리를 쓰지 않는다
|
|
169
183
|
|
|
170
184
|
`npm ls`가 비어 있습니다. `fs` · `path` · `url` · `zlib` 만 씁니다. 설치 시간도, 취약점 알림도, 라이선스 검토도 없습니다.
|
|
171
185
|
|
|
@@ -250,6 +264,17 @@ node skills/project-init/build.mjs selftest
|
|
|
250
264
|
npx @qualisoft/ai-skills@latest install --force
|
|
251
265
|
```
|
|
252
266
|
|
|
267
|
+
> **`npx` 가 옛 버전을 실행하는 경우** — 과거에 `npm i -g @qualisoft/ai-skills` 로
|
|
268
|
+
> 전역 설치를 한 적이 있으면, `npx` 는 레지스트리에서 최신을 받지 않고 **그 전역
|
|
269
|
+
> 설치본을 그대로 실행합니다.** 버전을 확인하고, 낡았으면 전역 설치를 갱신하세요.
|
|
270
|
+
>
|
|
271
|
+
> ```bash
|
|
272
|
+
> npm ls -g --depth=0 | grep qualisoft # 전역 설치 버전 확인
|
|
273
|
+
> npm i -g @qualisoft/ai-skills@latest # 갱신
|
|
274
|
+
> ```
|
|
275
|
+
>
|
|
276
|
+
> 전역 설치를 쓰지 않는다면 버전을 명시하면 확실합니다 — `npx @qualisoft/ai-skills@latest install`
|
|
277
|
+
|
|
253
278
|
## 저장소 · 문의
|
|
254
279
|
|
|
255
280
|
- 소스 — <https://github.com/qualisoft-service/project-ai-skills>
|
package/package.json
CHANGED
|
@@ -45,6 +45,47 @@ node ~/.claude/skills/project-init/build.mjs selftest
|
|
|
45
45
|
|
|
46
46
|
빌드한 HTML의 좌측 메뉴는 1~4단계 진행률을 자동 표시한다. 1단계는 인터뷰 기록, 2단계는 문서 상태(`draft` 25 / `review` 65 / `approved` 100 가중 평균), 3단계는 디자인 기록·스타일가이드·시안, 4단계는 `docs.config.json.stageProgress["4"]`를 기준으로 한다. 구현 스킬은 검증된 완료율만 4단계 값에 기록한다. 색은 0% 회색, 1~24% 빨강, 25~49% 노랑, 50~99% 주황, 100% 초록이다. 가이드·추적 메뉴는 회색, 홈처럼 주요 이동 항목만 파랑으로 분리한다.
|
|
47
47
|
|
|
48
|
+
## 연동 서비스
|
|
49
|
+
|
|
50
|
+
좌측에는 **`연동 서비스 관리` 한 줄만** 둔다(붙은 개수 배지 포함).
|
|
51
|
+
서비스를 전부 나열하면 사이드바가 문서 메뉴보다 길어져 연동 목록판이 된다.
|
|
52
|
+
목록은 `연동 서비스` 페이지가 맡고, 카드마다 **연동 방법 묻기 복사** 버튼이 있어
|
|
53
|
+
AI에게 그대로 붙여넣을 문구를 클립보드에 담는다.
|
|
54
|
+
|
|
55
|
+
**목록은 네 갈래에서 모은다. 아래일수록 약하다.**
|
|
56
|
+
|
|
57
|
+
| 출처 | 어디서 | 딱지 |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| **설정** | `Docs/docs.config.json` 의 `integrations` — 사람이 적은 것. **언제나 이긴다** | `설정` |
|
|
60
|
+
| **자동 감지** | `git remote origin`(github.com이면 GitHub), 루트·`docs/`·`api/`·`openapi/` 의 `openapi\|swagger.(yaml\|yml\|json)` | `자동 감지` |
|
|
61
|
+
| **기획** | **문서 본문에 서비스 이름이 있으면 연동 예정으로 본다.** 근거 문서를 카드에 적는다 | `기획` |
|
|
62
|
+
| **추천** | 위 셋이 **하나도 없을 때만** 카탈로그 기본값으로 채운다 | `추천` |
|
|
63
|
+
|
|
64
|
+
**기획에서 잡는 것이 이 체계의 요점이다.** 연동은 설정 파일보다 기획서에 먼저 적힌다 —
|
|
65
|
+
“GCP 에 배포한다”, “카카오 알림톡을 쓴다”. 그걸 사람이 설정에 한 번 더 옮겨 적게 만들지 않는다.
|
|
66
|
+
`기획` 딱지는 자동으로 올라온 것이라 **오탐일 수 있다.** 아니면 설정에서 빼면 된다.
|
|
67
|
+
|
|
68
|
+
확정된 연동은 설정에 적는다. **설정이 항상 이긴다.**
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"integrations": {
|
|
73
|
+
"notion": { "url": "https://notion.so/...", "note": "회의록 DB" },
|
|
74
|
+
"figma": { "status": "none" },
|
|
75
|
+
"sentry": { "name": "Sentry", "slug": "sentry", "color": "362D59", "what": "에러 추적", "url": "https://sentry.io/acme" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`status` 를 안 적으면 `url` 유무로 판정한다. 카탈로그에 없는 id도 쓸 수 있고,
|
|
81
|
+
그때는 `name`·`slug`(simple-icons)·`color`(# 없는 HEX)를 같이 적는다.
|
|
82
|
+
로고는 cdn.simpleicons.org 에서 받고, 막힌 환경에서는 브랜드색 이니셜로 떨어진다.
|
|
83
|
+
연동을 새로 붙였으면 `integrations` 를 고치고 `build` 를 다시 돌린다.
|
|
84
|
+
|
|
85
|
+
카탈로그에는 감지용 표기(`detect`)가 함께 들어 있다 — GitHub·Swagger·Notion·Figma·Slack·Jira(추천 기본값)와
|
|
86
|
+
GCP·AWS·Vercel·Cloudflare·GA4·GTM·Sentry·카카오·네이버·Stripe·토스페이먼츠·Firebase·Supabase·SendGrid·GitHub Actions.
|
|
87
|
+
새 서비스를 카탈로그에 넣을 때는 `detect` 를 같이 적는다. **없으면 기획에서 잡히지 않는다.**
|
|
88
|
+
|
|
48
89
|
## 작업 순서
|
|
49
90
|
|
|
50
91
|
**새 프로젝트를 시작할 때**
|
|
@@ -368,6 +409,57 @@ npm view react versions --json # 선택지가 필요하면
|
|
|
368
409
|
> 그 도메인의 프로젝트를 실제로 해보기 전에는 채우지 않는다.
|
|
369
410
|
> 겪지 않은 논점을 상상해서 적으면 다음 프로젝트가 틀린 지침을 물려받는다.
|
|
370
411
|
|
|
412
|
+
## 파일 스크립트 — 산출물 생성 대본
|
|
413
|
+
|
|
414
|
+
빌드한 HTML 의 각 문서 우측 상단에 **`파일 스크립트`** 버튼이 있다. 누르면 모달이
|
|
415
|
+
열리고, 형식을 고르면 **AI 에게 줄 지시문(대본)** 이 나온다. 코드가 아니라 대본이다.
|
|
416
|
+
복사해서 에이전트에 붙여 넣으면 그 형식으로 산출물을 만든다.
|
|
417
|
+
|
|
418
|
+
형식은 PDF · Excel · PowerPoint · Word · CSV 다섯이다.
|
|
419
|
+
|
|
420
|
+
**서식 규격이 대본 안에 박혀 있다.** 색 여섯 개, 글자 위계 네 단계, 표 스타일,
|
|
421
|
+
하지 말 것 목록이 `_style.md` 에 고정되어 모든 대본에 들어간다. 그래서 착수 때
|
|
422
|
+
만들든 중간에 만들든, 누가 어떤 도구로 만들든 **같은 모양이 나온다.** 이게 대본을
|
|
423
|
+
쓰는 이유다 — 매번 "모던하게 만들어 줘" 라고 말하면 매번 다른 것이 나온다.
|
|
424
|
+
|
|
425
|
+
**대본은 내용을 품지 않는다.** 원본 `.md` 경로만 가리키고 "이 파일을 먼저 읽어라"
|
|
426
|
+
라고 지시한다. 그래서 문서를 고친 뒤 같은 대본을 다시 써도 최신 내용으로 만들어지고,
|
|
427
|
+
HTML 크기가 문서 수와 무관하다.
|
|
428
|
+
|
|
429
|
+
대본은 `templates/script/*.md` 에 있다. `_style.md` 가 공통 서식 규격이고 빌드할 때
|
|
430
|
+
각 형식 대본의 `__STYLE__` 자리에 끼워진다. `__DOC_FILE__` · `__DOC_TITLE__` ·
|
|
431
|
+
`__DOC_STEM__` 은 모달을 열 때 브라우저에서 치환된다.
|
|
432
|
+
|
|
433
|
+
서식을 바꾸려면 **`templates/script/` 의 마크다운을 고친다.** `build.mjs` 는 파일을
|
|
434
|
+
읽어 나르기만 한다.
|
|
435
|
+
|
|
436
|
+
## 비활성 메뉴 — 안 쓴 메뉴를 죽여 둔다
|
|
437
|
+
|
|
438
|
+
기획안을 근거로 메뉴는 만들어졌지만 끝까지 손대지 않는 문서가 생긴다. 그런 문서를
|
|
439
|
+
"작성 중"으로 두면 진행률이 영원히 낮게 깔리고, **무엇이 남은 일이고 무엇이 애초에
|
|
440
|
+
필요 없던 항목인지** 구분되지 않는다.
|
|
441
|
+
|
|
442
|
+
한 번도 쓰이지 않은 문서는 좌측 메뉴에서 **검정 점**으로 바뀌고 이름이 흐려진다.
|
|
443
|
+
판정은 `docUsed()` 가 한다.
|
|
444
|
+
|
|
445
|
+
| 신호 | 판정 |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| 템플릿 골격을 뺀 실질 글자 수 ≥ 400 | 쓰임 |
|
|
448
|
+
| `status` 가 `draft` 가 아님 (`log` 템플릿 제외) | 쓰임 |
|
|
449
|
+
| 추적 ID 를 본문에 정의 (코드 블록 예시는 제외) | 쓰임 |
|
|
450
|
+
| 위 어느 것도 아님 | **안 쓰임 — 검정 점** |
|
|
451
|
+
|
|
452
|
+
임계값 400 은 실측으로 잡았다. 갓 만든 문서는 37~265, 한 번이라도 채운 문서는
|
|
453
|
+
996 이상으로 갈라진다.
|
|
454
|
+
|
|
455
|
+
> **상태나 링크를 주 신호로 쓰지 않는다.** `log` 템플릿은 `status` 가 `approved` 로
|
|
456
|
+
> 시작하고, 모든 `spec` 템플릿은 리스크 대장으로 가는 링크를 자동으로 건다.
|
|
457
|
+
> 그걸 근거로 삼으면 갓 만든 대장이 늘 '사용됨'이 된다.
|
|
458
|
+
|
|
459
|
+
**사이드바 상단 `비활성 관리`** 를 켜면 메뉴마다 체크박스가 생긴다. 자동 판정을
|
|
460
|
+
사람이 덮어쓸 수 있다. 덮어쓴 값은 문서 id 로 `localStorage` 에 남으므로 **다시
|
|
461
|
+
빌드해도 유지된다.** 자동 판정과 같아지면 저장을 지워, 쓸데없는 기록을 남기지 않는다.
|
|
462
|
+
|
|
371
463
|
## 하지 말 것
|
|
372
464
|
|
|
373
465
|
- `index.html`을 직접 고치기 — 다음 빌드에 덮어써진다.
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
statSync,
|
|
30
30
|
writeFileSync,
|
|
31
31
|
} from "node:fs";
|
|
32
|
+
import { execFileSync } from "node:child_process";
|
|
32
33
|
import { dirname, join, resolve } from "node:path";
|
|
33
34
|
import { fileURLToPath } from "node:url";
|
|
34
35
|
|
|
@@ -168,6 +169,193 @@ function loadMockups(docsDir) {
|
|
|
168
169
|
.map((f) => ({ file: f, href: DESIGN_DIR + "/" + MOCKUP_DIR + "/" + f }));
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/* ================================================================== *
|
|
173
|
+
* 연동 서비스
|
|
174
|
+
*
|
|
175
|
+
* 문서 바깥에 있는 것들(저장소·API 문서·협업 도구)이 어디에 붙어 있는지
|
|
176
|
+
* 한곳에서 본다. 목록이 어디서 오는지는 네 갈래다. **아래일수록 약하다.**
|
|
177
|
+
*
|
|
178
|
+
* 1. 설정 `docs.config.json.integrations` — 사람이 적은 것. 언제나 이긴다
|
|
179
|
+
* 2. 자동 깃 리모트 · 스펙 파일 — 빌드가 직접 본다
|
|
180
|
+
* 3. 기획 문서 본문에서 잡은 **연동 예정** — 기획에 적혀 있으면 목록에 오른다
|
|
181
|
+
* 4. 추천 1~3 이 **하나도 없을 때만** 카탈로그 기본값으로 채운다
|
|
182
|
+
*
|
|
183
|
+
* 3번이 이 체계의 요점이다. 연동은 기획에서 먼저 정해지고 나중에 붙는다.
|
|
184
|
+
* 기획서에 "GCP 에 배포한다"고 적어 두었으면 그건 이미 연동 예정이다 —
|
|
185
|
+
* 사람이 설정 파일에 한 번 더 적게 만들 이유가 없다.
|
|
186
|
+
* ================================================================== */
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `detect` 는 문서 본문에서 찾을 표기다. 서비스 이름이 본문에 있으면 연동 예정으로 본다.
|
|
190
|
+
* `recommend` 는 아무것도 못 잡았을 때 보여줄 기본 후보다.
|
|
191
|
+
* 카탈로그에 없는 서비스도 설정에 적으면 그대로 나온다.
|
|
192
|
+
*/
|
|
193
|
+
const LINK_CATALOG = [
|
|
194
|
+
{ id: "github", name: "GitHub", slug: "github", color: "181717", what: "코드 저장소 · 이슈 · PR",
|
|
195
|
+
recommend: true, detect: ["github", "깃허브"] },
|
|
196
|
+
{ id: "swagger", name: "Swagger", slug: "swagger", color: "85EA2D", what: "API 스펙 · 문서",
|
|
197
|
+
recommend: true, detect: ["swagger", "openapi", "스웨거"] },
|
|
198
|
+
{ id: "notion", name: "Notion", slug: "notion", color: "000000", what: "회의록 · 업무 위키",
|
|
199
|
+
recommend: true, detect: ["notion", "노션"] },
|
|
200
|
+
{ id: "figma", name: "Figma", slug: "figma", color: "F24E1E", what: "디자인 원본 · 시안",
|
|
201
|
+
recommend: true, detect: ["figma", "피그마"] },
|
|
202
|
+
{ id: "slack", name: "Slack", slug: "slack", color: "4A154B", what: "알림 · 커뮤니케이션",
|
|
203
|
+
recommend: true, detect: ["slack", "슬랙"] },
|
|
204
|
+
{ id: "jira", name: "Jira", slug: "jira", color: "0052CC", what: "이슈 · 스프린트",
|
|
205
|
+
recommend: true, detect: ["jira", "지라"] },
|
|
206
|
+
|
|
207
|
+
/* 아래는 추천 기본값이 아니다 — 기획에 적혀 있을 때만 올라온다 */
|
|
208
|
+
{ id: "gcp", name: "Google Cloud", slug: "googlecloud", color: "4285F4", what: "배포 · 호스팅",
|
|
209
|
+
detect: ["gcp", "google cloud", "구글 클라우드", "cloud run", "app engine"] },
|
|
210
|
+
{ id: "aws", name: "AWS", slug: "amazonwebservices", color: "232F3E", what: "배포 · 호스팅",
|
|
211
|
+
detect: ["aws", "amazon web services", "ec2", "s3 버킷"] },
|
|
212
|
+
{ id: "vercel", name: "Vercel", slug: "vercel", color: "000000", what: "배포 · 호스팅",
|
|
213
|
+
detect: ["vercel", "버셀"] },
|
|
214
|
+
{ id: "cloudflare", name: "Cloudflare", slug: "cloudflare", color: "F38020", what: "CDN · DNS",
|
|
215
|
+
detect: ["cloudflare", "클라우드플레어"] },
|
|
216
|
+
{ id: "ga4", name: "Google Analytics", slug: "googleanalytics", color: "E37400", what: "측정 · 전환 추적",
|
|
217
|
+
detect: ["ga4", "google analytics", "구글 애널리틱스"] },
|
|
218
|
+
{ id: "gtm", name: "Google Tag Manager", slug: "googletagmanager", color: "246FDB", what: "태그 운영",
|
|
219
|
+
detect: ["gtm", "google tag manager", "태그매니저", "태그 매니저"] },
|
|
220
|
+
{ id: "sentry", name: "Sentry", slug: "sentry", color: "362D59", what: "에러 추적",
|
|
221
|
+
detect: ["sentry", "센트리"] },
|
|
222
|
+
{ id: "kakao", name: "카카오", slug: "kakaotalk", color: "FFCD00", what: "채널 · 알림톡",
|
|
223
|
+
detect: ["카카오", "알림톡", "kakao"] },
|
|
224
|
+
{ id: "naver", name: "네이버", slug: "naver", color: "03C75A", what: "검색 등록 · 서치어드바이저",
|
|
225
|
+
detect: ["서치어드바이저", "네이버 웹마스터", "naver search advisor"] },
|
|
226
|
+
{ id: "stripe", name: "Stripe", slug: "stripe", color: "635BFF", what: "결제",
|
|
227
|
+
detect: ["stripe", "스트라이프"] },
|
|
228
|
+
{ id: "tosspayments", name: "토스페이먼츠", slug: "tosspayments", color: "0064FF", what: "결제",
|
|
229
|
+
detect: ["토스페이먼츠", "tosspayments", "toss payments"] },
|
|
230
|
+
{ id: "firebase", name: "Firebase", slug: "firebase", color: "DD2C00", what: "인증 · 실시간 DB",
|
|
231
|
+
detect: ["firebase", "파이어베이스"] },
|
|
232
|
+
{ id: "supabase", name: "Supabase", slug: "supabase", color: "3FCF8E", what: "DB · 인증",
|
|
233
|
+
detect: ["supabase", "수파베이스"] },
|
|
234
|
+
{ id: "sendgrid", name: "SendGrid", slug: "sendgrid", color: "1A82E2", what: "메일 발송",
|
|
235
|
+
detect: ["sendgrid", "센드그리드"] },
|
|
236
|
+
{ id: "githubactions", name: "GitHub Actions", slug: "githubactions", color: "2088FF", what: "CI · CD",
|
|
237
|
+
detect: ["github actions", "깃허브 액션", "ci/cd"] },
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
/** git@host:a/b.git → https://host/a/b */
|
|
241
|
+
function normalizeRemote(url) {
|
|
242
|
+
const ssh = url.match(/^git@([^:]+):(.+?)(\.git)?$/);
|
|
243
|
+
if (ssh) return "https://" + ssh[1] + "/" + ssh[2];
|
|
244
|
+
return url.replace(/\.git$/, "");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 프로젝트 루트에서 알아낼 수 있는 연동만 자동으로 집는다. */
|
|
248
|
+
function detectLinks(docsDir) {
|
|
249
|
+
const root = dirname(resolve(docsDir));
|
|
250
|
+
const found = {};
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const remote = execFileSync("git", ["-C", root, "remote", "get-url", "origin"], {
|
|
254
|
+
encoding: "utf8",
|
|
255
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
256
|
+
}).trim();
|
|
257
|
+
if (/github\.com/.test(remote)) found.github = { url: normalizeRemote(remote), note: "git origin" };
|
|
258
|
+
} catch {}
|
|
259
|
+
|
|
260
|
+
const specDirs = ["", "docs", "api", "openapi"];
|
|
261
|
+
for (const d of specDirs) {
|
|
262
|
+
const dir = d ? join(root, d) : root;
|
|
263
|
+
if (!existsSync(dir)) continue;
|
|
264
|
+
const hit = readdirSync(dir).find((f) => /^(openapi|swagger)\.(ya?ml|json)$/i.test(f));
|
|
265
|
+
if (hit) {
|
|
266
|
+
found.swagger = { url: (d ? "../" + d + "/" : "../") + hit, note: "스펙 파일 " + (d ? d + "/" : "") + hit };
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return found;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 기획 문서 본문에서 **연동 예정**을 잡는다.
|
|
275
|
+
*
|
|
276
|
+
* 연동은 설정 파일보다 기획서에 먼저 적힌다 — "GCP 에 배포한다", "카카오 알림톡을 쓴다".
|
|
277
|
+
* 그걸 사람이 설정에 한 번 더 옮겨 적게 만들 이유가 없으므로 본문에서 직접 읽는다.
|
|
278
|
+
* 근거(어느 문서에서 잡았는지)를 함께 돌려주어 오탐을 눈으로 판단할 수 있게 한다.
|
|
279
|
+
*/
|
|
280
|
+
function detectPlannedLinks(docs) {
|
|
281
|
+
const found = {};
|
|
282
|
+
for (const c of LINK_CATALOG) {
|
|
283
|
+
if (!c.detect || !c.detect.length) continue;
|
|
284
|
+
for (const d of docs) {
|
|
285
|
+
const body = authoredText(d.body || "");
|
|
286
|
+
const hit = c.detect.find((k) => body.includes(k.toLowerCase()));
|
|
287
|
+
if (!hit) continue;
|
|
288
|
+
const title = (d.meta && d.meta.title) || d.id;
|
|
289
|
+
found[c.id] = { planned: true, note: "기획에 언급됨 — " + title };
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return found;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 사람이 **쓴** 본문만 남긴다.
|
|
298
|
+
*
|
|
299
|
+
* 빼는 것 세 가지. 전부 실제로 오탐을 냈다.
|
|
300
|
+
* - `> **작성 지침**` — 템플릿 지침에 서비스 이름이 예시로 들어 있다.
|
|
301
|
+
* 같이 읽으면 **빈 새 프로젝트에서도 연동이 잡힌다**
|
|
302
|
+
* - 코드 블록 — 설정 예시에 서비스 id 가 들어 있다
|
|
303
|
+
* - 인라인 코드 — `openapi` 처럼 **식별자**를 가리키는 표기다.
|
|
304
|
+
* "openapi 파일을 찾는다"는 설명이지 "openapi 를 쓴다"가 아니다
|
|
305
|
+
*
|
|
306
|
+
* 지침과 식별자는 "이런 걸 적어라"이지 "이걸 쓴다"가 아니다.
|
|
307
|
+
*/
|
|
308
|
+
function authoredText(body) {
|
|
309
|
+
return body
|
|
310
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
311
|
+
.split("\n")
|
|
312
|
+
.filter((line) => !/^\s*>/.test(line))
|
|
313
|
+
.join("\n")
|
|
314
|
+
.replace(/`[^`\n]*`/g, "")
|
|
315
|
+
.toLowerCase();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 설정 + 자동 감지 + 기획 감지를 합친다. **설정이 항상 이긴다.**
|
|
320
|
+
* 셋 다 비면 카탈로그의 추천 기본값으로 채운다 — 빈 페이지를 보여주지 않기 위해서다.
|
|
321
|
+
* 카탈로그에 없는 id도 설정에 적으면 그대로 나온다(name 은 직접 적는다).
|
|
322
|
+
*/
|
|
323
|
+
function loadLinks(docsDir, config, docs = []) {
|
|
324
|
+
const conf = config.integrations || {};
|
|
325
|
+
const auto = detectLinks(docsDir);
|
|
326
|
+
const planned = detectPlannedLinks(docs);
|
|
327
|
+
|
|
328
|
+
let ids = [...new Set([...Object.keys(conf), ...Object.keys(auto), ...Object.keys(planned)])];
|
|
329
|
+
const recommended = ids.length === 0;
|
|
330
|
+
if (recommended) ids = LINK_CATALOG.filter((c) => c.recommend).map((c) => c.id);
|
|
331
|
+
|
|
332
|
+
return ids.map((id) => {
|
|
333
|
+
const base = LINK_CATALOG.find((c) => c.id === id) || { id, name: id, slug: id, color: "6b7280", what: "" };
|
|
334
|
+
const c = typeof conf[id] === "string" ? { url: conf[id] } : conf[id] || {};
|
|
335
|
+
const merged = { ...base, ...(planned[id] || {}), ...(auto[id] || {}), ...c };
|
|
336
|
+
merged.source = conf[id] ? "설정" : auto[id] ? "자동 감지" : planned[id] ? "기획" : "추천";
|
|
337
|
+
merged.connected = merged.status ? merged.status === "connected" : Boolean(merged.url);
|
|
338
|
+
merged.prompt = merged.connected
|
|
339
|
+
? merged.name + " 연동(" + (merged.url || "") + ")이 지금 제대로 물려 있는지 점검해줘. " +
|
|
340
|
+
"끊겼거나 갱신이 필요한 부분을 알려주고, 바뀐 내용은 Docs/docs.config.json 의 integrations." + id +
|
|
341
|
+
" 에 반영한 뒤 project-init build 를 다시 돌려줘."
|
|
342
|
+
: "이 프로젝트를 " + merged.name + (merged.what ? "(" + merged.what + ")" : "") + "에 연동하고 싶어. " +
|
|
343
|
+
"지금 프로젝트 상태와 이미 붙어 있는 연동을 먼저 확인하고, " + merged.name +
|
|
344
|
+
" 연동에 필요한 준비물과 단계를 순서대로 알려줘. 연동이 끝나면 Docs/docs.config.json 의 integrations." + id +
|
|
345
|
+
' 에 url 과 status:"connected" 를 기록하고 project-init build 를 다시 돌려서 문서에 반영해줘.';
|
|
346
|
+
return merged;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const LINK_ADD_PROMPT =
|
|
351
|
+
"연동하고 싶은 서비스가 하나 더 있어: (서비스 이름). 이 프로젝트에 어떻게 붙이면 되는지 알려주고, " +
|
|
352
|
+
"Docs/docs.config.json 의 integrations 에 항목을 추가한 뒤 project-init build 를 다시 돌려줘.";
|
|
353
|
+
|
|
354
|
+
/** 로고 타일. CDN이 막힌 환경에서는 이니셜로 떨어진다. */
|
|
355
|
+
const linkLogo = (l) =>
|
|
356
|
+
'<span class="logo" style="--brand:#' + l.color + '"><i>' + escapeHtml(l.name.charAt(0)) + "</i>" +
|
|
357
|
+
'<img src="https://cdn.simpleicons.org/' + l.slug + "/" + l.color + '" alt="" loading="lazy" onerror="this.remove()"></span>';
|
|
358
|
+
|
|
171
359
|
/* ================================================================== *
|
|
172
360
|
* 3단계 — 시안 스냅샷과 규칙 검증
|
|
173
361
|
*
|
|
@@ -891,7 +1079,49 @@ function progressClass(percent) {
|
|
|
891
1079
|
* 렌더
|
|
892
1080
|
* ================================================================== */
|
|
893
1081
|
|
|
894
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* 이 문서가 프로젝트에서 실제로 쓰였는가.
|
|
1084
|
+
*
|
|
1085
|
+
* 기획안을 근거로 메뉴는 만들어졌지만 끝까지 손대지 않는 문서가 생긴다.
|
|
1086
|
+
* 그런 문서를 "작성 중"으로 두면 진행률이 영원히 낮게 깔리고, 무엇이 남은
|
|
1087
|
+
* 일이고 무엇이 애초에 필요 없던 항목인지 구분되지 않는다.
|
|
1088
|
+
*
|
|
1089
|
+
* 판정은 네 신호 중 하나라도 걸리면 '쓰임'이다. 실측하면 갓 만든 문서는
|
|
1090
|
+
* 37~265, 한 번이라도 채운 문서는 996 이상으로 갈라진다.
|
|
1091
|
+
*/
|
|
1092
|
+
const USAGE_MIN_CHARS = 400;
|
|
1093
|
+
|
|
1094
|
+
function docUsed(doc) {
|
|
1095
|
+
// 주 신호는 **내용**이다. 상태나 링크는 템플릿이 만들어 낸 흔적일 수 있다:
|
|
1096
|
+
// log 템플릿은 status 가 approved 로 시작하고, 모든 spec 템플릿은 리스크
|
|
1097
|
+
// 대장으로 가는 링크를 자동으로 건다. 그걸 근거로 삼으면 갓 만든 대장이
|
|
1098
|
+
// 늘 '사용됨'이 된다.
|
|
1099
|
+
if (meaningfulChars(doc.body) >= USAGE_MIN_CHARS) return true;
|
|
1100
|
+
|
|
1101
|
+
// 내용이 적어도 사람이 손댄 흔적이 있으면 사용으로 본다
|
|
1102
|
+
if (doc.meta.template !== "log" && doc.meta.status && doc.meta.status !== "draft") {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
const outside = stripCode(doc.body || ""); // 템플릿 예시 ID 는 제외
|
|
1106
|
+
for (const id of doc.refs?.defined || []) {
|
|
1107
|
+
if (outside.includes(id)) return true;
|
|
1108
|
+
}
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** 템플릿 골격(제목·지침·빈 표·TODO)을 뺀 실질 글자 수. */
|
|
1113
|
+
function meaningfulChars(body = "") {
|
|
1114
|
+
return body
|
|
1115
|
+
.replace(/^---\n[\s\S]*?\n---\n/, "")
|
|
1116
|
+
.replace(/^#{1,4} .*$/gm, "")
|
|
1117
|
+
.replace(/^\|[\s|:-]*\|\s*$/gm, "")
|
|
1118
|
+
.replace(/TODO\([a-z]+\)/g, "")
|
|
1119
|
+
.replace(/^> .*$/gm, "")
|
|
1120
|
+
.replace(/\|\s*\|/g, "")
|
|
1121
|
+
.replace(/[\s|·—▪•\-*`>]+/g, "").length;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function render(docs, config, trace, discovery = [], design = [], mockups = [], styleguide = null, docsDir = "", build = [], links = []) {
|
|
895
1125
|
const byId = new Map([...discovery, ...design, ...docs].map((d) => [d.id, d]));
|
|
896
1126
|
const resolveLink = (target) => {
|
|
897
1127
|
const d = byId.get(target);
|
|
@@ -910,14 +1140,17 @@ function render(docs, config, trace, discovery = [], design = [], mockups = [],
|
|
|
910
1140
|
g.docs.push(d);
|
|
911
1141
|
}
|
|
912
1142
|
|
|
913
|
-
const navLink = (id, label, status) =>
|
|
914
|
-
'<a class="nav-item
|
|
1143
|
+
const navLink = (id, label, status, used) =>
|
|
1144
|
+
'<a class="nav-item' +
|
|
1145
|
+
(used === false ? " unused" : "") +
|
|
1146
|
+
'" data-id="' +
|
|
915
1147
|
id +
|
|
1148
|
+
(used === undefined ? "" : '" data-used="' + (used ? "1" : "0")) +
|
|
916
1149
|
'" href="#/' +
|
|
917
1150
|
id +
|
|
918
1151
|
'"><span class="nav-dot s-' +
|
|
919
1152
|
status +
|
|
920
|
-
'"></span><span>' +
|
|
1153
|
+
'"></span><span class="nav-label">' +
|
|
921
1154
|
escapeHtml(label) +
|
|
922
1155
|
"</span></a>";
|
|
923
1156
|
|
|
@@ -997,7 +1230,9 @@ function render(docs, config, trace, discovery = [], design = [], mockups = [],
|
|
|
997
1230
|
escapeHtml(p.name) +
|
|
998
1231
|
"</div>" +
|
|
999
1232
|
p.docs
|
|
1000
|
-
.map((d) =>
|
|
1233
|
+
.map((d) =>
|
|
1234
|
+
navLink(d.id, d.meta.title || d.id, d.meta.status || "draft", docUsed(d)),
|
|
1235
|
+
)
|
|
1001
1236
|
.join("") +
|
|
1002
1237
|
"</div>",
|
|
1003
1238
|
)
|
|
@@ -1006,7 +1241,94 @@ function render(docs, config, trace, discovery = [], design = [], mockups = [],
|
|
|
1006
1241
|
stageTitle(4, "구현") + '<div class="nav-empty">' +
|
|
1007
1242
|
(progress[4] ? "구현 진행 중" : "아직 산출물 없음") + "</div></div>";
|
|
1008
1243
|
|
|
1009
|
-
|
|
1244
|
+
|
|
1245
|
+
/* --- 연동 서비스 --- */
|
|
1246
|
+
/* 좌측에는 **관리 메뉴 하나만** 둔다.
|
|
1247
|
+
서비스를 전부 나열하면 문서 메뉴보다 길어져 사이드바가 연동 목록판이 된다.
|
|
1248
|
+
목록은 연동 서비스 페이지가 맡는다. 여기서는 몇 개가 붙었는지만 보여준다. */
|
|
1249
|
+
const linkedCount = links.filter((l) => l.connected).length;
|
|
1250
|
+
const linksNav =
|
|
1251
|
+
'<div class="nav-group nav-links">' +
|
|
1252
|
+
'<a class="nav-item" data-id="__links" href="#/__links">' +
|
|
1253
|
+
'<span class="nav-dot s-utility"></span><span>연동 서비스 관리</span><span class="nav-progress ' +
|
|
1254
|
+
progressClass(links.length ? Math.round((linkedCount / links.length) * 100) : 0) + '">' +
|
|
1255
|
+
linkedCount + "/" + links.length + "</span></a>" +
|
|
1256
|
+
"</div>";
|
|
1257
|
+
|
|
1258
|
+
const SRC_PILL = { "설정": "s-approved", "자동 감지": "s-review", "기획": "s-review", "추천": "s-draft" };
|
|
1259
|
+
const linkCard = (l) =>
|
|
1260
|
+
'<div class="link-card' + (l.connected ? " on" : "") + '">' +
|
|
1261
|
+
'<div class="link-head">' + linkLogo(l) +
|
|
1262
|
+
'<span class="link-name">' + escapeHtml(l.name) + "</span>" +
|
|
1263
|
+
'<span class="pill ' + (SRC_PILL[l.source] || "s-draft") + '">' + escapeHtml(l.source || "") + "</span>" +
|
|
1264
|
+
'<span class="pill ' + (l.connected ? "s-approved" : "s-draft") + '">' +
|
|
1265
|
+
(l.connected ? "연결됨" : "미연동") + "</span></div>" +
|
|
1266
|
+
'<p class="card-summary">' + escapeHtml(l.what || "") +
|
|
1267
|
+
(l.note ? " — " + escapeHtml(l.note) : "") + "</p>" +
|
|
1268
|
+
(l.url ? '<div class="link-url">' + escapeHtml(l.url) + "</div>" : "") +
|
|
1269
|
+
'<div class="link-actions">' +
|
|
1270
|
+
(l.connected && l.url
|
|
1271
|
+
? '<a class="link-btn primary" href="' + escapeHtml(l.url) + '" target="_blank" rel="noreferrer">열기 ↗</a>'
|
|
1272
|
+
: "") +
|
|
1273
|
+
'<button class="link-btn" type="button" data-copy="' + escapeHtml(l.prompt) + '">' +
|
|
1274
|
+
(l.connected ? "점검 요청 복사" : "연동 방법 묻기 복사") + "</button></div></div>";
|
|
1275
|
+
|
|
1276
|
+
const linksArticle =
|
|
1277
|
+
'<article class="doc" data-id="__links" data-title="연동 서비스"><div class="doc-inner"><div class="doc-body">' +
|
|
1278
|
+
"<h1>연동 서비스</h1>" +
|
|
1279
|
+
'<p class="home-lead">이 프로젝트가 밖으로 물려 있는 곳들입니다. ' +
|
|
1280
|
+
"연결된 카드는 눌러서 바로 이동하고, 아직 안 붙은 카드는 <strong>연동 방법 묻기</strong>를 눌러 " +
|
|
1281
|
+
"문구를 복사한 뒤 AI에게 그대로 물어보면 됩니다.</p>" +
|
|
1282
|
+
(links.every((l) => l.source === "추천")
|
|
1283
|
+
? '<p class="home-lead links-hint"><strong>아직 정해진 연동이 없어 추천 목록을 보여줍니다.</strong> ' +
|
|
1284
|
+
"기획 문서에 쓸 서비스를 적으면(예: “GCP 에 배포한다”) 다음 빌드부터 이 자리에 그것이 올라옵니다. " +
|
|
1285
|
+
"확정된 것은 <code>Docs/docs.config.json</code> 의 <code>integrations</code> 에 적습니다.</p>"
|
|
1286
|
+
: '<p class="home-lead links-hint">목록은 <strong>설정 → 자동 감지 → 기획 본문</strong> 순으로 모읍니다. ' +
|
|
1287
|
+
"<code>기획</code> 딱지가 붙은 카드는 문서에 언급되어 자동으로 올라온 것이라 " +
|
|
1288
|
+
"오탐일 수 있습니다 — 아니면 <code>integrations</code> 에서 빼면 됩니다.</p>") +
|
|
1289
|
+
'<div class="links-grid">' +
|
|
1290
|
+
links.map(linkCard).join("") +
|
|
1291
|
+
'<div class="link-card link-add"><div class="link-head"><span class="logo logo-add"><i>+</i></span>' +
|
|
1292
|
+
'<span class="link-name">새 서비스 추가</span></div>' +
|
|
1293
|
+
'<p class="card-summary">카탈로그에 없는 서비스도 붙일 수 있습니다.</p>' +
|
|
1294
|
+
'<div class="link-actions"><button class="link-btn" type="button" data-copy="' +
|
|
1295
|
+
escapeHtml(LINK_ADD_PROMPT) + '">추가 문의 문구 복사</button></div></div>' +
|
|
1296
|
+
"</div></div></div></article>";
|
|
1297
|
+
|
|
1298
|
+
/* 산출물 대본.
|
|
1299
|
+
*
|
|
1300
|
+
* 코드가 아니라 **AI 에게 주는 지시문**이다. 복사해서 에이전트에 붙여 넣으면
|
|
1301
|
+
* 그 형식으로 산출물을 만든다. 서식 규격(`_style.md`)이 대본 안에 박혀 있으므로
|
|
1302
|
+
* 착수 때 만들든 중간에 만들든, 누가 어떤 도구로 만들든 같은 모양이 나온다.
|
|
1303
|
+
*
|
|
1304
|
+
* 대본은 원본 .md 경로만 가리킨다. 내용을 품지 않으므로 문서를 고친 뒤 같은
|
|
1305
|
+
* 대본을 다시 쓰면 최신 내용으로 만들어진다. HTML 크기도 문서 수와 무관하다. */
|
|
1306
|
+
const SCRIPT_FORMATS = [
|
|
1307
|
+
{ id: "pdf", label: "PDF", ext: "pdf" },
|
|
1308
|
+
{ id: "excel", label: "Excel (XLSX)", ext: "xlsx" },
|
|
1309
|
+
{ id: "pptx", label: "PowerPoint (PPTX)", ext: "pptx" },
|
|
1310
|
+
{ id: "docx", label: "Word (DOCX)", ext: "docx" },
|
|
1311
|
+
{ id: "csv", label: "CSV", ext: "csv" },
|
|
1312
|
+
];
|
|
1313
|
+
|
|
1314
|
+
function loadScripts(project) {
|
|
1315
|
+
const dir = join(SKILL_DIR, "templates", "script");
|
|
1316
|
+
const stylePath = join(dir, "_style.md");
|
|
1317
|
+
if (!existsSync(stylePath)) return null;
|
|
1318
|
+
const style = readFileSync(stylePath, "utf8").trim();
|
|
1319
|
+
const out = {};
|
|
1320
|
+
for (const f of SCRIPT_FORMATS) {
|
|
1321
|
+
const path = join(dir, f.id + ".md");
|
|
1322
|
+
if (!existsSync(path)) continue;
|
|
1323
|
+
out[f.id] = readFileSync(path, "utf8")
|
|
1324
|
+
.replace("__STYLE__", style)
|
|
1325
|
+
.split("__PROJECT__").join(project || "이");
|
|
1326
|
+
}
|
|
1327
|
+
return Object.keys(out).length ? out : null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/* --- 문서 본문 (1단계 기록 + 2단계 문서) --- */
|
|
1331
|
+
const scripts = loadScripts(config.project);
|
|
1010
1332
|
const searchIndex = [];
|
|
1011
1333
|
const articles = [...discovery, ...design, ...docs]
|
|
1012
1334
|
.map((d) => {
|
|
@@ -1087,7 +1409,15 @@ function render(docs, config, trace, discovery = [], design = [], mockups = [],
|
|
|
1087
1409
|
escapeHtml(d.meta.updated || d.meta.date || "—") +
|
|
1088
1410
|
'</span><span class="meta-sep"></span><span class="mono">' +
|
|
1089
1411
|
escapeHtml(d.file) +
|
|
1090
|
-
"</span
|
|
1412
|
+
"</span>" +
|
|
1413
|
+
(stage1
|
|
1414
|
+
? ""
|
|
1415
|
+
: '<button class="doc-export" type="button" data-export-file="' +
|
|
1416
|
+
escapeHtml(d.file) +
|
|
1417
|
+
'" data-export-title="' +
|
|
1418
|
+
escapeHtml(d.meta.title || d.id) +
|
|
1419
|
+
'">파일 스크립트</button>') +
|
|
1420
|
+
"</div>" +
|
|
1091
1421
|
html +
|
|
1092
1422
|
definedHtml +
|
|
1093
1423
|
backHtml +
|
|
@@ -1317,9 +1647,15 @@ function render(docs, config, trace, discovery = [], design = [], mockups = [],
|
|
|
1317
1647
|
'<span class="search-icon">' + ICON.search + "</span>" +
|
|
1318
1648
|
'<input id="search" type="search" placeholder="문서 검색" autocomplete="off" spellcheck="false">' +
|
|
1319
1649
|
'<kbd class="search-kbd">/</kbd></div></div>' +
|
|
1650
|
+
'<div class="nav-tools">' +
|
|
1651
|
+
'<label class="nav-toggle"><input type="checkbox" id="inactiveMode">' +
|
|
1652
|
+
'<span>비활성 관리</span></label>' +
|
|
1653
|
+
'<span class="nav-count" id="inactiveCount"></span>' +
|
|
1654
|
+
"</div>" +
|
|
1320
1655
|
'<div class="nav" id="nav">' +
|
|
1321
1656
|
guideNav +
|
|
1322
1657
|
navLink("__home", "홈", "home") +
|
|
1658
|
+
linksNav +
|
|
1323
1659
|
navLink("__trace", "추적성 매트릭스", "utility") +
|
|
1324
1660
|
discoveryNav +
|
|
1325
1661
|
sidebar +
|
|
@@ -1341,10 +1677,40 @@ function render(docs, config, trace, discovery = [], design = [], mockups = [],
|
|
|
1341
1677
|
'<div class="doc-body"><h1>검색</h1><div id="results"></div></div></div></article>' +
|
|
1342
1678
|
guideArticle +
|
|
1343
1679
|
home +
|
|
1680
|
+
linksArticle +
|
|
1344
1681
|
mockupArticle +
|
|
1345
1682
|
traceArticle +
|
|
1346
1683
|
articles +
|
|
1347
1684
|
"</main>" +
|
|
1685
|
+
(scripts
|
|
1686
|
+
? '<div class="modal" id="exportModal" hidden>' +
|
|
1687
|
+
'<div class="modal-back" data-close-export></div>' +
|
|
1688
|
+
'<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="exportTitle">' +
|
|
1689
|
+
'<div class="modal-head">' +
|
|
1690
|
+
'<div><div class="modal-title" id="exportTitle">파일 스크립트 — 산출물 생성 대본</div>' +
|
|
1691
|
+
'<div class="modal-sub" id="exportSub"></div></div>' +
|
|
1692
|
+
'<button class="icon-btn" type="button" data-close-export aria-label="닫기">\u2715</button>' +
|
|
1693
|
+
"</div>" +
|
|
1694
|
+
'<div class="modal-bar">' +
|
|
1695
|
+
'<label class="modal-label" for="exportFormat">형식</label>' +
|
|
1696
|
+
'<select class="modal-select" id="exportFormat">' +
|
|
1697
|
+
SCRIPT_FORMATS.filter((f) => scripts[f.id])
|
|
1698
|
+
.map((f) => '<option value="' + f.id + '">' + f.label + "</option>")
|
|
1699
|
+
.join("") +
|
|
1700
|
+
"</select>" +
|
|
1701
|
+
'<span class="modal-install" id="exportInstall"></span>' +
|
|
1702
|
+
'<button class="modal-copy" type="button" id="exportCopy">대본 복사</button>' +
|
|
1703
|
+
"</div>" +
|
|
1704
|
+
'<div class="modal-body"><pre class="modal-code" id="exportCode"></pre></div>' +
|
|
1705
|
+
'<div class="modal-foot">복사해서 AI 에게 그대로 붙여 넣으면 됩니다. ' +
|
|
1706
|
+
'대본은 원본 <span class="mono" id="exportFile"></span> 을 가리킬 뿐 내용을 품지 않으므로, ' +
|
|
1707
|
+
"문서를 고친 뒤 같은 대본을 다시 써도 최신 내용으로 만들어집니다." +
|
|
1708
|
+
"</div></div></div>" +
|
|
1709
|
+
'<script id="exportScripts" type="application/json">' +
|
|
1710
|
+
JSON.stringify({ scripts: scripts, formats: SCRIPT_FORMATS })
|
|
1711
|
+
.replace(/</g, "\\u003c") +
|
|
1712
|
+
"<\/script>"
|
|
1713
|
+
: "") +
|
|
1348
1714
|
'<script id="searchIndex" type="application/json">' +
|
|
1349
1715
|
JSON.stringify(searchIndex).replace(/</g, "\\u003c") +
|
|
1350
1716
|
"<\/script>" +
|
|
@@ -1380,6 +1746,82 @@ const CSS = `
|
|
|
1380
1746
|
--s-draft:#979a9b; --s-review:#d9730d; --s-approved:#4dab9a;
|
|
1381
1747
|
--alert-bg:#2d1f1c; --alert-line:#5c332a; --alert-tx:#ff9b8a;
|
|
1382
1748
|
}
|
|
1749
|
+
|
|
1750
|
+
/* ── 파일 스크립트 버튼 · 모달 ─────────────────────────────── */
|
|
1751
|
+
.doc-export{margin-left:auto; font:inherit; font-size:12px; font-weight:500;
|
|
1752
|
+
color:var(--tx-dim); background:transparent; border:1px solid var(--line-strong);
|
|
1753
|
+
border-radius:6px; padding:3px 10px; cursor:pointer; white-space:nowrap;
|
|
1754
|
+
transition:background .12s, color .12s, border-color .12s}
|
|
1755
|
+
.doc-export:hover{background:var(--hover); color:var(--tx); border-color:var(--tx-faint)}
|
|
1756
|
+
.modal[hidden]{display:none}
|
|
1757
|
+
.modal{position:fixed; inset:0; z-index:90; display:flex; align-items:center;
|
|
1758
|
+
justify-content:center; padding:24px}
|
|
1759
|
+
.modal-back{position:absolute; inset:0; background:rgba(15,15,15,.45);
|
|
1760
|
+
backdrop-filter:blur(2px); animation:mfade .16s var(--ease)}
|
|
1761
|
+
.modal-card{position:relative; display:flex; flex-direction:column;
|
|
1762
|
+
width:min(880px,100%); max-height:min(86vh,760px); background:var(--bg);
|
|
1763
|
+
border:1px solid var(--line-strong); border-radius:14px; overflow:hidden;
|
|
1764
|
+
box-shadow:0 24px 64px rgba(15,15,15,.22); animation:mrise .2s var(--ease)}
|
|
1765
|
+
@keyframes mfade{from{opacity:0}}
|
|
1766
|
+
@keyframes mrise{from{opacity:0; transform:translateY(10px) scale(.985)}}
|
|
1767
|
+
.modal-head{display:flex; align-items:flex-start; gap:16px; padding:18px 20px 14px;
|
|
1768
|
+
border-bottom:1px solid var(--line)}
|
|
1769
|
+
.modal-title{font-size:16px; font-weight:600; letter-spacing:-.01em}
|
|
1770
|
+
.modal-sub{font-size:12.5px; color:var(--tx-dim); margin-top:2px}
|
|
1771
|
+
.modal-head .icon-btn{margin-left:auto; flex:none}
|
|
1772
|
+
.modal-bar{display:flex; align-items:center; gap:10px; flex-wrap:wrap;
|
|
1773
|
+
padding:12px 20px; background:var(--bg-side); border-bottom:1px solid var(--line)}
|
|
1774
|
+
.modal-label{font-size:12px; color:var(--tx-dim); font-weight:500}
|
|
1775
|
+
.modal-select{font:inherit; font-size:13px; color:var(--tx); background:var(--bg);
|
|
1776
|
+
border:1px solid var(--line-strong); border-radius:7px; padding:5px 10px; cursor:pointer}
|
|
1777
|
+
.modal-install{font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace);
|
|
1778
|
+
font-size:11.5px; color:var(--tx-dim); background:var(--bg-code);
|
|
1779
|
+
border-radius:5px; padding:3px 8px}
|
|
1780
|
+
.modal-copy{margin-left:auto; font:inherit; font-size:12.5px; font-weight:600;
|
|
1781
|
+
color:#fff; background:var(--accent); border:0; border-radius:7px;
|
|
1782
|
+
padding:6px 14px; cursor:pointer}
|
|
1783
|
+
.modal-copy:hover{filter:brightness(1.06)}
|
|
1784
|
+
.modal-body{overflow:auto; background:var(--bg-code)}
|
|
1785
|
+
.modal-code{margin:0; padding:18px 20px; font-size:12px; line-height:1.65;
|
|
1786
|
+
white-space:pre; color:var(--tx);
|
|
1787
|
+
font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace)}
|
|
1788
|
+
.modal-foot{padding:11px 20px; font-size:12px; color:var(--tx-dim);
|
|
1789
|
+
border-top:1px solid var(--line)}
|
|
1790
|
+
@media(max-width:700px){
|
|
1791
|
+
.modal{padding:10px}
|
|
1792
|
+
.modal-copy{margin-left:0}
|
|
1793
|
+
.modal-code{font-size:11px}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/* ── 비활성 메뉴 ─────────────────────────────────────────── */
|
|
1797
|
+
.nav-tools{display:flex; align-items:center; gap:8px; padding:2px 14px 8px}
|
|
1798
|
+
.nav-toggle{display:inline-flex; align-items:center; gap:6px; cursor:pointer;
|
|
1799
|
+
font-size:11.5px; color:var(--tx-faint); user-select:none}
|
|
1800
|
+
.nav-toggle input{width:12px; height:12px; margin:0; cursor:pointer; accent-color:var(--accent)}
|
|
1801
|
+
.nav-toggle:hover{color:var(--tx-dim)}
|
|
1802
|
+
.nav-count{margin-left:auto; font-size:10.5px; color:var(--tx-faint); font-variant-numeric:tabular-nums}
|
|
1803
|
+
/* 한 번도 쓰이지 않은 메뉴 — 노란 점이 아니라 검정 점으로 죽여 둔다 */
|
|
1804
|
+
.nav-item.unused .nav-dot,
|
|
1805
|
+
.nav-group.p-yellow .nav-item.unused .nav-dot,
|
|
1806
|
+
.nav-group.p-red .nav-item.unused .nav-dot,
|
|
1807
|
+
.nav-group.p-orange .nav-item.unused .nav-dot,
|
|
1808
|
+
.nav-group.p-green .nav-item.unused .nav-dot,
|
|
1809
|
+
.nav-group.p-gray .nav-item.unused .nav-dot{
|
|
1810
|
+
background:#000; box-shadow:0 0 0 1px var(--line-strong)}
|
|
1811
|
+
[data-theme=dark] .nav-item.unused .nav-dot,
|
|
1812
|
+
[data-theme=dark] .nav-group .nav-item.unused .nav-dot{
|
|
1813
|
+
background:#000; box-shadow:0 0 0 1px rgba(255,255,255,.22)}
|
|
1814
|
+
.nav-item.unused .nav-label{color:var(--tx-faint)}
|
|
1815
|
+
.nav-item.unused:hover .nav-label{color:var(--tx-dim)}
|
|
1816
|
+
/* 관리 모드 */
|
|
1817
|
+
body.inactive-mode .nav-item{position:relative; padding-left:30px}
|
|
1818
|
+
body.inactive-mode .nav-item .nav-dot{position:absolute; left:18px}
|
|
1819
|
+
.nav-check{position:absolute; left:7px; top:50%; transform:translateY(-50%);
|
|
1820
|
+
width:12px; height:12px; margin:0; cursor:pointer; accent-color:var(--accent);
|
|
1821
|
+
display:none}
|
|
1822
|
+
body.inactive-mode .nav-check{display:block}
|
|
1823
|
+
body.inactive-mode .nav-tools{background:var(--hover); border-radius:7px;
|
|
1824
|
+
margin:0 6px 6px; padding:6px 8px}
|
|
1383
1825
|
html{-webkit-text-size-adjust:100%}
|
|
1384
1826
|
body{
|
|
1385
1827
|
margin:0; background:var(--bg); color:var(--tx);
|
|
@@ -1460,6 +1902,29 @@ a{color:inherit}
|
|
|
1460
1902
|
.nav-group.nav-stage3{padding-bottom:12px; border-bottom:1px solid var(--line)}
|
|
1461
1903
|
.panel-stage3{border-color:var(--stage3)}
|
|
1462
1904
|
.panel-stage3 .panel-title{color:var(--stage3)}
|
|
1905
|
+
.logo{position:relative; width:18px; height:18px; flex:none; border-radius:4px; background:#fff;
|
|
1906
|
+
border:1px solid var(--line-strong); overflow:hidden}
|
|
1907
|
+
.logo i{position:absolute; inset:0; display:grid; place-items:center; font-style:normal;
|
|
1908
|
+
font-size:10px; font-weight:700; color:var(--brand,#555)}
|
|
1909
|
+
.logo img{position:relative; display:block; width:100%; height:100%; padding:3px; object-fit:contain}
|
|
1910
|
+
.logo-add i{color:var(--tx-faint); font-size:13px}
|
|
1911
|
+
.links-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(250px,1fr)); gap:10px; margin-top:22px}
|
|
1912
|
+
.link-card{display:flex; flex-direction:column; gap:6px; padding:14px 16px;
|
|
1913
|
+
border:1px solid var(--line-strong); border-radius:8px; background:var(--bg)}
|
|
1914
|
+
.link-card.on{border-color:var(--green)}
|
|
1915
|
+
.link-card.link-add{border-style:dashed}
|
|
1916
|
+
.link-head{display:flex; align-items:center; gap:8px}
|
|
1917
|
+
.link-head .logo{width:22px; height:22px}
|
|
1918
|
+
.link-name{font-weight:600; font-size:14px}
|
|
1919
|
+
.link-head .pill{margin-left:auto}
|
|
1920
|
+
.link-head .pill ~ .pill{margin-left:0}
|
|
1921
|
+
.link-url{font-size:11.5px; color:var(--tx-faint); word-break:break-all}
|
|
1922
|
+
.link-actions{display:flex; gap:6px; margin-top:auto; padding-top:6px}
|
|
1923
|
+
.link-btn{display:inline-flex; align-items:center; justify-content:center; padding:5px 10px;
|
|
1924
|
+
border:1px solid var(--line-strong); border-radius:6px; background:transparent; cursor:pointer;
|
|
1925
|
+
font:inherit; font-size:12px; color:var(--tx-dim); text-decoration:none; white-space:nowrap}
|
|
1926
|
+
.link-btn:hover{background:var(--hover); color:var(--tx)}
|
|
1927
|
+
.link-btn.primary{border-color:var(--accent); color:var(--accent)}
|
|
1463
1928
|
.mockups{display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:10px; margin-top:20px}
|
|
1464
1929
|
.mockup{display:flex; align-items:center; justify-content:space-between; gap:10px;
|
|
1465
1930
|
padding:14px 16px; border:1px solid var(--line-strong); border-radius:8px;
|
|
@@ -1643,6 +2108,7 @@ a.panel-row:hover{color:var(--tx)}
|
|
|
1643
2108
|
.doc-body h1{font-size:30px}
|
|
1644
2109
|
.doc-body h2{font-size:21px}
|
|
1645
2110
|
.cards{grid-template-columns:1fr}
|
|
2111
|
+
.links-grid{grid-template-columns:1fr}
|
|
1646
2112
|
}
|
|
1647
2113
|
@media (prefers-reduced-motion:reduce){ *{transition-duration:.01ms !important; animation-duration:.01ms !important} }
|
|
1648
2114
|
`;
|
|
@@ -1709,6 +2175,199 @@ const JS = `
|
|
|
1709
2175
|
if(el){ e.preventDefault(); el.scrollIntoView({behavior:'smooth', block:'start'}); }
|
|
1710
2176
|
});
|
|
1711
2177
|
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
/* ── 비활성 메뉴 관리 ────────────────────────────
|
|
2181
|
+
* 자동 판정(data-used=0)이 기본이고, 사용자가 체크로 덮어쓴다.
|
|
2182
|
+
* index.html 은 빌드마다 새로 만들어지므로 덮어쓴 값은 문서 id 로
|
|
2183
|
+
* localStorage 에 둔다 — 다시 빌드해도 유지된다. */
|
|
2184
|
+
(function(){
|
|
2185
|
+
var box = document.getElementById('inactiveMode');
|
|
2186
|
+
var nav = document.getElementById('nav');
|
|
2187
|
+
var count = document.getElementById('inactiveCount');
|
|
2188
|
+
if(!box || !nav) return;
|
|
2189
|
+
var KEY = 'inactiveDocs';
|
|
2190
|
+
|
|
2191
|
+
function load(){
|
|
2192
|
+
try { return JSON.parse(localStorage.getItem(KEY)) || {}; }
|
|
2193
|
+
catch(e){ return {}; }
|
|
2194
|
+
}
|
|
2195
|
+
function save(map){
|
|
2196
|
+
try { localStorage.setItem(KEY, JSON.stringify(map)); } catch(e){}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
var overrides = load();
|
|
2200
|
+
var items = [].slice.call(nav.querySelectorAll('.nav-item[data-used]'));
|
|
2201
|
+
|
|
2202
|
+
function isInactive(item){
|
|
2203
|
+
var id = item.getAttribute('data-id');
|
|
2204
|
+
if(Object.prototype.hasOwnProperty.call(overrides, id)) return !!overrides[id];
|
|
2205
|
+
return item.getAttribute('data-used') === '0';
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function paint(){
|
|
2209
|
+
var n = 0;
|
|
2210
|
+
items.forEach(function(item){
|
|
2211
|
+
var off = isInactive(item);
|
|
2212
|
+
item.classList.toggle('unused', off);
|
|
2213
|
+
var cb = item.querySelector('.nav-check');
|
|
2214
|
+
if(cb) cb.checked = off;
|
|
2215
|
+
if(off) n++;
|
|
2216
|
+
});
|
|
2217
|
+
count.textContent = n ? ('비활성 ' + n + ' / ' + items.length) : '';
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// 관리 모드에서 쓸 체크박스를 미리 넣어 둔다 (CSS 로 보였다 숨긴다)
|
|
2221
|
+
items.forEach(function(item){
|
|
2222
|
+
var cb = document.createElement('input');
|
|
2223
|
+
cb.type = 'checkbox';
|
|
2224
|
+
cb.className = 'nav-check';
|
|
2225
|
+
cb.title = '비활성으로 표시';
|
|
2226
|
+
item.insertBefore(cb, item.firstChild);
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
/* 체크박스가 <a> 안에 있어 클릭이 그대로 두면 문서 이동까지 간다.
|
|
2230
|
+
* 문서 캡처 단계에서 먼저 가로채 라우터보다 앞서 끊는다. */
|
|
2231
|
+
document.addEventListener('click', function(e){
|
|
2232
|
+
var cb = e.target;
|
|
2233
|
+
if(!cb.classList || !cb.classList.contains('nav-check')) return;
|
|
2234
|
+
e.preventDefault();
|
|
2235
|
+
e.stopPropagation();
|
|
2236
|
+
if(e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2237
|
+
var item = cb.closest('.nav-item');
|
|
2238
|
+
if(!item) return;
|
|
2239
|
+
var id = item.getAttribute('data-id');
|
|
2240
|
+
var next = !isInactive(item);
|
|
2241
|
+
var auto = item.getAttribute('data-used') === '0';
|
|
2242
|
+
// 자동 판정과 같아지면 덮어쓰기를 지운다 — 쓸데없는 저장을 남기지 않는다
|
|
2243
|
+
if(next === auto) delete overrides[id];
|
|
2244
|
+
else overrides[id] = next;
|
|
2245
|
+
save(overrides);
|
|
2246
|
+
paint();
|
|
2247
|
+
}, true);
|
|
2248
|
+
|
|
2249
|
+
box.addEventListener('change', function(){
|
|
2250
|
+
document.body.classList.toggle('inactive-mode', box.checked);
|
|
2251
|
+
try { localStorage.setItem('inactiveMode', box.checked ? '1' : ''); } catch(e){}
|
|
2252
|
+
});
|
|
2253
|
+
try {
|
|
2254
|
+
if(localStorage.getItem('inactiveMode')){
|
|
2255
|
+
box.checked = true;
|
|
2256
|
+
document.body.classList.add('inactive-mode');
|
|
2257
|
+
}
|
|
2258
|
+
} catch(e){}
|
|
2259
|
+
|
|
2260
|
+
paint();
|
|
2261
|
+
})();
|
|
2262
|
+
|
|
2263
|
+
/* ── 파일 스크립트 모달 ──────────────────────────── */
|
|
2264
|
+
(function(){
|
|
2265
|
+
var node = document.getElementById('exportScripts');
|
|
2266
|
+
var modal = document.getElementById('exportModal');
|
|
2267
|
+
if(!node || !modal) return;
|
|
2268
|
+
var data = JSON.parse(node.textContent);
|
|
2269
|
+
var sel = document.getElementById('exportFormat');
|
|
2270
|
+
var code = document.getElementById('exportCode');
|
|
2271
|
+
var install = document.getElementById('exportInstall');
|
|
2272
|
+
var sub = document.getElementById('exportSub');
|
|
2273
|
+
var fileEl = document.getElementById('exportFile');
|
|
2274
|
+
var copyBtn = document.getElementById('exportCopy');
|
|
2275
|
+
var cur = { file: '', title: '' };
|
|
2276
|
+
var lastFocus = null;
|
|
2277
|
+
|
|
2278
|
+
function fmt(id){
|
|
2279
|
+
for(var i=0;i<data.formats.length;i++){ if(data.formats[i].id===id) return data.formats[i]; }
|
|
2280
|
+
return data.formats[0];
|
|
2281
|
+
}
|
|
2282
|
+
function stem(file){ return file.replace(/\.[^.]+$/, ''); }
|
|
2283
|
+
|
|
2284
|
+
function render(){
|
|
2285
|
+
var f = fmt(sel.value);
|
|
2286
|
+
var src = data.scripts[f.id] || '';
|
|
2287
|
+
code.textContent = src
|
|
2288
|
+
.split('__DOC_FILE__').join(cur.file)
|
|
2289
|
+
.split('__DOC_TITLE__').join(cur.title)
|
|
2290
|
+
.split('__DOC_STEM__').join(stem(cur.file));
|
|
2291
|
+
install.textContent = stem(cur.file) + '.' + f.ext;
|
|
2292
|
+
try { localStorage.setItem('exportFormat', f.id); } catch(e){}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function open(btn){
|
|
2296
|
+
cur.file = btn.getAttribute('data-export-file') || '';
|
|
2297
|
+
cur.title = btn.getAttribute('data-export-title') || '';
|
|
2298
|
+
sub.textContent = cur.title;
|
|
2299
|
+
fileEl.textContent = cur.file;
|
|
2300
|
+
try {
|
|
2301
|
+
var saved = localStorage.getItem('exportFormat');
|
|
2302
|
+
if(saved && data.scripts[saved]) sel.value = saved;
|
|
2303
|
+
} catch(e){}
|
|
2304
|
+
render();
|
|
2305
|
+
lastFocus = btn;
|
|
2306
|
+
modal.hidden = false;
|
|
2307
|
+
document.body.style.overflow = 'hidden';
|
|
2308
|
+
sel.focus();
|
|
2309
|
+
}
|
|
2310
|
+
function close(){
|
|
2311
|
+
modal.hidden = true;
|
|
2312
|
+
document.body.style.overflow = '';
|
|
2313
|
+
if(lastFocus) lastFocus.focus();
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
document.addEventListener('click', function(e){
|
|
2317
|
+
var btn = e.target.closest && e.target.closest('[data-export-file]');
|
|
2318
|
+
if(btn){ e.preventDefault(); open(btn); return; }
|
|
2319
|
+
if(e.target.closest && e.target.closest('[data-close-export]')) close();
|
|
2320
|
+
});
|
|
2321
|
+
document.addEventListener('keydown', function(e){
|
|
2322
|
+
if(e.key === 'Escape' && !modal.hidden) close();
|
|
2323
|
+
});
|
|
2324
|
+
sel.addEventListener('change', render);
|
|
2325
|
+
|
|
2326
|
+
copyBtn.addEventListener('click', function(){
|
|
2327
|
+
var text = code.textContent;
|
|
2328
|
+
var label = copyBtn.textContent;
|
|
2329
|
+
function done(){
|
|
2330
|
+
copyBtn.textContent = '복사됨 ✓';
|
|
2331
|
+
setTimeout(function(){ copyBtn.textContent = label; }, 1600);
|
|
2332
|
+
}
|
|
2333
|
+
function fallback(){
|
|
2334
|
+
var ta = document.createElement('textarea');
|
|
2335
|
+
ta.value = text; ta.setAttribute('readonly','');
|
|
2336
|
+
ta.style.position='fixed'; ta.style.opacity='0';
|
|
2337
|
+
document.body.appendChild(ta); ta.select();
|
|
2338
|
+
try { document.execCommand('copy'); done(); }
|
|
2339
|
+
catch(err){ copyBtn.textContent = '복사 실패'; }
|
|
2340
|
+
ta.remove();
|
|
2341
|
+
}
|
|
2342
|
+
if(navigator.clipboard && navigator.clipboard.writeText){
|
|
2343
|
+
navigator.clipboard.writeText(text).then(done, fallback);
|
|
2344
|
+
} else { fallback(); }
|
|
2345
|
+
});
|
|
2346
|
+
})();
|
|
2347
|
+
|
|
2348
|
+
/* ── 연동 카드: 문구 복사 ──────────────────────── */
|
|
2349
|
+
document.addEventListener('click', function(e){
|
|
2350
|
+
var b = e.target.closest && e.target.closest('[data-copy]');
|
|
2351
|
+
if(!b) return;
|
|
2352
|
+
var text = b.getAttribute('data-copy');
|
|
2353
|
+
var label = b.textContent;
|
|
2354
|
+
function done(){
|
|
2355
|
+
b.textContent = '복사됨 ✓';
|
|
2356
|
+
setTimeout(function(){ b.textContent = label; }, 1600);
|
|
2357
|
+
}
|
|
2358
|
+
function fallback(){
|
|
2359
|
+
var ta = document.createElement('textarea');
|
|
2360
|
+
ta.value = text; ta.setAttribute('readonly','');
|
|
2361
|
+
ta.style.position = 'fixed'; ta.style.opacity = '0';
|
|
2362
|
+
document.body.appendChild(ta); ta.select();
|
|
2363
|
+
try { document.execCommand('copy'); done(); } catch(err) { b.textContent = '복사 실패'; }
|
|
2364
|
+
ta.remove();
|
|
2365
|
+
}
|
|
2366
|
+
if(navigator.clipboard && navigator.clipboard.writeText){
|
|
2367
|
+
navigator.clipboard.writeText(text).then(done, fallback);
|
|
2368
|
+
} else { fallback(); }
|
|
2369
|
+
});
|
|
2370
|
+
|
|
1712
2371
|
/* ── 전문 검색 ───────────────────────────────────── */
|
|
1713
2372
|
var search = document.getElementById('search');
|
|
1714
2373
|
var results = document.getElementById('results');
|
|
@@ -2034,9 +2693,10 @@ function cmdBuild(docsDir) {
|
|
|
2034
2693
|
const config = loadConfig(docsDir);
|
|
2035
2694
|
const mockups = loadMockups(docsDir);
|
|
2036
2695
|
const styleguide = findStyleguide(docsDir);
|
|
2696
|
+
const links = loadLinks(docsDir, config, docs);
|
|
2037
2697
|
const trace = buildTraceability([...discovery, ...design, ...docs]);
|
|
2038
2698
|
const out = join(docsDir, "index.html");
|
|
2039
|
-
writeFileSync(out, render(docs, config, trace, discovery, design, mockups, styleguide, docsDir, build), "utf8");
|
|
2699
|
+
writeFileSync(out, render(docs, config, trace, discovery, design, mockups, styleguide, docsDir, build, links), "utf8");
|
|
2040
2700
|
console.log(
|
|
2041
2701
|
"생성됨: " +
|
|
2042
2702
|
out +
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## 산출물 공통 서식 규격
|
|
2
|
+
|
|
3
|
+
**이 규격은 고정입니다.** 프로젝트 초기에 만들든 중간에 만들든, 누가 어떤 도구로
|
|
4
|
+
만들든 **같은 값을 써서 같은 모양이 나와야 합니다.** 아래 값을 임의로 바꾸지 마세요.
|
|
5
|
+
|
|
6
|
+
### 색
|
|
7
|
+
|
|
8
|
+
| 쓰임 | 값 |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| 본문 글자 | `#1C1B19` |
|
|
11
|
+
| 보조 글자 (메타·주석) | `#6B6862` |
|
|
12
|
+
| 선·테두리 | `#E4E2DD` |
|
|
13
|
+
| 강조 (제목 앞 막대, 링크) | `#2383E2` |
|
|
14
|
+
| 표 짝수행·코드 배경 | `#FAFAF8` |
|
|
15
|
+
| 표 머리행 배경 | `#1C1B19` (글자는 흰색) |
|
|
16
|
+
|
|
17
|
+
**색은 이 여섯 개가 전부입니다.** 경고나 상태 표시가 꼭 필요하면 `#C0392B`(위험),
|
|
18
|
+
`#0F7B6C`(완료) 두 개만 더 씁니다.
|
|
19
|
+
|
|
20
|
+
### 글자
|
|
21
|
+
|
|
22
|
+
- 한글 본문은 산세리프: `맑은 고딕` → `Apple SD Gothic Neo` → 시스템 기본 순서로 대체
|
|
23
|
+
- 코드·경로·ID 는 고정폭
|
|
24
|
+
- 크기 위계는 **네 단계만**: 제목 / 절 제목 / 소제목 / 본문
|
|
25
|
+
- 본문 줄간격은 1.5~1.65. 빽빽하게 붙이지 않습니다
|
|
26
|
+
|
|
27
|
+
### 표
|
|
28
|
+
|
|
29
|
+
- 머리행은 먹 배경에 흰 글자, 굵게
|
|
30
|
+
- 짝수 데이터행에 `#FAFAF8` 음영 (얼룩무늬)
|
|
31
|
+
- 테두리는 가로선 위주로 얇게. 격자를 꽉 채우지 않습니다
|
|
32
|
+
- 셀 안쪽 여백을 넉넉히 (상하 6pt 이상)
|
|
33
|
+
- 머리행은 페이지·슬라이드가 넘어가도 반복
|
|
34
|
+
|
|
35
|
+
### 여백
|
|
36
|
+
|
|
37
|
+
- 바깥 여백을 넉넉히 둡니다. 가장자리까지 채우지 않습니다
|
|
38
|
+
- 절과 절 사이는 본문 간격의 두 배 이상
|
|
39
|
+
|
|
40
|
+
### 하지 말 것
|
|
41
|
+
|
|
42
|
+
- 무지개색·그라디언트·네온
|
|
43
|
+
- 그림자, 3D, 워드아트, 반사 효과
|
|
44
|
+
- 장식용 아이콘·클립아트·이모지
|
|
45
|
+
- 본문 기울임체 (한글에서 읽기 나쁩니다)
|
|
46
|
+
- 가운데 정렬 본문 (제목만 필요하면 씁니다)
|
|
47
|
+
- 원본에 없는 내용을 지어내기
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 산출물 생성 지시 — CSV
|
|
2
|
+
|
|
3
|
+
`__PROJECT__` 프로젝트의 문서 **`__DOC_TITLE__`** 을 CSV 로 만들어 주세요.
|
|
4
|
+
|
|
5
|
+
## 원본
|
|
6
|
+
|
|
7
|
+
`__DOC_FILE__` — **이 파일을 먼저 읽고**, 그 내용만으로 만듭니다.
|
|
8
|
+
읽을 수 없으면 만들지 말고 파일을 달라고 요청하세요.
|
|
9
|
+
|
|
10
|
+
## 결과물
|
|
11
|
+
|
|
12
|
+
파일 여러 개로 나눕니다.
|
|
13
|
+
|
|
14
|
+
| 파일 | 내용 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `__DOC_STEM___개요.csv` | 본문 전체를 구조화한 것 |
|
|
17
|
+
| `__DOC_STEM___표1.csv`, `표2.csv` … | 원본의 표를 순서대로 하나씩 |
|
|
18
|
+
|
|
19
|
+
## 인코딩 — 이것만은 반드시
|
|
20
|
+
|
|
21
|
+
**UTF-8 with BOM(`utf-8-sig`) 으로 저장합니다.** BOM 이 없으면 엑셀에서 열 때
|
|
22
|
+
한글이 깨집니다. 이 한 가지가 CSV 산출물에서 가장 자주 나는 사고입니다.
|
|
23
|
+
|
|
24
|
+
줄바꿈은 `\r\n`, 값에 쉼표·따옴표·줄바꿈이 있으면 큰따옴표로 감싸고 내부
|
|
25
|
+
따옴표는 두 번 씁니다 (표준 CSV 규칙).
|
|
26
|
+
|
|
27
|
+
## 개요 CSV 형식
|
|
28
|
+
|
|
29
|
+
머리행은 `구분,섹션,하위,내용` 네 칸 고정입니다.
|
|
30
|
+
|
|
31
|
+
| 구분 | 언제 |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `본문` | 문단·인용 |
|
|
34
|
+
| `목록` | 불릿 항목 하나당 한 줄 |
|
|
35
|
+
| `코드` | 코드 블록. 줄바꿈은 ` ⏎ ` 로 바꿔 한 줄에 |
|
|
36
|
+
|
|
37
|
+
`섹션` 은 가장 가까운 `##`, `하위` 는 가장 가까운 `###`/`####` 입니다.
|
|
38
|
+
|
|
39
|
+
## 표 CSV 형식
|
|
40
|
+
|
|
41
|
+
- 원본 표의 **머리행과 행 순서를 그대로** 옮깁니다
|
|
42
|
+
- 칸 수가 행마다 다르면 가장 넓은 행에 맞춰 빈 칸으로 채웁니다
|
|
43
|
+
- 마크다운 기호(`**`, 백틱, `[[링크]]`)는 벗겨내고 글자만 남깁니다
|
|
44
|
+
|
|
45
|
+
## 지켜야 할 것
|
|
46
|
+
|
|
47
|
+
- 값을 정렬하거나 합치지 마세요. 원본 순서가 곧 의미입니다.
|
|
48
|
+
- 원본의 `TODO(...)` 는 그대로 남깁니다.
|
|
49
|
+
- 다 만든 뒤, 만든 파일 목록과 각 파일의 행 수를 알려주세요.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 산출물 생성 지시 — 워드 (DOCX)
|
|
2
|
+
|
|
3
|
+
`__PROJECT__` 프로젝트의 문서 **`__DOC_TITLE__`** 을 워드 산출물로 만들어 주세요.
|
|
4
|
+
|
|
5
|
+
## 원본
|
|
6
|
+
|
|
7
|
+
`__DOC_FILE__` — **이 파일을 먼저 읽고**, 그 내용만으로 만듭니다.
|
|
8
|
+
읽을 수 없으면 만들지 말고 파일을 달라고 요청하세요.
|
|
9
|
+
|
|
10
|
+
## 결과물
|
|
11
|
+
|
|
12
|
+
- 파일명: `__DOC_STEM__.docx`
|
|
13
|
+
- A4 세로, 위아래 여백 2.2cm, 좌우 2.4cm
|
|
14
|
+
|
|
15
|
+
__STYLE__
|
|
16
|
+
|
|
17
|
+
## 워드 고유 규칙
|
|
18
|
+
|
|
19
|
+
1. 본문 기본 10pt, 줄간격 1.5, 문단 뒤 간격 6pt.
|
|
20
|
+
2. 제목 22pt 굵게 → 바로 아래 메타 한 줄 9pt 보조색 → 빈 줄.
|
|
21
|
+
3. `##` 15pt 굵게(앞 간격 16pt), `###` 12pt 굵게, `####` 10.5pt 보조색.
|
|
22
|
+
4. **머리글 스타일(Heading 1/2/3)을 실제로 적용**해서 목차와 탐색 창이 동작하게
|
|
23
|
+
합니다. 글자 크기만 키우지 마세요.
|
|
24
|
+
5. 목록은 워드 글머리 기호 스타일을 씁니다.
|
|
25
|
+
6. 표는 `Table Grid` 기반에 공통 규격의 음영을 입힙니다. 표 뒤에 빈 문단 하나.
|
|
26
|
+
7. 인용(`>`)은 왼쪽 들여쓰기 0.6cm + 보조 글자색.
|
|
27
|
+
8. 한글 서체를 **동아시아 글꼴 속성에도 함께 지정**합니다. 이걸 빠뜨리면 한글만
|
|
28
|
+
다른 서체로 보입니다.
|
|
29
|
+
|
|
30
|
+
## 지켜야 할 것
|
|
31
|
+
|
|
32
|
+
- **원본의 문장을 바꾸지 마세요.** 서식만 입힙니다.
|
|
33
|
+
- 원본의 `TODO(...)` 는 그대로 남깁니다.
|
|
34
|
+
- 표지·목차 페이지를 임의로 추가하지 마세요. 필요하면 따로 요청합니다.
|
|
35
|
+
- 다 만든 뒤, 적용한 머리글 수준별 개수를 알려주세요.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 산출물 생성 지시 — 엑셀 (XLSX)
|
|
2
|
+
|
|
3
|
+
`__PROJECT__` 프로젝트의 문서 **`__DOC_TITLE__`** 을 엑셀 산출물로 만들어 주세요.
|
|
4
|
+
|
|
5
|
+
## 원본
|
|
6
|
+
|
|
7
|
+
`__DOC_FILE__` — **이 파일을 먼저 읽고**, 그 내용만으로 만듭니다.
|
|
8
|
+
읽을 수 없으면 만들지 말고 파일을 달라고 요청하세요.
|
|
9
|
+
|
|
10
|
+
## 결과물
|
|
11
|
+
|
|
12
|
+
- 파일명: `__DOC_STEM__.xlsx`
|
|
13
|
+
|
|
14
|
+
__STYLE__
|
|
15
|
+
|
|
16
|
+
## 시트 구성 규칙
|
|
17
|
+
|
|
18
|
+
1. **첫 시트 `문서`** — 본문이 위에서 아래로 흐릅니다.
|
|
19
|
+
- A열은 폭 3(여백용), B열은 폭 110에 내용을 담고 자동 줄바꿈
|
|
20
|
+
- 제목 20pt 굵게 → 메타 한 줄 9pt 보조색 → 빈 줄 → 본문
|
|
21
|
+
- `##` 14pt 굵게, `###` 12pt 굵게, `####` 11pt 보조색
|
|
22
|
+
- 목록은 `•` 를 붙이고 한 칸 들여쓰기
|
|
23
|
+
2. **표는 각각 별도 시트** (`표1`, `표2`, …) 로 뺍니다.
|
|
24
|
+
`문서` 시트의 그 자리에는 `→ 표1 시트 참조` 라고 강조색으로 적습니다.
|
|
25
|
+
3. 표 시트마다:
|
|
26
|
+
- 엑셀 **표 기능(Ctrl+T)** 을 적용해 정렬·필터가 켜지게 합니다
|
|
27
|
+
- 머리행 고정(틀 고정 A2)
|
|
28
|
+
- 열 너비는 내용에 맞춰 12~52 사이로
|
|
29
|
+
- 얼룩무늬 켜기
|
|
30
|
+
4. **모든 시트에서 눈금선을 끕니다.**
|
|
31
|
+
5. 수식을 넣지 않습니다. 값만 씁니다.
|
|
32
|
+
|
|
33
|
+
## 지켜야 할 것
|
|
34
|
+
|
|
35
|
+
- **표의 행 순서·칸 수·값을 바꾸지 마세요.** 정렬하거나 합치지 마세요.
|
|
36
|
+
- 빈 칸을 임의로 채우지 마세요. 원본이 비어 있으면 비워 둡니다.
|
|
37
|
+
- 원본의 `TODO(...)` 는 그대로 남깁니다.
|
|
38
|
+
- 다 만든 뒤, 시트 수와 각 표의 행 수를 알려주세요.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 산출물 생성 지시 — PDF
|
|
2
|
+
|
|
3
|
+
`__PROJECT__` 프로젝트의 문서 **`__DOC_TITLE__`** 을 PDF 산출물로 만들어 주세요.
|
|
4
|
+
|
|
5
|
+
## 원본
|
|
6
|
+
|
|
7
|
+
`__DOC_FILE__` — **이 파일을 먼저 읽고**, 그 내용만으로 만듭니다.
|
|
8
|
+
읽을 수 없으면 만들지 말고 파일을 달라고 요청하세요.
|
|
9
|
+
|
|
10
|
+
## 결과물
|
|
11
|
+
|
|
12
|
+
- 파일명: `__DOC_STEM__.pdf`
|
|
13
|
+
- 용지 A4 세로, 바깥 여백 20mm
|
|
14
|
+
|
|
15
|
+
__STYLE__
|
|
16
|
+
|
|
17
|
+
## PDF 고유 규칙
|
|
18
|
+
|
|
19
|
+
1. **표지 없이 본문부터** 시작합니다. 첫 줄이 문서 제목입니다.
|
|
20
|
+
2. 제목 바로 아래 한 줄로 메타를 넣습니다 — `문서ID · 상태 · 담당 · 수정일`,
|
|
21
|
+
보조 글자색 9pt.
|
|
22
|
+
3. 모든 쪽 상단에 머리말: 왼쪽에 문서 제목, 오른쪽에 문서 ID, 그 아래 얇은 구분선.
|
|
23
|
+
4. 모든 쪽 하단 가운데에 쪽번호.
|
|
24
|
+
5. 절 제목(`##`)은 앞에 새 쪽을 강제하지 않습니다. 다만 **제목만 남고 내용이 다음
|
|
25
|
+
쪽으로 넘어가는 일은 없게** 합니다.
|
|
26
|
+
6. 인용(`>`)은 왼쪽에 강조색 세로 막대를 두고 보조 글자색으로 씁니다.
|
|
27
|
+
7. 코드 블록은 `#FAFAF8` 배경에 고정폭, 줄바꿈하지 말고 넘치면 줄여 씁니다.
|
|
28
|
+
8. 표가 한 쪽에 안 들어가면 머리행을 반복하며 나눕니다.
|
|
29
|
+
|
|
30
|
+
## 지켜야 할 것
|
|
31
|
+
|
|
32
|
+
- **원본의 문장을 바꾸지 마세요.** 서식만 입힙니다. 요약·윤색·보완 금지.
|
|
33
|
+
- 원본에 `TODO(...)` 가 있으면 **그대로 남깁니다.** 채우지 마세요.
|
|
34
|
+
- 표의 행 순서와 칸 수를 바꾸지 마세요.
|
|
35
|
+
- 다 만든 뒤, 원본의 절 제목이 모두 들어갔는지 세어서 알려주세요.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 산출물 생성 지시 — 발표자료 (PPTX)
|
|
2
|
+
|
|
3
|
+
`__PROJECT__` 프로젝트의 문서 **`__DOC_TITLE__`** 을 발표자료로 만들어 주세요.
|
|
4
|
+
|
|
5
|
+
## 원본
|
|
6
|
+
|
|
7
|
+
`__DOC_FILE__` — **이 파일을 먼저 읽고**, 그 내용만으로 만듭니다.
|
|
8
|
+
읽을 수 없으면 만들지 말고 파일을 달라고 요청하세요.
|
|
9
|
+
|
|
10
|
+
## 결과물
|
|
11
|
+
|
|
12
|
+
- 파일명: `__DOC_STEM__.pptx`
|
|
13
|
+
- 16:9 (13.333 × 7.5 inch)
|
|
14
|
+
|
|
15
|
+
__STYLE__
|
|
16
|
+
|
|
17
|
+
## 슬라이드 구성 규칙
|
|
18
|
+
|
|
19
|
+
1. **1장: 표지** — 왼쪽 끝에 강조색 세로 막대(폭 0.18in, 높이 전체).
|
|
20
|
+
제목은 40pt 굵게, 그 아래 `문서ID · 단계 · 담당 · 수정일` 13pt 보조색,
|
|
21
|
+
그 아래 문서 요약 15pt.
|
|
22
|
+
2. **2장부터: 원본의 `##` 하나가 슬라이드 하나.** 제목을 임의로 합치거나 쪼개지
|
|
23
|
+
마세요.
|
|
24
|
+
3. 각 슬라이드 제목은 24pt 굵게, 제목 왼쪽에 강조색 짧은 막대.
|
|
25
|
+
4. 한 슬라이드에 **불릿 7개, 표 14행을 넘기지 않습니다.** 넘치면
|
|
26
|
+
`(이어서)` 제목으로 다음 슬라이드에 잇습니다.
|
|
27
|
+
5. 본문은 12pt. **문장을 통째로 넣지 말고 구(句) 단위로 끊습니다.**
|
|
28
|
+
단, 원본에 없는 말을 만들지는 마세요 — 자르기만 합니다.
|
|
29
|
+
6. 표는 공통 규격의 표 스타일 그대로. 머리행 먹 배경 흰 글자.
|
|
30
|
+
7. 애니메이션·화면전환 효과는 넣지 않습니다.
|
|
31
|
+
|
|
32
|
+
## 지켜야 할 것
|
|
33
|
+
|
|
34
|
+
- **원본에 없는 내용을 만들지 마세요.** 특히 숫자·일정·담당자.
|
|
35
|
+
- 원본의 `TODO(...)` 는 그대로 남깁니다. 발표자료라고 숨기지 마세요.
|
|
36
|
+
- 마지막에 "감사합니다" 같은 빈 슬라이드를 붙이지 마세요.
|
|
37
|
+
- 다 만든 뒤, 슬라이드 수와 원본 `##` 개수가 맞는지 알려주세요.
|